diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 9a263205..701b6ac9 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -61,7 +61,6 @@ func NewHandler( if oidcUpstream != nil { if len(r.Header.Values(CustomUsernameHeaderName)) > 0 { // The client set a username header, so they are trying to log in with a username/password. - // TODO unit test this return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) } return handleAuthRequestForOIDCUpstreamAuthcodeGrant(r, w, @@ -128,19 +127,6 @@ func handleAuthRequestForLDAPUpstream( return nil } -func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) { - username := r.Header.Get(CustomUsernameHeaderName) - password := r.Header.Get(CustomPasswordHeaderName) - if username == "" || password == "" { - // Return an error according to OIDC spec 3.1.2.6 (second paragraph). - err := errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing or blank username or password.")) - plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) - oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) - return "", "", false - } - return username, password, true -} - func handleAuthRequestForOIDCUpstreamPasswordGrant( r *http.Request, w http.ResponseWriter, @@ -157,11 +143,27 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( return nil } + if !oidcUpstream.AllowsPasswordGrant() { + // Return a user-friendly error for this case which is entirely within our control. + err := errors.WithStack(fosite.ErrAccessDenied. + WithHint("Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), + ) + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil + } + token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password) if err != nil { + // Upstream password grant errors can be generic errors (e.g. a network failure) or can be oauth2.RetrieveError errors + // which represent the http response from the upstream server. These could be a 5XX or some other unexpected error, + // or could be a 400 with a JSON body as described by https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + // which notes that wrong resource owner credentials should result in an "invalid_grant" error. + // However, the exact response is undefined in the sense that there is no such thing as a password grant in + // the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the + // spec and returns something other than an "invalid_grant" error for bad resource owner credentials.) // Return an error according to OIDC spec 3.1.2.6 (second paragraph). - // TODO do not return the full details of the error to the client, but let them know if it is because password grants are disallowed - err := errors.WithStack(fosite.ErrAccessDenied.WithHintf(err.Error())) + err := errors.WithStack(fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) return nil @@ -181,8 +183,9 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) if err != nil { - plog.WarningErr("error while generating and saving authcode", err, "upstreamName", oidcUpstream.GetName()) - return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err) + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil } oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder) @@ -286,6 +289,19 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( return nil } +func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) { + username := r.Header.Get(CustomUsernameHeaderName) + password := r.Header.Get(CustomPasswordHeaderName) + if username == "" || password == "" { + // Return an error according to OIDC spec 3.1.2.6 (second paragraph). + err := errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing or blank username or password.")) + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return "", "", false + } + return username, password, true +} + func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider) (fosite.AuthorizeRequester, bool) { authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r) if err != nil { diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 8736c84b..ce615297 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -18,6 +18,7 @@ import ( "github.com/gorilla/securecookie" "github.com/ory/fosite" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/kubernetes/fake" @@ -37,7 +38,8 @@ import ( func TestAuthorizationEndpoint(t *testing.T) { const ( - passwordGrantUpstreamName = "some-password-granting-oidc-idp" + oidcUpstreamName = "some-oidc-idp" + oidcPasswordGrantUpstreamName = "some-password-granting-oidc-idp" oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL @@ -126,6 +128,12 @@ func TestAuthorizationEndpoint(t *testing.T) { "state": happyState, } + fositeAccessDeniedErrorQuery = map[string]string{ + "error": "access_denied", + "error_description": "The resource owner or authorization server denied the request. Make sure that the request you are making is valid. Maybe the credential or request parameters you are using are limited in scope or otherwise restricted.", + "state": happyState, + } + fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery = map[string]string{ "error": "access_denied", "error_description": "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", @@ -137,6 +145,12 @@ func TestAuthorizationEndpoint(t *testing.T) { "error_description": "The resource owner or authorization server denied the request. Missing or blank username or password.", "state": happyState, } + + fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery = map[string]string{ + "error": "access_denied", + "error_description": "The resource owner or authorization server denied the request. Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.", + "state": happyState, + } ) hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } @@ -158,18 +172,20 @@ func TestAuthorizationEndpoint(t *testing.T) { upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth") require.NoError(t, err) - upstreamOIDCIdentityProvider := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). - WithName("some-oidc-idp"). - WithClientID("some-client-id"). - WithAuthorizationURL(*upstreamAuthURL). - WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow - WithAllowPasswordGrant(false). - WithPasswordGrantError(errors.New("should not have used password grant on this instance")). - Build() + upstreamOIDCIdentityProvider := func() *oidctestutil.TestUpstreamOIDCIdentityProvider { + return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName(oidcUpstreamName). + WithClientID("some-client-id"). + WithAuthorizationURL(*upstreamAuthURL). + WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow + WithAllowPasswordGrant(false). + WithPasswordGrantError(errors.New("should not have used password grant on this instance")). + Build() + } passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). - WithName(passwordGrantUpstreamName). + WithName(oidcPasswordGrantUpstreamName). WithClientID("some-client-id"). WithAuthorizationURL(*upstreamAuthURL). WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow @@ -309,7 +325,7 @@ func TestAuthorizationEndpoint(t *testing.T) { if csrfValueOverride != "" { csrf = csrfValueOverride } - upstreamName := upstreamOIDCIdentityProvider.Name + upstreamName := oidcUpstreamName if upstreamNameOverride != "" { upstreamName = upstreamNameOverride } @@ -396,7 +412,7 @@ func TestAuthorizationEndpoint(t *testing.T) { tests := []testCase{ { name: "OIDC upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -419,7 +435,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: &expectedPasswordGrant{ - performedByUpstreamName: passwordGrantUpstreamName, + performedByUpstreamName: oidcPasswordGrantUpstreamName, args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ Username: oidcUpstreamUsername, Password: oidcUpstreamPassword, @@ -458,8 +474,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, { - name: "OIDC upstream happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "OIDC upstream browser flow happy path using GET with a CSRF cookie", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -475,8 +491,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyStringWithLocationInHref: true, }, { - name: "OIDC upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "OIDC upstream browser flow happy path using POST", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -493,6 +509,34 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, }, + { + name: "OIDC upstream password grant happy path using POST", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodPost, + path: "/some/path", + contentType: "application/x-www-form-urlencoded", + body: encodeQuery(happyGetRequestQueryMap), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenUsername: oidcUpstreamUsername, + wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, { name: "LDAP upstream happy path using POST", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -516,8 +560,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, { - name: "OIDC upstream happy path with prompt param login passed through to redirect uri", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "OIDC upstream browser flow happy path with prompt param login passed through to redirect uri", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -535,8 +579,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, }, { - name: "OIDC upstream with error while decoding CSRF cookie just generates a new cookie and succeeds as usual", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "OIDC upstream browser flow with error while decoding CSRF cookie just generates a new cookie and succeeds as usual", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -554,8 +598,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyStringWithLocationInHref: true, }, { - name: "OIDC upstream happy path when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "OIDC upstream browser flow happy path when downstream redirect uri matches what is configured for client except for the port number", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -574,6 +618,34 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "OIDC upstream password grant happy path when downstream redirect uri matches what is configured for client except for the port number", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{ + "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client + }), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenUsername: oidcUpstreamUsername, + wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURIWithDifferentPort, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, { name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -597,8 +669,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, { - name: "OIDC upstream happy path when downstream requested scopes include offline_access", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "OIDC upstream browser flow happy path when downstream requested scopes include offline_access", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -626,6 +698,29 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "Bad Gateway: unexpected error during upstream authentication\n", }, + { + name: "wrong upstream credentials for OIDC password grant authentication", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + passwordGrantUpstreamOIDCIdentityProviderBuilder(). + // This is similar to the error that would be returned by the underlying call to oauth2.PasswordCredentialsToken() + WithPasswordGrantError(&oauth2.RetrieveError{Response: &http.Response{Status: "fake status"}, Body: []byte("fake body")}). + Build(), + ), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr("wrong-password"), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: "wrong-password", + }}, + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedErrorQuery), + wantBodyString: "", + }, { name: "wrong upstream password for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -675,8 +770,32 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "downstream redirect uri does not match what is configured for client when using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "missing upstream password on request for OIDC password grant authentication", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: nil, // do not send header + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, + { + name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery), + wantBodyString: "", + }, + { + name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -690,6 +809,19 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, + { + name: "downstream redirect uri does not match what is configured for client when using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{ + "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", + }), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusBadRequest, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidRedirectURIErrorBody, + }, { name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -704,8 +836,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, { - name: "downstream client does not exist when using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "downstream client does not exist when using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -717,6 +849,17 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidClientErrorBody, }, + { + name: "downstream client does not exist when using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusUnauthorized, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidClientErrorBody, + }, { name: "downstream client does not exist when using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -727,8 +870,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyJSON: fositeInvalidClientErrorBody, }, { - name: "response type is unsupported when using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "response type is unsupported when using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -741,6 +884,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, + { + name: "response type is unsupported when using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), + wantBodyString: "", + }, { name: "response type is unsupported when using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -752,8 +907,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "downstream scopes do not match what is configured for client using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -766,6 +921,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, + { + name: "downstream scopes do not match what is configured for client using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), + wantBodyString: "", + }, { name: "downstream scopes do not match what is configured for client using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -779,8 +946,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing response type in request using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "missing response type in request using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -793,6 +960,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, + { + name: "missing response type in request using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), + wantBodyString: "", + }, { name: "missing response type in request using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -804,8 +983,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing client id in request using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "missing client id in request using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -817,6 +996,17 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidClientErrorBody, }, + { + name: "missing client id in request using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusUnauthorized, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidClientErrorBody, + }, { name: "missing client id in request using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -827,8 +1017,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyJSON: fositeInvalidClientErrorBody, }, { - name: "missing PKCE code_challenge in request using OIDC upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "missing PKCE code_challenge in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -841,6 +1031,25 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantBodyString: "", }, + { + name: "missing PKCE code_challenge in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, { name: "missing PKCE code_challenge in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -855,8 +1064,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error }, { - name: "invalid value for PKCE code_challenge_method in request using OIDC upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -869,6 +1078,25 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantBodyString: "", }, + { + name: "invalid value for PKCE code_challenge_method in request using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, { name: "invalid value for PKCE code_challenge_method in request using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -883,8 +1111,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error }, { - name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -897,6 +1125,25 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", }, + { + name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, { name: "when PKCE code_challenge_method in request is `plain` using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -911,8 +1158,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error }, { - name: "missing PKCE code_challenge_method in request using OIDC upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -925,6 +1172,25 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", }, + { + name: "missing PKCE code_challenge_method in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, { name: "missing PKCE code_challenge_method in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -940,9 +1206,9 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running - // through that part of the fosite library when using an OIDC upstream. - name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + // through that part of the fosite library when using an OIDC upstream browser flow. + name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -955,6 +1221,27 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantBodyString: "", }, + { + // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running + // through that part of the fosite library when using an OIDC upstream password grant. + name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error + }, { // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an LDAP upstream. @@ -971,8 +1258,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error }, { - name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -990,6 +1277,33 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + // The following prompt value is illegal when openid is requested, but note that openid is not requested. + path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantPasswordGrantCall: &expectedPasswordGrant{ + performedByUpstreamName: oidcPasswordGrantUpstreamName, + args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{ + Username: oidcUpstreamUsername, + Password: oidcUpstreamPassword, + }}, + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenUsername: oidcUpstreamUsername, + wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, + wantDownstreamRequestedScopes: []string{"email"}, // only email was requested + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: []string{}, // no scopes granted + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -1012,8 +1326,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, { - name: "downstream state does not have enough entropy using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "downstream state does not have enough entropy using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1026,6 +1340,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantBodyString: "", }, + { + name: "downstream state does not have enough entropy using OIDC upstream password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), + wantBodyString: "", + }, { name: "downstream state does not have enough entropy using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -1039,8 +1365,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "error while encoding upstream state param using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "error while encoding upstream state param using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1053,8 +1379,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error encoding upstream state param\n", }, { - name: "error while encoding CSRF cookie value for new cookie using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "error while encoding CSRF cookie value for new cookie using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1067,8 +1393,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error encoding CSRF cookie\n", }, { - name: "error while generating CSRF token using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "error while generating CSRF token using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: sadCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1081,8 +1407,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error generating CSRF token\n", }, { - name: "error while generating nonce using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "error while generating nonce using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: sadNonceGenerator, @@ -1095,8 +1421,8 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error generating nonce param\n", }, { - name: "error while generating PKCE using OIDC upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + name: "error while generating PKCE using OIDC upstream browser flow", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: sadPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1119,7 +1445,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured: multiple OIDC", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider, upstreamOIDCIdentityProvider), // more than one not allowed + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider(), upstreamOIDCIdentityProvider()), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -1137,7 +1463,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured: both OIDC and LDAP", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -1146,7 +1472,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "PUT is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), method: http.MethodPut, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -1155,7 +1481,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "PATCH is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), method: http.MethodPatch, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -1164,7 +1490,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "DELETE is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), method: http.MethodDelete, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index d29979c8..e45564e9 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -251,11 +251,17 @@ func FositeErrorForLog(err error) []interface{} { rfc6749Error := fosite.ErrorToRFC6749Error(err) keysAndValues := make([]interface{}, 0) keysAndValues = append(keysAndValues, "name") - keysAndValues = append(keysAndValues, rfc6749Error.ErrorField) + keysAndValues = append(keysAndValues, rfc6749Error.Error()) // Error() returns the ErrorField keysAndValues = append(keysAndValues, "status") - keysAndValues = append(keysAndValues, rfc6749Error.Status()) + keysAndValues = append(keysAndValues, rfc6749Error.Status()) // Status() encodes the CodeField as a string keysAndValues = append(keysAndValues, "description") - keysAndValues = append(keysAndValues, rfc6749Error.DescriptionField) + keysAndValues = append(keysAndValues, rfc6749Error.GetDescription()) // GetDescription() returns the DescriptionField and the HintField + keysAndValues = append(keysAndValues, "debug") + keysAndValues = append(keysAndValues, rfc6749Error.Debug()) // Debug() returns the DebugField + if cause := rfc6749Error.Cause(); cause != nil { // Cause() returns the underlying error, or nil + keysAndValues = append(keysAndValues, "cause") + keysAndValues = append(keysAndValues, cause.Error()) + } return keysAndValues } diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index e7e2e8bb..7ca73ee8 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -73,7 +73,7 @@ func (p *ProviderConfig) AllowsPasswordGrant() bool { func (p *ProviderConfig) PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) { // Disallow this grant when requested. if !p.AllowPasswordGrant { - return nil, fmt.Errorf("resource owner password grant is not allowed for this upstream provider according to its configuration") + return nil, fmt.Errorf("resource owner password credentials grant is not allowed for this upstream provider according to its configuration") } // Note that this implicitly uses the scopes from p.Config.Scopes. diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index baed255f..abba4417 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -147,7 +147,7 @@ func TestProviderConfig(t *testing.T) { { name: "password grant not allowed", disallowPasswordGrant: true, // password grant is not allowed in this ProviderConfig - wantErr: "resource owner password grant is not allowed for this upstream provider according to its configuration", + wantErr: "resource owner password credentials grant is not allowed for this upstream provider according to its configuration", }, { name: "token request fails with http error",