diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index 7f855b22..b6cb7c5f 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -296,6 +296,7 @@ Provider holds all configuration for a single provider | `loginURL` | _string_ | LoginURL is the authentication endpoint | | `redeemURL` | _string_ | RedeemURL is the token redemption endpoint | | `profileURL` | _string_ | ProfileURL is the profile access endpoint | +| `introspectURL` | _string_ | IntrospectURL is the claims and permissions access Endpoint (OIDC) | | `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD only) | | `validateURL` | _string_ | ValidateURL is the access token validation endpoint | | `scope` | _string_ | Scope is the OAuth scope specification | diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 5519963a..a7d4df3d 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -180,9 +180,10 @@ type LegacyHeaders struct { PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"` PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header"` - SetBasicAuth bool `flag:"set-basic-auth" cfg:"set_basic_auth"` - SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"` - SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header"` + SetBasicAuth bool `flag:"set-basic-auth" cfg:"set_basic_auth"` + SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"` + SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header"` + SetIntrospectionResponse bool `flag:"set-introspect-response-header" cfg:"set_introspect_response_header"` PreferEmailToUser bool `flag:"prefer-email-to-user" cfg:"prefer_email_to_user"` BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"` @@ -200,6 +201,7 @@ func legacyHeadersFlagSet() *pflag.FlagSet { flagSet.Bool("set-basic-auth", false, "set HTTP Basic Auth information in response (useful in Nginx auth_request mode)") flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)") flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)") + flagSet.Bool("set-introspect-response-header", false, "set the Introspect claims in the response headers") flagSet.Bool("prefer-email-to-user", false, "Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. Used in conjunction with -pass-basic-auth and -pass-user-headers") flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header") @@ -260,6 +262,9 @@ func (l *LegacyHeaders) getResponseHeaders() []Header { responseHeaders = append(responseHeaders, getAuthorizationHeader()) } + if l.SetIntrospectionResponse { + responseHeaders = append(responseHeaders, getXAuthIntrospectResponseHeaders()) + } return responseHeaders } @@ -438,6 +443,19 @@ func getXAuthRequestAccessTokenHeader() Header { } } +func getXAuthIntrospectResponseHeaders() Header { + return Header{ + Name: "X-Auth-Introspect-Response", + Values: []HeaderValue{ + { + ClaimSource: &ClaimSource{ + Claim: "introspect-claims", + }, + }, + }, + } +} + type LegacyServer struct { MetricsAddress string `flag:"metrics-address" cfg:"metrics_address"` MetricsSecureAddress string `flag:"metrics-secure-address" cfg:"metrics_secure_address"` @@ -499,6 +517,7 @@ type LegacyProvider struct { LoginURL string `flag:"login-url" cfg:"login_url"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` ProfileURL string `flag:"profile-url" cfg:"profile_url"` + IntrospectURL string `flag:"introspect-url" cfg:"introspect_url"` ProtectedResource string `flag:"resource" cfg:"resource"` ValidateURL string `flag:"validate-url" cfg:"validate_url"` Scope string `flag:"scope" cfg:"scope"` @@ -547,6 +566,7 @@ func legacyProviderFlagSet() *pflag.FlagSet { flagSet.String("login-url", "", "Authentication endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("profile-url", "", "Profile access endpoint") + flagSet.String("introspect-url", "", "Introspect claims access endpoint") flagSet.String("resource", "", "The resource that is protected (Azure AD only)") flagSet.String("validate-url", "", "Access token validation endpoint") flagSet.String("scope", "", "OAuth scope specification") @@ -616,6 +636,7 @@ func (l *LegacyProvider) convert() (Providers, error) { LoginURL: l.LoginURL, RedeemURL: l.RedeemURL, ProfileURL: l.ProfileURL, + IntrospectURL: l.IntrospectURL, ProtectedResource: l.ProtectedResource, ValidateURL: l.ValidateURL, Scope: l.Scope, diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 94a8e4c7..5b4babd8 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -55,6 +55,8 @@ type Provider struct { RedeemURL string `json:"redeemURL,omitempty"` // ProfileURL is the profile access endpoint ProfileURL string `json:"profileURL,omitempty"` + // IntrospectURL is the claims and permissions access Endpoint (OIDC) + IntrospectURL string `json:"introspectURL,omitempty"` // ProtectedResource is the resource that is protected (Azure AD only) ProtectedResource string `json:"resource,omitempty"` // ValidateURL is the access token validation endpoint diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index 8e9d006b..641a0b5b 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -28,6 +28,7 @@ type SessionState struct { User string `msgpack:"u,omitempty"` Groups []string `msgpack:"g,omitempty"` PreferredUsername string `msgpack:"pu,omitempty"` + IntrospectClaims string `msgpack:"ic,omitempty"` } // IsExpired checks whether the session has expired @@ -67,6 +68,9 @@ func (s *SessionState) String() string { if len(s.Groups) > 0 { o += fmt.Sprintf(" groups:%v", s.Groups) } + if s.IntrospectClaims != "" { + o += fmt.Sprintf(" Introspection Claims:%s", s.IntrospectClaims) + } return o + "}" } @@ -95,6 +99,8 @@ func (s *SessionState) GetClaim(claim string) []string { return groups case "preferred_username": return []string{s.PreferredUsername} + case "introspect-claims": + return []string{s.IntrospectClaims} default: return []string{} } diff --git a/pkg/validation/options.go b/pkg/validation/options.go index 5e36f894..9ec274a6 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -98,6 +98,10 @@ func Validate(o *options.Options) error { o.Providers[0].ProfileURL = body.Get("userinfo_endpoint").MustString() } + if o.Providers[0].IntrospectURL == "" { + o.Providers[0].IntrospectURL = body.Get("introspection_endpoint").MustString() + } + o.Providers[0].OIDCConfig.SkipDiscovery = true } } @@ -208,6 +212,7 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { p.LoginURL, msgs = parseURL(o.Providers[0].LoginURL, "login", msgs) p.RedeemURL, msgs = parseURL(o.Providers[0].RedeemURL, "redeem", msgs) p.ProfileURL, msgs = parseURL(o.Providers[0].ProfileURL, "profile", msgs) + p.IntrospectURL, msgs = parseURL(o.Providers[0].IntrospectURL, "introspect", msgs) p.ValidateURL, msgs = parseURL(o.Providers[0].ValidateURL, "validate", msgs) p.ProtectedResource, msgs = parseURL(o.Providers[0].ProtectedResource, "resource", msgs) diff --git a/providers/oidc.go b/providers/oidc.go index d73b4d69..d6e14dcc 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -1,9 +1,12 @@ package providers import ( + "bytes" "context" + b64 "encoding/base64" "errors" "fmt" + "net/url" "reflect" "time" @@ -52,6 +55,13 @@ func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (*s // EnrichSession is called after Redeem to allow providers to enrich session fields // such as User, Email, Groups with provider specific API calls. func (p *OIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { + if p.IntrospectURL.String() != "" { + err := p.enrichFromIntrospectURL(ctx, s) + if err != nil { + logger.Errorf("Warning: Introspect URL request failed: %v", err) + } + } + if p.ProfileURL.String() == "" { if s.Email == "" { return errors.New("id_token did not contain an email and profileURL is not defined") @@ -107,6 +117,38 @@ func (p *OIDCProvider) enrichFromProfileURL(ctx context.Context, s *sessions.Ses return nil } +// enrichFromIntrospectURL enriches a session's claims and permissions via the JSON response of +// an OIDC Introspection URL +func (p *OIDCProvider) enrichFromIntrospectURL(ctx context.Context, s *sessions.SessionState) error { + clientSecret, err := p.GetClientSecret() + if err != nil { + return err + } + params := url.Values{} + params.Add("token", s.AccessToken) + basicAuth := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", p.ClientID, clientSecret))) + respJSON, err := requests.New(p.IntrospectURL.String()). + WithContext(ctx). + WithMethod("POST"). + WithBody(bytes.NewBufferString(params.Encode())). + SetHeader("Authorization", fmt.Sprintf("Basic %s", basicAuth)). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + Do(). + UnmarshalJSON() + + if err != nil { + logger.Errorf("Warning: Error while fetching introspection claims , error %s", err) + return err + } + b, err := respJSON.MarshalJSON() + if err != nil { + logger.Errorf("Cannot convert to JSON , error %s", err) + return err + } + s.IntrospectClaims = b64.StdEncoding.EncodeToString([]byte(string(b))) + return nil +} + // ValidateSession checks that the session's IDToken is still valid func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool { _, err := p.Verifier.Verify(ctx, s.IDToken) diff --git a/providers/provider_data.go b/providers/provider_data.go index a9a41232..ddcf3727 100644 --- a/providers/provider_data.go +++ b/providers/provider_data.go @@ -27,6 +27,7 @@ type ProviderData struct { LoginURL *url.URL RedeemURL *url.URL ProfileURL *url.URL + IntrospectURL *url.URL ProtectedResource *url.URL ValidateURL *url.URL // Auth request params & related, see