authenticate: make callback path configurable (#493)

Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
Bobby DeSimone 2020-02-08 09:06:23 -08:00 committed by GitHub
parent 1901cb5ca0
commit 5716113c2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 26 deletions

View file

@ -27,8 +27,6 @@ import (
"github.com/pomerium/pomerium/internal/urlutil" "github.com/pomerium/pomerium/internal/urlutil"
) )
const callbackPath = "/oauth2/callback"
// ValidateOptions checks that configuration are complete and valid. // ValidateOptions checks that configuration are complete and valid.
// Returns on first error found. // Returns on first error found.
func ValidateOptions(o config.Options) error { func ValidateOptions(o config.Options) error {
@ -47,6 +45,9 @@ func ValidateOptions(o config.Options) error {
if o.ClientSecret == "" { if o.ClientSecret == "" {
return errors.New("authenticate: 'IDP_CLIENT_SECRET' is required") return errors.New("authenticate: 'IDP_CLIENT_SECRET' is required")
} }
if o.AuthenticateCallbackPath == "" {
return errors.New("authenticate: 'AUTHENTICATE_CALLBACK_PATH' is required")
}
return nil return nil
} }
@ -149,7 +150,7 @@ func New(opts config.Options) (*Authenticate, error) {
headerStore := header.NewStore(encryptedEncoder, "Pomerium") headerStore := header.NewStore(encryptedEncoder, "Pomerium")
redirectURL, _ := urlutil.DeepCopy(opts.AuthenticateURL) redirectURL, _ := urlutil.DeepCopy(opts.AuthenticateURL)
redirectURL.Path = callbackPath redirectURL.Path = opts.AuthenticateCallbackPath
// configure our identity provider // configure our identity provider
provider, err := identity.New( provider, err := identity.New(
opts.Provider, opts.Provider,

View file

@ -43,6 +43,8 @@ func TestOptions_Validate(t *testing.T) {
badSharedKey.SharedKey = "" badSharedKey.SharedKey = ""
badAuthenticateURL := newTestOptions(t) badAuthenticateURL := newTestOptions(t)
badAuthenticateURL.AuthenticateURL = nil badAuthenticateURL.AuthenticateURL = nil
badCallbackPath := newTestOptions(t)
badCallbackPath.AuthenticateCallbackPath = ""
tests := []struct { tests := []struct {
name string name string
@ -60,6 +62,7 @@ func TestOptions_Validate(t *testing.T) {
{"no client id", emptyClientID, true}, {"no client id", emptyClientID, true},
{"no client secret", emptyClientSecret, true}, {"no client secret", emptyClientSecret, true},
{"empty authenticate url", badAuthenticateURL, true}, {"empty authenticate url", badAuthenticateURL, true},
{"empty callback path", badCallbackPath, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -31,8 +31,8 @@ func (a *Authenticate) Handler() http.Handler {
a.cookieSecret, a.cookieSecret,
csrf.Secure(a.cookieOptions.Secure), csrf.Secure(a.cookieOptions.Secure),
csrf.Path("/"), csrf.Path("/"),
csrf.UnsafePaths([]string{callbackPath}), // enforce CSRF on "safe" handler csrf.UnsafePaths([]string{a.RedirectURL.Path}), // enforce CSRF on "safe" handler
csrf.FormValueName("state"), // rfc6749 section-10.12 csrf.FormValueName("state"), // rfc6749 section-10.12
csrf.CookieName(fmt.Sprintf("%s_csrf", a.cookieOptions.Name)), csrf.CookieName(fmt.Sprintf("%s_csrf", a.cookieOptions.Name)),
csrf.ErrorHandler(httputil.HandlerFunc(httputil.CSRFFailureHandler)), csrf.ErrorHandler(httputil.HandlerFunc(httputil.CSRFFailureHandler)),
)) ))

View file

@ -89,6 +89,12 @@ type Options struct {
AuthenticateURLString string `mapstructure:"authenticate_service_url" yaml:"authenticate_service_url,omitempty"` AuthenticateURLString string `mapstructure:"authenticate_service_url" yaml:"authenticate_service_url,omitempty"`
AuthenticateURL *url.URL `yaml:"-,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 // Session/Cookie management
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
CookieName string `mapstructure:"cookie_name" yaml:"cookie_name,omitempty"` CookieName string `mapstructure:"cookie_name" yaml:"cookie_name,omitempty"`
@ -211,16 +217,17 @@ var defaultOptions = Options{
"X-XSS-Protection": "1; mode=block", "X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
}, },
Addr: ":443", Addr: ":443",
ReadHeaderTimeout: 10 * time.Second, ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second, ReadTimeout: 30 * time.Second,
WriteTimeout: 0, // support streaming by default WriteTimeout: 0, // support streaming by default
IdleTimeout: 5 * time.Minute, IdleTimeout: 5 * time.Minute,
RefreshCooldown: 5 * time.Minute, RefreshCooldown: 5 * time.Minute,
GRPCAddr: ":443", GRPCAddr: ":443",
GRPCClientTimeout: 10 * time.Second, // Try to withstand transient service failures for a single request GRPCClientTimeout: 10 * time.Second, // Try to withstand transient service failures for a single request
GRPCClientDNSRoundRobin: true, GRPCClientDNSRoundRobin: true,
CacheStore: "autocache", CacheStore: "autocache",
AuthenticateCallbackPath: "/oauth2/callback",
} }
// NewDefaultOptions returns a copy the default options. It's the caller's // NewDefaultOptions returns a copy the default options. It's the caller's

View file

@ -219,11 +219,12 @@ func TestOptionsFromViper(t *testing.T) {
{"good", {"good",
[]byte(`{"insecure_server":true,"policy":[{"from": "https://from.example","to":"https://to.example"}]}`), []byte(`{"insecure_server":true,"policy":[{"from": "https://from.example","to":"https://to.example"}]}`),
&Options{ &Options{
Policies: []Policy{{From: "https://from.example", To: "https://to.example"}}, Policies: []Policy{{From: "https://from.example", To: "https://to.example"}},
CookieName: "_pomerium", CookieName: "_pomerium",
CookieSecure: true, CookieSecure: true,
InsecureServer: true, InsecureServer: true,
CookieHTTPOnly: true, CookieHTTPOnly: true,
AuthenticateCallbackPath: "/oauth2/callback",
Headers: map[string]string{ Headers: map[string]string{
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"X-Frame-Options": "SAMEORIGIN", "X-Frame-Options": "SAMEORIGIN",
@ -233,12 +234,13 @@ func TestOptionsFromViper(t *testing.T) {
{"good disable header", {"good disable header",
[]byte(`{"insecure_server":true,"headers": {"disable":"true"},"policy":[{"from": "https://from.example","to":"https://to.example"}]}`), []byte(`{"insecure_server":true,"headers": {"disable":"true"},"policy":[{"from": "https://from.example","to":"https://to.example"}]}`),
&Options{ &Options{
Policies: []Policy{{From: "https://from.example", To: "https://to.example"}}, Policies: []Policy{{From: "https://from.example", To: "https://to.example"}},
CookieName: "_pomerium", CookieName: "_pomerium",
CookieSecure: true, AuthenticateCallbackPath: "/oauth2/callback",
CookieHTTPOnly: true, CookieSecure: true,
InsecureServer: true, CookieHTTPOnly: true,
Headers: map[string]string{}}, InsecureServer: true,
Headers: map[string]string{}},
false}, false},
{"bad url", []byte(`{"policy":[{"from": "https://","to":"https://to.example"}]}`), nil, true}, {"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}, {"bad policy", []byte(`{"policy":[{"allow_public_unauthenticated_access": "dog","to":"https://to.example"}]}`), nil, true},

View file

@ -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. 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 ## Proxy Service
### Signing Key ### Signing Key