diff --git a/.travis.yml b/.travis.yml index c0db4d08..8c830da6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - 1.7.5 - - 1.8 + - 1.8.1 script: - curl -s https://raw.githubusercontent.com/pote/gpm/v1.4.0/bin/gpm > gpm - chmod +x gpm diff --git a/README.md b/README.md index e324da36..a11312f5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ to validate accounts by email, domain or group. ## Installation -1. Download [Prebuilt Binary](https://github.com/bitly/oauth2_proxy/releases) (current release is `v2.1`) or build with `$ go get github.com/bitly/oauth2_proxy` which will put the binary in `$GOROOT/bin` +1. Download [Prebuilt Binary](https://github.com/bitly/oauth2_proxy/releases) (current release is `v2.2`) or build with `$ go get github.com/bitly/oauth2_proxy` which will put the binary in `$GOROOT/bin` 2. Select a Provider and Register an OAuth Application with a Provider 3. Configure OAuth2 Proxy using config file, command line options, or environment variables 4. Configure SSL or Deploy behind a SSL endpoint (example provided for Nginx) @@ -44,7 +44,7 @@ For Google, the registration steps are: 1. Create a new project: https://console.developers.google.com/project 2. Choose the new project from the top right project dropdown (only if another project is selected) -3. In the project Dashboard center pane, choose **"Enable and manage APIs"** +3. In the project Dashboard center pane, choose **"API Manager"** 4. In the left Nav pane, choose **"Credentials"** 5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save. 6. In the center pane, choose **"Credentials"** tab. @@ -53,7 +53,7 @@ For Google, the registration steps are: * Choose **"Web application"** * Application name is freeform, choose something appropriate * Authorized JavaScript origins is your domain ex: `https://internal.yourcompany.com` - * Authorized redirect URIs is the location of oath2/callback ex: `https://internal.yourcompany.com/oauth2/callback` + * Authorized redirect URIs is the location of oauth2/callback ex: `https://internal.yourcompany.com/oauth2/callback` * Choose **"Create"** 4. Take note of the **Client ID** and **Client Secret** @@ -72,12 +72,22 @@ https://www.googleapis.com/auth/admin.directory.user.readonly ``` 6. Follow the steps on https://support.google.com/a/answer/60757 to enable Admin API access. 7. Create or choose an existing administrative email address on the Gmail domain to assign to the ```google-admin-email``` flag. This email will be impersonated by this client to make calls to the Admin SDK. See the note on the link from step 5 for the reason why. -8. Create or choose an existing email group and set that email to the ```permit-groups``` flag. You can pass multiple instances of this flag with different groups and the user will be checked against all the provided groups. +8. Create or choose an existing email group and set that email to the ```google-group``` flag. You can pass multiple instances of this flag with different groups +and the user will be checked against all the provided groups. 9. Lock down the permissions on the json file downloaded from step 1 so only oauth2_proxy is able to read the file and set the path to the file in the ```google-service-account-json``` flag. 10. Restart oauth2_proxy. Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ). +### Azure Auth Provider + +1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant. +2. On the App properties page provide the correct Sign-On URL ie `https://internal.yourcompany.com/oauth2/callback` +3. If applicable take note of your `TenantID` and provide it via the `--azure-tenant=` commandline option. Default the `common` tenant is used. + +The Azure AD auth provider uses `openid` as it default scope. It uses `https://graph.windows.net` as a default protected resource. It call to `https://graph.windows.net/me` to get the email address of the user that logs in. + + ### Facebook Auth Provider 1. Create a new FB App from @@ -125,7 +135,7 @@ For LinkedIn, the registration steps are: The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa)) -### Microsoft Azure Active Directory Provider +### Microsoft Azure AD Provider 1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant. 2. On the App properties page provide the correct Sign-On URL ie `https://internal.yourcompany.com/oauth2/callback` @@ -163,55 +173,59 @@ An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is i ``` Usage of oauth2_proxy: - -approval-prompt="force": Oauth approval_prompt - -authenticated-emails-file="": authenticate against emails via file (one per line) - -azure-tenant="common": go to a tenant-specific or common (tenant-independent) endpoint. - -basic-auth-password="": the password to set when passing the HTTP Basic Auth header - -client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com" - -client-secret="": the OAuth Client Secret - -config="": path to config file - -cookie-domain="": an optional cookie domain to force cookies to (ie: .yourcompany.com)* - -cookie-expire=168h0m0s: expire timeframe for cookie - -cookie-httponly=true: set HttpOnly cookie flag - -cookie-name="_oauth2_proxy": the name of the cookie that the oauth_proxy creates - -cookie-refresh=0: refresh the cookie after this duration; 0 to disable - -cookie-secret="": the seed string for secure cookies - -cookie-secure=true: set secure (HTTPS) cookie flag - -custom-templates-dir="": path to custom html templates - -display-htpasswd-form=true: display username / password login form if an htpasswd file is provided - -email-domain=: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email - -filter-groups="": only pass groups in the X-Forwarded-Groups header that contain this string - -github-org="": restrict logins to members of this organisation - -github-team="": restrict logins to members of this team - -google-admin-email="": the google admin to impersonate for api calls - -google-service-account-json="": the path to the service account json credentials - -htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption - -http-address="127.0.0.1:4180": [http://]: or unix:// to listen on for HTTP clients - -https-address=":443": : to listen on for HTTPS clients - -login-url="": Authentication endpoint - -pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header - -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream - -pass-user-headers=true: pass X-Forwarded-User and X-Forwarded-Email information to upstream - -pass-host-header=true: pass the request Host Header to upstream - -pass-groups=false: pass a pipe-separated list of groups to which the user belongs in the X-Forwarded-Groups header - -permit-groups="": The user groups to which a user must belong in order to use the proxy (see also filter-groups) - -profile-url="": Profile access endpoint - -provider="google": OAuth provider - -proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. //sign_in) - -redeem-url="": Token redemption endpoint - -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" - -resource="": the resource that is being protected. ie: "https://graph.microsoft.com". Currently only used in the Azure provider. - -request-logging=true: Log requests to stdout - -scope="": Oauth scope specification - -signature-key="": GAP-Signature request signature key (algorithm:secretkey) - -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times) - -skip-provider-button=false: will skip sign-in-page to directly reach the next step: oauth/start + -approval-prompt string: OAuth approval_prompt (default "force") + -authenticated-emails-file string: authenticate against emails via file (one per line) + -azure-tenant string: go to a tenant-specific or common (tenant-independent) endpoint. (default "common") + -basic-auth-password string: the password to set when passing the HTTP Basic Auth header + -client-id string: the OAuth Client ID: ie: "123456.apps.googleusercontent.com" + -client-secret string: the OAuth Client Secret + -config string: path to config file + -cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com)* + -cookie-expire duration: expire timeframe for cookie (default 168h0m0s) + -cookie-httponly: set HttpOnly cookie flag (default true) + -cookie-name string: the name of the cookie that the oauth_proxy creates (default "_oauth2_proxy") + -cookie-refresh duration: refresh the cookie after this duration; 0 to disable + -cookie-secret string: the seed string for secure cookies (optionally base64 encoded) + -cookie-secure: set secure (HTTPS) cookie flag (default true) + -custom-templates-dir string: path to custom html templates + -display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true) + -email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email + -filter-groups string: only pass groups in the X-Forwarded-Groups header that contain this string + -footer string: custom footer string. Use "-" to disable default footer. + -github-org string: restrict logins to members of this organisation + -github-team string: restrict logins to members of this team + -google-admin-email string: the google admin to impersonate for api calls + -google-group value: restrict logins to members of this google group (may be given multiple times). + -google-service-account-json string: the path to the service account json credentials + -htpasswd-file string: additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption + -http-address string: [http://]: or unix:// to listen on for HTTP clients (default "127.0.0.1:4180") + -https-address string: : to listen on for HTTPS clients (default ":443") + -login-url string: Authentication endpoint + -pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header + -pass-basic-auth: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream (default true) + -pass-host-header: pass the request Host Header to upstream (default true) + -pass-user-headers: pass X-Forwarded-User and X-Forwarded-Email information to upstream (default true) + -pass-groups boolean: pass a pipe-separated list of groups to which the user belongs in the X-Forwarded-Groups header + -permit-groups string: The user groups to which a user must belong in order to use the proxy (see also filter-groups) + -profile-url string: Profile access endpoint + -provider string: OAuth provider (default "google") + -proxy-prefix string: the url root path that this proxy should be nested under (e.g. //sign_in) (default "/oauth2") + -redeem-url string: Token redemption endpoint + -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" + -request-logging: Log requests to stdout (default true) + -resource string: The resource that is protected (Azure AD only) + -scope string: OAuth scope specification + -set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) + -signature-key string: GAP-Signature request signature key (algorithm:secretkey) + -skip-auth-preflight: will skip authentication for OPTIONS requests + -skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times) + -skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start -ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS - -tls-cert="": path to certificate file - -tls-key="": path to private key file - -upstream=: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path - -validate-url="": Access token validation endpoint - -version=false: print version string + -tls-cert string: path to certificate file + -tls-key string: path to private key file + -upstream value: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path + -validate-url string: Access token validation endpoint + -version: print version string ``` See below for provider specific options @@ -355,15 +369,10 @@ The [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth ```nginx server { - listen 443 ssl spdy; + listen 443 ssl; server_name ...; include ssl/ssl.conf; - location = /oauth2/auth { - internal; - proxy_pass http://127.0.0.1:4180; - } - location /oauth2/ { proxy_pass http://127.0.0.1:4180; proxy_set_header Host $host; @@ -372,7 +381,7 @@ server { proxy_set_header X-Auth-Request-Redirect $request_uri; } - location /upstream/ { + location / { auth_request /oauth2/auth; error_page 401 = /oauth2/sign_in; @@ -383,14 +392,12 @@ server { proxy_set_header X-User $user; proxy_set_header X-Email $email; + # if you enabled --cookie-refresh, this is needed for it to work with auth_request + auth_request_set $auth_cookie $upstream_http_set_cookie; + add_header Set-Cookie $auth_cookie; + proxy_pass http://backend/; - } - - location / { - auth_request /oauth2/auth; - error_page 401 = https://example.com/oauth2/sign_in; - - root /path/to/the/site; + # or "root /path/to/site;" or "fastcgi_pass ..." etc } } ``` diff --git a/dist.sh b/dist.sh index 99b13c63..18c5d02e 100755 --- a/dist.sh +++ b/dist.sh @@ -25,7 +25,8 @@ for os in windows linux darwin; do fi BUILD=$(mktemp -d ${TMPDIR:-/tmp}/oauth2_proxy.XXXXXX) TARGET="oauth2_proxy-$version.$os-$arch.$goversion" - GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build -o $BUILD/$TARGET/oauth2_proxy$EXT || exit 1 + GOOS=$os GOARCH=$arch CGO_ENABLED=0 \ + go build -ldflags="-s -w" -o $BUILD/$TARGET/oauth2_proxy$EXT || exit 1 pushd $BUILD tar czvf $TARGET.tar.gz $TARGET mv $TARGET.tar.gz $DIR/dist diff --git a/http.go b/http.go index 3b3d1386..aa764c84 100644 --- a/http.go +++ b/http.go @@ -5,7 +5,6 @@ import ( "log" "net" "net/http" - "net/url" "strings" "time" ) @@ -24,19 +23,24 @@ func (s *Server) ListenAndServe() { } func (s *Server) ServeHTTP() { - u, err := url.Parse(s.Opts.HttpAddress) - if err != nil { - log.Fatalf("FATAL: could not parse %#v: %v", s.Opts.HttpAddress, err) + httpAddress := s.Opts.HttpAddress + scheme := "" + + i := strings.Index(httpAddress, "://") + if i > -1 { + scheme = httpAddress[0:i] } var networkType string - switch u.Scheme { + switch scheme { case "", "http": networkType = "tcp" default: - networkType = u.Scheme + networkType = scheme } - listenAddr := strings.TrimPrefix(u.String(), u.Scheme+"://") + + slice := strings.SplitN(httpAddress, "//", 2) + listenAddr := slice[len(slice)-1] listener, err := net.Listen(networkType, listenAddr) if err != nil { diff --git a/main.go b/main.go index 4217e050..8554e7f4 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ func main() { flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream") flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)") flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start") + flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests") flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS") flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") diff --git a/oauthproxy.go b/oauthproxy.go index b58edcf8..61a78742 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -71,6 +71,7 @@ type OAuthProxy struct { PassAccessToken bool CookieCipher *cookie.Cipher skipAuthRegex []string + skipAuthPreflight bool compiledRegex []*regexp.Regexp templates *template.Template Footer string @@ -204,6 +205,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { serveMux: serveMux, redirectURL: redirectURL, skipAuthRegex: opts.SkipAuthRegex, + skipAuthPreflight: opts.SkipAuthPreflight, compiledRegex: opts.CompiledRegex, SetXAuthRequest: opts.SetXAuthRequest, PassBasicAuth: opts.PassBasicAuth, @@ -430,6 +432,11 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) return } +func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) { + isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" + return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) +} + func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) { for _, u := range p.compiledRegex { ok = u.MatchString(path) @@ -454,7 +461,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { p.RobotsTxt(rw) case path == p.PingPath: p.PingPage(rw) - case p.IsWhitelistedPath(path): + case p.IsWhitelistedRequest(req): p.serveMux.ServeHTTP(rw, req) case path == p.SignInPath: p.SignIn(rw, req) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 027ccbe5..ccb820f2 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -646,6 +646,33 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { assert.Equal(t, "oauth_user@example.com", pc_test.rw.HeaderMap["X-Auth-Request-Email"][0]) } +func TestAuthSkippedForPreflightRequests(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("response")) + })) + defer upstream.Close() + + opts := NewOptions() + opts.Upstreams = append(opts.Upstreams, upstream.URL) + opts.ClientID = "bazquux" + opts.ClientSecret = "foobar" + opts.CookieSecret = "xyzzyplugh" + opts.SkipAuthPreflight = true + opts.Validate() + + upstream_url, _ := url.Parse(upstream.URL) + opts.provider = NewTestProvider(upstream_url, "") + + proxy := NewOAuthProxy(opts, func(string) bool { return false }) + rw := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil) + proxy.ServeHTTP(rw, req) + + assert.Equal(t, 200, rw.Code) + assert.Equal(t, "response", rw.Body.String()) +} + type SignatureAuthenticator struct { auth hmacauth.HmacAuth } diff --git a/options.go b/options.go index 40dadf27..1951af8b 100644 --- a/options.go +++ b/options.go @@ -60,6 +60,7 @@ type Options struct { PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"` SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"` SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"` + SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"` // These options allow for other providers besides Google, with // potential overrides. @@ -101,6 +102,7 @@ func NewOptions() *Options { CookieExpire: time.Duration(168) * time.Hour, CookieRefresh: time.Duration(0), SetXAuthRequest: false, + SkipAuthPreflight: false, PassBasicAuth: true, PassUserHeaders: true, PassGroups: false, diff --git a/providers/github.go b/providers/github.go index f6d78b2a..512eed86 100644 --- a/providers/github.go +++ b/providers/github.go @@ -62,8 +62,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { } params := url.Values{ - "access_token": {accessToken}, - "limit": {"100"}, + "limit": {"100"}, } endpoint := &url.URL{ @@ -74,6 +73,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { } req, _ := http.NewRequest("GET", endpoint.String(), nil) req.Header.Set("Accept", "application/vnd.github.v3+json") + req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken)) resp, err := http.DefaultClient.Do(req) if err != nil { return false, err @@ -86,7 +86,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) { } if resp.StatusCode != 200 { return false, fmt.Errorf( - "got %d from %q %s", resp.StatusCode, stripToken(endpoint.String()), body) + "got %d from %q %s", resp.StatusCode, endpoint.String(), body) } if err := json.Unmarshal(body, &orgs); err != nil { @@ -118,8 +118,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) { } params := url.Values{ - "access_token": {accessToken}, - "limit": {"100"}, + "limit": {"100"}, } endpoint := &url.URL{ @@ -130,6 +129,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) { } req, _ := http.NewRequest("GET", endpoint.String(), nil) req.Header.Set("Accept", "application/vnd.github.v3+json") + req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken)) resp, err := http.DefaultClient.Do(req) if err != nil { return false, err @@ -142,7 +142,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) { } if resp.StatusCode != 200 { return false, fmt.Errorf( - "got %d from %q %s", resp.StatusCode, stripToken(endpoint.String()), body) + "got %d from %q %s", resp.StatusCode, endpoint.String(), body) } if err := json.Unmarshal(body, &teams); err != nil { @@ -198,17 +198,14 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) { } } - params := url.Values{ - "access_token": {s.AccessToken}, - } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/user/emails"), - RawQuery: params.Encode(), + Scheme: p.ValidateURL.Scheme, + Host: p.ValidateURL.Host, + Path: path.Join(p.ValidateURL.Path, "/user/emails"), } - resp, err := http.DefaultClient.Get(endpoint.String()) + req, _ := http.NewRequest("GET", endpoint.String(), nil) + req.Header.Set("Authorization", fmt.Sprintf("token %s", s.AccessToken)) + resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } @@ -220,9 +217,9 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) { if resp.StatusCode != 200 { return "", fmt.Errorf("got %d from %q %s", - resp.StatusCode, stripToken(endpoint.String()), body) + resp.StatusCode, endpoint.String(), body) } else { - log.Printf("got %d from %q %s", resp.StatusCode, stripToken(endpoint.String()), body) + log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body) } if err := json.Unmarshal(body, &emails); err != nil { diff --git a/version.go b/version.go index 954119fd..e5b063a1 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -const VERSION = "2.2.0-alpha" +const VERSION = "2.2.1-alpha"