diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index a6e63a1b9..45b107d42 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -27,8 +27,6 @@ import ( "github.com/pomerium/pomerium/internal/urlutil" ) -const callbackPath = "/oauth2/callback" - // ValidateOptions checks that configuration are complete and valid. // Returns on first error found. func ValidateOptions(o config.Options) error { @@ -47,6 +45,9 @@ func ValidateOptions(o config.Options) error { if o.ClientSecret == "" { return errors.New("authenticate: 'IDP_CLIENT_SECRET' is required") } + if o.AuthenticateCallbackPath == "" { + return errors.New("authenticate: 'AUTHENTICATE_CALLBACK_PATH' is required") + } return nil } @@ -149,7 +150,7 @@ func New(opts config.Options) (*Authenticate, error) { headerStore := header.NewStore(encryptedEncoder, "Pomerium") redirectURL, _ := urlutil.DeepCopy(opts.AuthenticateURL) - redirectURL.Path = callbackPath + redirectURL.Path = opts.AuthenticateCallbackPath // configure our identity provider provider, err := identity.New( opts.Provider, diff --git a/authenticate/authenticate_test.go b/authenticate/authenticate_test.go index a28387cda..6083dab92 100644 --- a/authenticate/authenticate_test.go +++ b/authenticate/authenticate_test.go @@ -43,6 +43,8 @@ func TestOptions_Validate(t *testing.T) { badSharedKey.SharedKey = "" badAuthenticateURL := newTestOptions(t) badAuthenticateURL.AuthenticateURL = nil + badCallbackPath := newTestOptions(t) + badCallbackPath.AuthenticateCallbackPath = "" tests := []struct { name string @@ -60,6 +62,7 @@ func TestOptions_Validate(t *testing.T) { {"no client id", emptyClientID, true}, {"no client secret", emptyClientSecret, true}, {"empty authenticate url", badAuthenticateURL, true}, + {"empty callback path", badCallbackPath, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 75fc940ee..143a8e7b6 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -31,8 +31,8 @@ func (a *Authenticate) Handler() http.Handler { a.cookieSecret, csrf.Secure(a.cookieOptions.Secure), csrf.Path("/"), - csrf.UnsafePaths([]string{callbackPath}), // enforce CSRF on "safe" handler - csrf.FormValueName("state"), // rfc6749 section-10.12 + csrf.UnsafePaths([]string{a.RedirectURL.Path}), // enforce CSRF on "safe" handler + csrf.FormValueName("state"), // rfc6749 section-10.12 csrf.CookieName(fmt.Sprintf("%s_csrf", a.cookieOptions.Name)), csrf.ErrorHandler(httputil.HandlerFunc(httputil.CSRFFailureHandler)), )) diff --git a/config/options.go b/config/options.go index abbdaa697..d5a35ffc9 100644 --- a/config/options.go +++ b/config/options.go @@ -89,6 +89,12 @@ type Options struct { AuthenticateURLString string `mapstructure:"authenticate_service_url" yaml:"authenticate_service_url,omitempty"` AuthenticateURL *url.URL `yaml:"-,omitempty"` + // AuthenticateCallbackPath is the path to the HTTP endpoint that will + // receive the response from your identity provider. The value must exactly + // match one of the authorized redirect URIs for the OAuth 2.0 client. + // Defaults to: `/oauth2/callback` + AuthenticateCallbackPath string `mapstructure:"authenticate_callback_path" yaml:"authenticate_callback_path,omitempty"` + // Session/Cookie management // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie CookieName string `mapstructure:"cookie_name" yaml:"cookie_name,omitempty"` @@ -211,16 +217,17 @@ var defaultOptions = Options{ "X-XSS-Protection": "1; mode=block", "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", }, - Addr: ":443", - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - WriteTimeout: 0, // support streaming by default - IdleTimeout: 5 * time.Minute, - RefreshCooldown: 5 * time.Minute, - GRPCAddr: ":443", - GRPCClientTimeout: 10 * time.Second, // Try to withstand transient service failures for a single request - GRPCClientDNSRoundRobin: true, - CacheStore: "autocache", + Addr: ":443", + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 0, // support streaming by default + IdleTimeout: 5 * time.Minute, + RefreshCooldown: 5 * time.Minute, + GRPCAddr: ":443", + GRPCClientTimeout: 10 * time.Second, // Try to withstand transient service failures for a single request + GRPCClientDNSRoundRobin: true, + CacheStore: "autocache", + AuthenticateCallbackPath: "/oauth2/callback", } // NewDefaultOptions returns a copy the default options. It's the caller's diff --git a/config/options_test.go b/config/options_test.go index e104defcb..d842a88fa 100644 --- a/config/options_test.go +++ b/config/options_test.go @@ -219,11 +219,12 @@ func TestOptionsFromViper(t *testing.T) { {"good", []byte(`{"insecure_server":true,"policy":[{"from": "https://from.example","to":"https://to.example"}]}`), &Options{ - Policies: []Policy{{From: "https://from.example", To: "https://to.example"}}, - CookieName: "_pomerium", - CookieSecure: true, - InsecureServer: true, - CookieHTTPOnly: true, + Policies: []Policy{{From: "https://from.example", To: "https://to.example"}}, + CookieName: "_pomerium", + CookieSecure: true, + InsecureServer: true, + CookieHTTPOnly: true, + AuthenticateCallbackPath: "/oauth2/callback", Headers: map[string]string{ "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", "X-Frame-Options": "SAMEORIGIN", @@ -233,12 +234,13 @@ func TestOptionsFromViper(t *testing.T) { {"good disable header", []byte(`{"insecure_server":true,"headers": {"disable":"true"},"policy":[{"from": "https://from.example","to":"https://to.example"}]}`), &Options{ - Policies: []Policy{{From: "https://from.example", To: "https://to.example"}}, - CookieName: "_pomerium", - CookieSecure: true, - CookieHTTPOnly: true, - InsecureServer: true, - Headers: map[string]string{}}, + Policies: []Policy{{From: "https://from.example", To: "https://to.example"}}, + CookieName: "_pomerium", + AuthenticateCallbackPath: "/oauth2/callback", + CookieSecure: true, + CookieHTTPOnly: true, + InsecureServer: true, + Headers: map[string]string{}}, false}, {"bad url", []byte(`{"policy":[{"from": "https://","to":"https://to.example"}]}`), nil, true}, {"bad policy", []byte(`{"policy":[{"allow_public_unauthenticated_access": "dog","to":"https://to.example"}]}`), nil, true}, diff --git a/docs/configuration/readme.md b/docs/configuration/readme.md index b8b17e68e..fc42977a4 100644 --- a/docs/configuration/readme.md +++ b/docs/configuration/readme.md @@ -503,6 +503,24 @@ Identity provider scopes correspond to access privilege scopes as defined in Sec Identity Provider Service Account is field used to configure any additional user account or access-token that may be required for querying additional user information during authentication. For a concrete example, Google an additional service account and to make a follow-up request to query a user's group membership. For more information, refer to the [identity provider] docs to see if your provider requires this setting. +### Authenticate Callback Path + +- Environmental Variable: `AUTHENTICATE_CALLBACK_PATH` +- Config File Key: `authenticate_callback_path` +- Type: `string` +- Default: `/oauth2/callback` +- Optional + +The authenticate callback path is the path/url from the authenticate service that will receive the response from your identity provider. The value must exactly match one of the authorized redirect URIs for the OAuth 2.0 client. + +This value is referred to as the `redirect_url` in the [OpenIDConnect](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) and OAuth2 specs. + +See also: + +- [OAuth2 RFC 6749](https://tools.ietf.org/html/rfc6749#section-3.1.2) +- [OIDC Spec](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) +- [Google - Setting Redirect URI](https://developers.google.com/identity/protocols/OpenIDConnect#setredirecturi) + ## Proxy Service ### Signing Key