diff --git a/CHANGELOG.md b/CHANGELOG.md index 19be37ec..f4dd76e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ ## Changes since v5.1.1 - [#615](https://github.com/oauth2-proxy/oauth2-proxy/pull/615) Kubernetes example based on Kind cluster and Nginx ingress (@EvgeniGordeev) +- [#596](https://github.com/oauth2-proxy/oauth2-proxy/pull/596) Validate Bearer IDTokens in headers with correct provider/extra JWT Verifier (@NickMeves) - [#620](https://github.com/oauth2-proxy/oauth2-proxy/pull/620) Add HealthCheck middleware (@JoelSpeed) - [#597](https://github.com/oauth2-proxy/oauth2-proxy/pull/597) Don't log invalid redirect if redirect is empty (@JoelSpeed) - [#604](https://github.com/oauth2-proxy/oauth2-proxy/pull/604) Add Keycloak local testing environment (@EvgeniGordeev) diff --git a/oauthproxy.go b/oauthproxy.go index 4c2b23c0..8c303df8 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -88,35 +88,36 @@ type OAuthProxy struct { AuthOnlyPath string UserInfoPath string - redirectURL *url.URL // the url to receive requests at - whitelistDomains []string - provider providers.Provider - providerNameOverride string - sessionStore sessionsapi.SessionStore - ProxyPrefix string - SignInMessage string - HtpasswdFile *HtpasswdFile - DisplayHtpasswdForm bool - serveMux http.Handler - SetXAuthRequest bool - PassBasicAuth bool - SetBasicAuth bool - SkipProviderButton bool - PassUserHeaders bool - BasicAuthPassword string - PassAccessToken bool - SetAuthorization bool - PassAuthorization bool - PreferEmailToUser bool - skipAuthRegex []string - skipAuthPreflight bool - skipJwtBearerTokens bool - jwtBearerVerifiers []*oidc.IDTokenVerifier - compiledRegex []*regexp.Regexp - templates *template.Template - realClientIPParser ipapi.RealClientIPParser - Banner string - Footer string + redirectURL *url.URL // the url to receive requests at + whitelistDomains []string + provider providers.Provider + providerNameOverride string + sessionStore sessionsapi.SessionStore + ProxyPrefix string + SignInMessage string + HtpasswdFile *HtpasswdFile + DisplayHtpasswdForm bool + serveMux http.Handler + SetXAuthRequest bool + PassBasicAuth bool + SetBasicAuth bool + SkipProviderButton bool + PassUserHeaders bool + BasicAuthPassword string + PassAccessToken bool + SetAuthorization bool + PassAuthorization bool + PreferEmailToUser bool + skipAuthRegex []string + skipAuthPreflight bool + skipJwtBearerTokens bool + mainJwtBearerVerifier *oidc.IDTokenVerifier + extraJwtBearerVerifiers []*oidc.IDTokenVerifier + compiledRegex []*regexp.Regexp + templates *template.Template + realClientIPParser ipapi.RealClientIPParser + Banner string + Footer string } // UpstreamProxy represents an upstream server to proxy to @@ -317,32 +318,33 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthPro AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix), UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix), - ProxyPrefix: opts.ProxyPrefix, - provider: opts.GetProvider(), - providerNameOverride: opts.ProviderName, - sessionStore: opts.GetSessionStore(), - serveMux: serveMux, - redirectURL: redirectURL, - whitelistDomains: opts.WhitelistDomains, - skipAuthRegex: opts.SkipAuthRegex, - skipAuthPreflight: opts.SkipAuthPreflight, - skipJwtBearerTokens: opts.SkipJwtBearerTokens, - jwtBearerVerifiers: opts.GetJWTBearerVerifiers(), - compiledRegex: opts.GetCompiledRegex(), - realClientIPParser: opts.GetRealClientIPParser(), - SetXAuthRequest: opts.SetXAuthRequest, - PassBasicAuth: opts.PassBasicAuth, - SetBasicAuth: opts.SetBasicAuth, - PassUserHeaders: opts.PassUserHeaders, - BasicAuthPassword: opts.BasicAuthPassword, - PassAccessToken: opts.PassAccessToken, - SetAuthorization: opts.SetAuthorization, - PassAuthorization: opts.PassAuthorization, - PreferEmailToUser: opts.PreferEmailToUser, - SkipProviderButton: opts.SkipProviderButton, - templates: loadTemplates(opts.CustomTemplatesDir), - Banner: opts.Banner, - Footer: opts.Footer, + ProxyPrefix: opts.ProxyPrefix, + provider: opts.GetProvider(), + providerNameOverride: opts.ProviderName, + sessionStore: opts.GetSessionStore(), + serveMux: serveMux, + redirectURL: redirectURL, + whitelistDomains: opts.WhitelistDomains, + skipAuthRegex: opts.SkipAuthRegex, + skipAuthPreflight: opts.SkipAuthPreflight, + skipJwtBearerTokens: opts.SkipJwtBearerTokens, + mainJwtBearerVerifier: opts.GetOIDCVerifier(), + extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(), + compiledRegex: opts.GetCompiledRegex(), + realClientIPParser: opts.GetRealClientIPParser(), + SetXAuthRequest: opts.SetXAuthRequest, + PassBasicAuth: opts.PassBasicAuth, + SetBasicAuth: opts.SetBasicAuth, + PassUserHeaders: opts.PassUserHeaders, + BasicAuthPassword: opts.BasicAuthPassword, + PassAccessToken: opts.PassAccessToken, + SetAuthorization: opts.SetAuthorization, + PassAuthorization: opts.PassAuthorization, + PreferEmailToUser: opts.PreferEmailToUser, + SkipProviderButton: opts.SkipProviderButton, + templates: loadTemplates(opts.CustomTemplatesDir), + Banner: opts.Banner, + Footer: opts.Footer, } } @@ -1139,15 +1141,24 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState return nil, err } - for _, verifier := range p.jwtBearerVerifiers { - bearerToken, err := verifier.Verify(req.Context(), rawBearerToken) + // If we are using an oidc provider, go ahead and try that provider first with its Verifier + // and Bearer Token -> Session converter + if p.mainJwtBearerVerifier != nil { + bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken) + if err == nil { + return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken) + } + } + // Otherwise, attempt to verify against the extra JWT issuers and use a more generic + // Bearer Token -> Session converter + for _, verifier := range p.extraJwtBearerVerifiers { + bearerToken, err := verifier.Verify(req.Context(), rawBearerToken) if err != nil { - logger.Printf("failed to verify bearer token: %v", err) continue } - return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken) + return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken) } return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization")) } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 32385323..0c244bae 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -1578,7 +1578,7 @@ func TestGetJwtSession(t *testing.T) { // Bearer expires := time.Unix(1912151821, 0) session, _ := test.proxy.GetJwtSession(test.req) - assert.Equal(t, session.User, "john@example.com") + assert.Equal(t, session.User, "1234567890") assert.Equal(t, session.Email, "john@example.com") assert.Equal(t, session.ExpiresOn, &expires) assert.Equal(t, session.IDToken, goodJwt) @@ -1590,12 +1590,12 @@ func TestGetJwtSession(t *testing.T) { // Check PassAuthorization, should overwrite Basic header assert.Equal(t, test.req.Header.Get("Authorization"), authHeader) - assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com") + assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "1234567890") assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com") // SetAuthorization and SetXAuthRequest assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader) - assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com") + assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "1234567890") assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com") } diff --git a/pkg/validation/options.go b/pkg/validation/options.go index d3ae4ece..c8d633c0 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -178,10 +178,6 @@ func Validate(o *options.Options) error { } if o.SkipJwtBearerTokens { - // If we are using an oidc provider, go ahead and add that provider to the list - if o.GetOIDCVerifier() != nil { - o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), o.GetOIDCVerifier())) - } // Configure extra issuers if len(o.ExtraJwtIssuers) > 0 { var jwtIssuers []jwtIssuer diff --git a/providers/oidc_test.go b/providers/oidc_test.go index c5d6b521..12b62a47 100644 --- a/providers/oidc_test.go +++ b/providers/oidc_test.go @@ -124,7 +124,6 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) { } func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) { - key, _ := rsa.GenerateKey(rand.Reader, 2048) standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims) return standardClaims.SignedString(key) diff --git a/providers/provider_default.go b/providers/provider_default.go index 9f7ba3c5..14cec9fe 100644 --- a/providers/provider_default.go +++ b/providers/provider_default.go @@ -164,14 +164,13 @@ func (p *ProviderData) CreateSessionStateFromBearerToken(ctx context.Context, ra newSession := &sessions.SessionState{ Email: claims.Email, - User: claims.Email, + User: claims.Subject, PreferredUsername: claims.PreferredUsername, + AccessToken: rawIDToken, + IDToken: rawIDToken, + RefreshToken: "", + ExpiresOn: &idToken.Expiry, } - newSession.AccessToken = rawIDToken - newSession.IDToken = rawIDToken - newSession.RefreshToken = "" - newSession.ExpiresOn = &idToken.Expiry - return newSession, nil } diff --git a/providers/provider_default_test.go b/providers/provider_default_test.go index 74d7096f..745bf2d4 100644 --- a/providers/provider_default_test.go +++ b/providers/provider_default_test.go @@ -2,10 +2,15 @@ package providers import ( "context" + "crypto/rand" + "crypto/rsa" "net/url" "testing" "time" + "github.com/coreos/go-oidc" + "github.com/dgrijalva/jwt-go" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/stretchr/testify/assert" ) @@ -47,3 +52,39 @@ func TestAcrValuesConfigured(t *testing.T) { result := p.GetLoginURL("https://my.test.app/oauth", "") assert.Contains(t, result, "acr_values=testValue") } + +func TestCreateSessionStateFromBearerToken(t *testing.T) { + minimalIDToken := jwt.StandardClaims{ + Audience: "asdf1234", + ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(), + Id: "id-some-id", + IssuedAt: time.Now().Unix(), + Issuer: "https://issuer.example.com", + NotBefore: 0, + Subject: "123456789", + } + // From oidc_test.go + verifier := oidc.NewVerifier( + "https://issuer.example.com", + fakeKeySetStub{}, + &oidc.Config{ClientID: "asdf1234"}, + ) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + assert.NoError(t, err) + rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, minimalIDToken).SignedString(key) + assert.NoError(t, err) + // Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below + idToken, err := verifier.Verify(context.Background(), rawIDToken) + assert.NoError(t, err) + + session, err := (*ProviderData)(nil).CreateSessionStateFromBearerToken(context.Background(), rawIDToken, idToken) + assert.NoError(t, err) + + assert.Equal(t, rawIDToken, session.AccessToken) + assert.Equal(t, rawIDToken, session.IDToken) + assert.Equal(t, "123456789", session.Email) + assert.Equal(t, "123456789", session.User) + assert.Empty(t, session.RefreshToken) + assert.Empty(t, session.PreferredUsername) +}