Added introspection support in OIDC

This commit is contained in:
princemelvin 2021-05-05 18:07:25 +05:30
parent 2de1ad441a
commit 35e1452e9d
7 changed files with 81 additions and 3 deletions

View File

@ -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 |

View File

@ -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,

View File

@ -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

View File

@ -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{}
}

View File

@ -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)

View File

@ -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)

View File

@ -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