mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-01 11:26:29 +02:00
authenticate: POC for supporting alternative OIDC redirect URLs
This commit is contained in:
parent
ace5bbb89a
commit
43dfdc0700
13 changed files with 203 additions and 50 deletions
|
@ -108,7 +108,7 @@ func (a *Authenticate) updateProvider(cfg *config.Config) error {
|
||||||
// configure our identity provider
|
// configure our identity provider
|
||||||
provider, err := identity.NewAuthenticator(
|
provider, err := identity.NewAuthenticator(
|
||||||
oauth.Options{
|
oauth.Options{
|
||||||
RedirectURL: redirectURL,
|
RedirectURL: redirectURL.String(),
|
||||||
ProviderName: cfg.Options.Provider,
|
ProviderName: cfg.Options.Provider,
|
||||||
ProviderURL: cfg.Options.ProviderURL,
|
ProviderURL: cfg.Options.ProviderURL,
|
||||||
ClientID: cfg.Options.ClientID,
|
ClientID: cfg.Options.ClientID,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/identity"
|
"github.com/pomerium/pomerium/internal/identity"
|
||||||
"github.com/pomerium/pomerium/internal/identity/manager"
|
"github.com/pomerium/pomerium/internal/identity/manager"
|
||||||
|
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||||
"github.com/pomerium/pomerium/internal/identity/oidc"
|
"github.com/pomerium/pomerium/internal/identity/oidc"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
"github.com/pomerium/pomerium/internal/middleware"
|
"github.com/pomerium/pomerium/internal/middleware"
|
||||||
|
@ -56,7 +57,6 @@ func (a *Authenticate) Mount(r *mux.Router) {
|
||||||
csrf.Path("/"),
|
csrf.Path("/"),
|
||||||
csrf.UnsafePaths(
|
csrf.UnsafePaths(
|
||||||
[]string{
|
[]string{
|
||||||
"/oauth2/callback", // rfc6749#section-10.12 accepts GET
|
|
||||||
"/.pomerium/sign_out", // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
"/.pomerium/sign_out", // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
||||||
}),
|
}),
|
||||||
csrf.FormValueName("state"), // rfc6749#section-10.12
|
csrf.FormValueName("state"), // rfc6749#section-10.12
|
||||||
|
@ -306,13 +306,16 @@ func (a *Authenticate) reauthenticateOrFail(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
state.sessionStore.ClearSession(w, r)
|
state.sessionStore.ClearSession(w, r)
|
||||||
redirectURL := state.redirectURL.ResolveReference(r.URL)
|
redirectURL := state.redirectURL.ResolveReference(r.URL)
|
||||||
nonce := csrf.Token(r)
|
|
||||||
now := time.Now().Unix()
|
if rawOAuthRedirectURI := r.FormValue(urlutil.QueryOAuthRedirectURI); rawOAuthRedirectURI != "" {
|
||||||
b := []byte(fmt.Sprintf("%s|%d|", nonce, now))
|
redirectURL, err = urlutil.ParseAndValidateURL(rawOAuthRedirectURI)
|
||||||
enc := cryptutil.Encrypt(state.cookieCipher, []byte(redirectURL.String()), b)
|
if err != nil {
|
||||||
b = append(b, enc...)
|
return httputil.NewError(http.StatusInternalServerError, err)
|
||||||
encodedState := base64.URLEncoding.EncodeToString(b)
|
}
|
||||||
signinURL, err := a.provider.Load().GetSignInURL(encodedState)
|
}
|
||||||
|
|
||||||
|
oauthState := oauth.NewState(redirectURL.String()).Encode(state.cookieCipher)
|
||||||
|
signinURL, err := a.provider.Load().GetSignInURL(oauthState, redirectURL.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httputil.NewError(http.StatusInternalServerError,
|
return httputil.NewError(http.StatusInternalServerError,
|
||||||
fmt.Errorf("failed to get sign in url: %w", err))
|
fmt.Errorf("failed to get sign in url: %w", err))
|
||||||
|
@ -370,34 +373,12 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
|
||||||
return nil, fmt.Errorf("error redeeming authenticate code: %w", err)
|
return nil, fmt.Errorf("error redeeming authenticate code: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// state includes a csrf nonce (validated by middleware) and redirect uri
|
oauthState, err := oauth.DecodeState(state.cookieCipher, r.FormValue("state"))
|
||||||
bytes, err := base64.URLEncoding.DecodeString(r.FormValue("state"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, httputil.NewError(http.StatusBadRequest, fmt.Errorf("bad bytes: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// split state into concat'd components
|
|
||||||
// (nonce|timestamp|redirect_url|encrypted_data(redirect_url)+mac(nonce,ts))
|
|
||||||
statePayload := strings.SplitN(string(bytes), "|", 3)
|
|
||||||
if len(statePayload) != 3 {
|
|
||||||
return nil, httputil.NewError(http.StatusBadRequest, fmt.Errorf("state malformed, size: %d", len(statePayload)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify that the returned timestamp is valid
|
|
||||||
if err := cryptutil.ValidTimestamp(statePayload[1]); err != nil {
|
|
||||||
return nil, httputil.NewError(http.StatusBadRequest, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use our AEAD construct to enforce secrecy and authenticity:
|
|
||||||
// mac: to validate the nonce again, and above timestamp
|
|
||||||
// decrypt: to prevent leaking 'redirect_uri' to IdP or logs
|
|
||||||
b := []byte(fmt.Sprint(statePayload[0], "|", statePayload[1], "|"))
|
|
||||||
redirectString, err := cryptutil.Decrypt(state.cookieCipher, []byte(statePayload[2]), b)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httputil.NewError(http.StatusBadRequest, err)
|
return nil, httputil.NewError(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL, err := urlutil.ParseAndValidateURL(string(redirectString))
|
redirectURL, err := urlutil.ParseAndValidateURL(oauthState.RedirectURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httputil.NewError(http.StatusBadRequest, err)
|
return nil, httputil.NewError(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ func (a *Authorize) handleResultAllowed(
|
||||||
func (a *Authorize) handleResultDenied(
|
func (a *Authorize) handleResultDenied(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
|
req *evaluator.Request,
|
||||||
result *evaluator.Result,
|
result *evaluator.Result,
|
||||||
isForwardAuthVerify bool,
|
isForwardAuthVerify bool,
|
||||||
reasons criteria.Reasons,
|
reasons criteria.Reasons,
|
||||||
|
@ -49,7 +50,7 @@ func (a *Authorize) handleResultDenied(
|
||||||
case reasons.Has(criteria.ReasonUserUnauthenticated):
|
case reasons.Has(criteria.ReasonUserUnauthenticated):
|
||||||
// when the user is unauthenticated it means they haven't
|
// when the user is unauthenticated it means they haven't
|
||||||
// logged in yet, so redirect to authenticate
|
// logged in yet, so redirect to authenticate
|
||||||
return a.requireLoginResponse(ctx, in, isForwardAuthVerify)
|
return a.requireLoginResponse(ctx, in, req, isForwardAuthVerify)
|
||||||
case reasons.Has(criteria.ReasonDeviceUnauthenticated):
|
case reasons.Has(criteria.ReasonDeviceUnauthenticated):
|
||||||
// when the user's device is unauthenticated it means they haven't
|
// when the user's device is unauthenticated it means they haven't
|
||||||
// registered a webauthn device yet, so redirect to the webauthn flow
|
// registered a webauthn device yet, so redirect to the webauthn flow
|
||||||
|
@ -141,19 +142,20 @@ func (a *Authorize) deniedResponse(
|
||||||
func (a *Authorize) requireLoginResponse(
|
func (a *Authorize) requireLoginResponse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
|
req *evaluator.Request,
|
||||||
isForwardAuthVerify bool,
|
isForwardAuthVerify bool,
|
||||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
opts := a.currentOptions.Load()
|
opts := a.currentOptions.Load()
|
||||||
state := a.state.Load()
|
state := a.state.Load()
|
||||||
authenticateURL, err := opts.GetAuthenticateURL()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !a.shouldRedirect(in) || isForwardAuthVerify {
|
if !a.shouldRedirect(in) || isForwardAuthVerify {
|
||||||
return a.deniedResponse(ctx, in, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil)
|
return a.deniedResponse(ctx, in, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticateURL, err := opts.GetAuthenticateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
signinURL := authenticateURL.ResolveReference(&url.URL{
|
signinURL := authenticateURL.ResolveReference(&url.URL{
|
||||||
Path: "/.pomerium/sign_in",
|
Path: "/.pomerium/sign_in",
|
||||||
})
|
})
|
||||||
|
@ -164,6 +166,17 @@ func (a *Authorize) requireLoginResponse(
|
||||||
checkRequestURL.Scheme = "https"
|
checkRequestURL.Scheme = "https"
|
||||||
|
|
||||||
q.Set(urlutil.QueryRedirectURI, checkRequestURL.String())
|
q.Set(urlutil.QueryRedirectURI, checkRequestURL.String())
|
||||||
|
|
||||||
|
// If an OAuthRedirectURL is explicitly set, pass that on the query string to
|
||||||
|
// override the default authenticate redirect url.
|
||||||
|
if req.Policy != nil && req.Policy.OAuthRedirectURL != "" {
|
||||||
|
u, err := urlutil.ParseAndValidateURL(req.Policy.OAuthRedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
q.Set(urlutil.QueryOAuthRedirectURI, u.String())
|
||||||
|
}
|
||||||
|
|
||||||
signinURL.RawQuery = q.Encode()
|
signinURL.RawQuery = q.Encode()
|
||||||
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe
|
||||||
|
|
||||||
// if there's a deny, the result is denied using the deny reasons.
|
// if there's a deny, the result is denied using the deny reasons.
|
||||||
if res.Deny.Value {
|
if res.Deny.Value {
|
||||||
return a.handleResultDenied(ctx, in, res, isForwardAuthVerify, res.Deny.Reasons)
|
return a.handleResultDenied(ctx, in, req, res, isForwardAuthVerify, res.Deny.Reasons)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there's an allow, the result is allowed.
|
// if there's an allow, the result is allowed.
|
||||||
|
@ -85,7 +85,7 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, the result is denied using the allow reasons.
|
// otherwise, the result is denied using the allow reasons.
|
||||||
return a.handleResultDenied(ctx, in, res, isForwardAuthVerify, res.Allow.Reasons)
|
return a.handleResultDenied(ctx, in, req, res, isForwardAuthVerify, res.Allow.Reasons)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getForwardAuthURL(r *http.Request) *url.URL {
|
func getForwardAuthURL(r *http.Request) *url.URL {
|
||||||
|
|
|
@ -3,8 +3,10 @@ package envoyconfig
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
|
@ -17,6 +19,7 @@ import (
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
|
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -268,6 +271,14 @@ func (b *Builder) buildPolicyRoutes(options *config.Options, domain string) ([]*
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if policy.OAuthRedirectURL != "" {
|
||||||
|
oauthRedirectURLRoute, err := b.buildOAuthRedirectURLRoute(options, policy.OAuthRedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
routes = append(routes, oauthRedirectURLRoute)
|
||||||
|
}
|
||||||
|
|
||||||
match := mkRouteMatch(&policy)
|
match := mkRouteMatch(&policy)
|
||||||
envoyRoute := &envoy_config_route_v3.Route{
|
envoyRoute := &envoy_config_route_v3.Route{
|
||||||
Name: fmt.Sprintf("policy-%d", i),
|
Name: fmt.Sprintf("policy-%d", i),
|
||||||
|
@ -446,6 +457,69 @@ func (b *Builder) buildPolicyRouteRouteAction(options *config.Options, policy *c
|
||||||
return action, nil
|
return action, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Builder) buildOAuthRedirectURLRoute(options *config.Options, rawOAuthRedirectURL string) (*envoy_config_route_v3.Route, error) {
|
||||||
|
oauthRedirectURL, err := urlutil.ParseAndValidateURL(rawOAuthRedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
envoyRedirect, err := b.buildAuthenticateCallbackRouteRedirectAction(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
envoyRoute := &envoy_config_route_v3.Route{
|
||||||
|
Action: &envoy_config_route_v3.Route_Redirect{
|
||||||
|
Redirect: envoyRedirect,
|
||||||
|
},
|
||||||
|
Match: &envoy_config_route_v3.RouteMatch{
|
||||||
|
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
|
||||||
|
Path: oauthRedirectURL.Path,
|
||||||
|
},
|
||||||
|
QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{
|
||||||
|
{Name: "state", QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_StringMatch{
|
||||||
|
StringMatch: &envoy_type_matcher_v3.StringMatcher{
|
||||||
|
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Prefix{
|
||||||
|
Prefix: oauth.StatePrefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TypedPerFilterConfig: map[string]*any.Any{
|
||||||
|
"envoy.filters.http.ext_authz": disableExtAuthz,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return envoyRoute, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) buildAuthenticateCallbackRouteRedirectAction(options *config.Options) (*envoy_config_route_v3.RedirectAction, error) {
|
||||||
|
authenticateURL, err := options.GetAuthenticateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticateURL = authenticateURL.ResolveReference(&url.URL{
|
||||||
|
Path: options.AuthenticateCallbackPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
redirect := &envoy_config_route_v3.RedirectAction{}
|
||||||
|
if host, rawPort, err := net.SplitHostPort(authenticateURL.Host); err == nil {
|
||||||
|
if port, err := strconv.ParseUint(rawPort, 10, 32); err == nil {
|
||||||
|
redirect.HostRedirect = host
|
||||||
|
redirect.PortRedirect = uint32(port)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid port in authenticate URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redirect.PathRewriteSpecifier = &envoy_config_route_v3.RedirectAction_PathRedirect{
|
||||||
|
PathRedirect: authenticateURL.Path,
|
||||||
|
}
|
||||||
|
redirect.ResponseCode = envoy_config_route_v3.RedirectAction_FOUND
|
||||||
|
|
||||||
|
return redirect, nil
|
||||||
|
}
|
||||||
|
|
||||||
func mkEnvoyHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
|
func mkEnvoyHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
|
||||||
return &envoy_config_core_v3.HeaderValueOption{
|
return &envoy_config_core_v3.HeaderValueOption{
|
||||||
Header: &envoy_config_core_v3.HeaderValue{
|
Header: &envoy_config_core_v3.HeaderValue{
|
||||||
|
|
|
@ -899,7 +899,7 @@ func (o *Options) GetOauthOptions() (oauth.Options, error) {
|
||||||
Path: o.AuthenticateCallbackPath,
|
Path: o.AuthenticateCallbackPath,
|
||||||
})
|
})
|
||||||
return oauth.Options{
|
return oauth.Options{
|
||||||
RedirectURL: redirectURL,
|
RedirectURL: redirectURL.String(),
|
||||||
ProviderName: o.Provider,
|
ProviderName: o.Provider,
|
||||||
ProviderURL: o.ProviderURL,
|
ProviderURL: o.ProviderURL,
|
||||||
ClientID: o.ClientID,
|
ClientID: o.ClientID,
|
||||||
|
|
|
@ -162,6 +162,9 @@ type Policy struct {
|
||||||
// SetResponseHeaders sets response headers.
|
// SetResponseHeaders sets response headers.
|
||||||
SetResponseHeaders map[string]string `mapstructure:"set_response_headers" yaml:"set_response_headers,omitempty"`
|
SetResponseHeaders map[string]string `mapstructure:"set_response_headers" yaml:"set_response_headers,omitempty"`
|
||||||
|
|
||||||
|
// OAuthRedirectURL overrides the default authenticate redirect URL for this route.
|
||||||
|
OAuthRedirectURL string `mapstructure:"oauth_redirect_url" yaml:"oauth_redirect_url,omitempty"`
|
||||||
|
|
||||||
Policy *PPLPolicy `mapstructure:"policy" yaml:"policy,omitempty" json:"policy,omitempty"`
|
Policy *PPLPolicy `mapstructure:"policy" yaml:"policy,omitempty" json:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||||
ClientID: o.ClientID,
|
ClientID: o.ClientID,
|
||||||
ClientSecret: o.ClientSecret,
|
ClientSecret: o.ClientSecret,
|
||||||
Scopes: o.Scopes,
|
Scopes: o.Scopes,
|
||||||
RedirectURL: o.RedirectURL.String(),
|
RedirectURL: o.RedirectURL,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: urlutil.Join(o.ProviderURL, authURL),
|
AuthURL: urlutil.Join(o.ProviderURL, authURL),
|
||||||
TokenURL: urlutil.Join(o.ProviderURL, tokenURL),
|
TokenURL: urlutil.Join(o.ProviderURL, tokenURL),
|
||||||
|
@ -241,8 +241,10 @@ func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||||
|
|
||||||
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page
|
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page
|
||||||
// that asks for permissions for the required scopes explicitly.
|
// that asks for permissions for the required scopes explicitly.
|
||||||
func (p *Provider) GetSignInURL(state string) (string, error) {
|
func (p *Provider) GetSignInURL(state, redirectURL string) (string, error) {
|
||||||
return p.Oauth.AuthCodeURL(state, oauth2.AccessTypeOffline), nil
|
oa := *p.Oauth
|
||||||
|
oa.RedirectURL = redirectURL
|
||||||
|
return oa.AuthCodeURL(state, oauth2.AccessTypeOffline), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogOut is not implemented by github.
|
// LogOut is not implemented by github.
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
// authorization with Bearer JWT.
|
// authorization with Bearer JWT.
|
||||||
package oauth
|
package oauth
|
||||||
|
|
||||||
import "net/url"
|
|
||||||
|
|
||||||
// Options contains the fields required for an OAuth 2.0 (inc. OIDC) auth flow.
|
// Options contains the fields required for an OAuth 2.0 (inc. OIDC) auth flow.
|
||||||
//
|
//
|
||||||
// https://tools.ietf.org/html/rfc6749
|
// https://tools.ietf.org/html/rfc6749
|
||||||
|
@ -22,7 +20,7 @@ type Options struct {
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
// RedirectURL is the URL to redirect users going through
|
// RedirectURL is the URL to redirect users going through
|
||||||
// the OAuth flow, after the resource owner's URLs.
|
// the OAuth flow, after the resource owner's URLs.
|
||||||
RedirectURL *url.URL
|
RedirectURL string
|
||||||
// Scope specifies optional requested permissions.
|
// Scope specifies optional requested permissions.
|
||||||
Scopes []string
|
Scopes []string
|
||||||
|
|
||||||
|
|
80
internal/identity/oauth/state.go
Normal file
80
internal/identity/oauth/state.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatePrefix is the prefix used to indicate the state is via pomerium.
|
||||||
|
const StatePrefix = "POMERIUM-"
|
||||||
|
|
||||||
|
// State is the state in the oauth query string.
|
||||||
|
type State struct {
|
||||||
|
Nonce string
|
||||||
|
Timestamp time.Time
|
||||||
|
RedirectURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewState creates a new State.
|
||||||
|
func NewState(redirectURL string) *State {
|
||||||
|
return &State{
|
||||||
|
Nonce: uuid.NewString(),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
RedirectURL: redirectURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeState decodes state from a raw state string.
|
||||||
|
func DecodeState(aead cipher.AEAD, rawState string) (*State, error) {
|
||||||
|
withoutPrefix := strings.TrimPrefix(rawState, StatePrefix)
|
||||||
|
rawStateBytes, err := base64.RawURLEncoding.DecodeString(withoutPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid state encoding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the state into its components
|
||||||
|
state := bytes.SplitN(rawStateBytes, []byte{'|'}, 3)
|
||||||
|
if len(state) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid state format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that the returned timestamp is valid
|
||||||
|
if err := cryptutil.ValidTimestamp(string(state[1])); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid state timestamp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := string(state[0])
|
||||||
|
timestamp, err := strconv.ParseInt(string(state[1]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid state timestamp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ad := []byte(fmt.Sprintf("%s|%d|", nonce, timestamp))
|
||||||
|
decrypted, err := cryptutil.Decrypt(aead, state[2], ad)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid state redirect URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{
|
||||||
|
Nonce: nonce,
|
||||||
|
Timestamp: time.Unix(timestamp, 0),
|
||||||
|
RedirectURL: string(decrypted),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes the state.
|
||||||
|
func (state *State) Encode(aead cipher.AEAD) string {
|
||||||
|
timestamp := state.Timestamp.Unix()
|
||||||
|
ad := []byte(fmt.Sprintf("%s|%d|", state.Nonce, timestamp))
|
||||||
|
encrypted := cryptutil.Encrypt(aead, []byte(state.RedirectURL), ad)
|
||||||
|
return StatePrefix + base64.RawURLEncoding.EncodeToString(append(ad, encrypted...))
|
||||||
|
}
|
|
@ -73,7 +73,7 @@ func New(ctx context.Context, o *oauth.Options, options ...Option) (*Provider, e
|
||||||
ClientSecret: o.ClientSecret,
|
ClientSecret: o.ClientSecret,
|
||||||
Scopes: o.Scopes,
|
Scopes: o.Scopes,
|
||||||
Endpoint: provider.Endpoint(),
|
Endpoint: provider.Endpoint(),
|
||||||
RedirectURL: o.RedirectURL.String(),
|
RedirectURL: o.RedirectURL,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
WithGetProvider(func() (*go_oidc.Provider, error) {
|
WithGetProvider(func() (*go_oidc.Provider, error) {
|
||||||
|
@ -103,11 +103,12 @@ func New(ctx context.Context, o *oauth.Options, options ...Option) (*Provider, e
|
||||||
// always provide a non-empty string and validate that it matches the
|
// always provide a non-empty string and validate that it matches the
|
||||||
// the state query parameter on your redirect callback.
|
// the state query parameter on your redirect callback.
|
||||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||||
func (p *Provider) GetSignInURL(state string) (string, error) {
|
func (p *Provider) GetSignInURL(state, redirectURL string) (string, error) {
|
||||||
oa, err := p.GetOauthConfig()
|
oa, err := p.GetOauthConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
oa.RedirectURL = redirectURL
|
||||||
|
|
||||||
opts := defaultAuthCodeOptions
|
opts := defaultAuthCodeOptions
|
||||||
for k, v := range p.AuthCodeOptions {
|
for k, v := range p.AuthCodeOptions {
|
||||||
|
|
|
@ -28,7 +28,7 @@ type Authenticator interface {
|
||||||
Authenticate(context.Context, string, identity.State) (*oauth2.Token, error)
|
Authenticate(context.Context, string, identity.State) (*oauth2.Token, error)
|
||||||
Refresh(context.Context, *oauth2.Token, identity.State) (*oauth2.Token, error)
|
Refresh(context.Context, *oauth2.Token, identity.State) (*oauth2.Token, error)
|
||||||
Revoke(context.Context, *oauth2.Token) error
|
Revoke(context.Context, *oauth2.Token) error
|
||||||
GetSignInURL(state string) (string, error)
|
GetSignInURL(state, redirectURL string) (string, error)
|
||||||
Name() string
|
Name() string
|
||||||
LogOut() (*url.URL, error)
|
LogOut() (*url.URL, error)
|
||||||
UpdateUserInfo(ctx context.Context, t *oauth2.Token, v interface{}) error
|
UpdateUserInfo(ctx context.Context, t *oauth2.Token, v interface{}) error
|
||||||
|
|
|
@ -10,6 +10,7 @@ const (
|
||||||
QueryEnrollmentToken = "pomerium_enrollment_token" //nolint
|
QueryEnrollmentToken = "pomerium_enrollment_token" //nolint
|
||||||
QueryIsProgrammatic = "pomerium_programmatic"
|
QueryIsProgrammatic = "pomerium_programmatic"
|
||||||
QueryForwardAuth = "pomerium_forward_auth"
|
QueryForwardAuth = "pomerium_forward_auth"
|
||||||
|
QueryOAuthRedirectURI = "pomerium_oauth_redirect_uri"
|
||||||
QueryPomeriumJWT = "pomerium_jwt"
|
QueryPomeriumJWT = "pomerium_jwt"
|
||||||
QuerySession = "pomerium_session"
|
QuerySession = "pomerium_session"
|
||||||
QuerySessionEncrypted = "pomerium_session_encrypted"
|
QuerySessionEncrypted = "pomerium_session_encrypted"
|
||||||
|
|
Loading…
Add table
Reference in a new issue