Refactor some authorize and callback error handling, and add more tests

This commit is contained in:
Ryan Richard 2021-08-18 12:06:46 -07:00
parent 04b8f0b455
commit 61c21d2977
5 changed files with 548 additions and 145 deletions

View File

@ -97,12 +97,8 @@ func handleAuthRequestForLDAPUpstream(
return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
}
if !authenticated {
plog.Debug("failed upstream LDAP authentication", "upstreamName", ldapUpstream.GetName())
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
err = errors.WithStack(fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."))
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."))
}
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
@ -130,12 +126,9 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
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
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHint(
"Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."))
}
token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password)
@ -147,16 +140,16 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
// 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).
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
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client
}
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
if err != nil {
return err
// Return a user-friendly error for this case which is entirely within our control.
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()),
)
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups)
@ -189,9 +182,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
},
})
if err != nil {
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
}
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
@ -258,6 +249,14 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
return nil
}
func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error) error {
errWithStack := errors.WithStack(err)
plog.Info("authorize response error", oidc.FositeErrorForLog(errWithStack)...)
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
}
func makeDownstreamSessionAndReturnAuthcodeRedirect(
r *http.Request,
w http.ResponseWriter,
@ -271,9 +270,7 @@ func makeDownstreamSessionAndReturnAuthcodeRedirect(
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil {
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
}
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
@ -285,10 +282,8 @@ func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseW
username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName)
password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName)
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)
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."))
return "", "", false
}
return username, password, true
@ -297,8 +292,7 @@ func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseW
func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider) (fosite.AuthorizeRequester, bool) {
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
if err != nil {
plog.Info("authorize request error", oidc.FositeErrorForLog(err)...)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
return nil, false
}

View File

@ -151,6 +151,36 @@ func TestAuthorizationEndpoint(t *testing.T) {
"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,
}
fositeAccessDeniedWithInvalidEmailVerifiedHintErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Reason: email_verified claim in upstream ID token has invalid format.",
"state": happyState,
}
fositeAccessDeniedWithFalseEmailVerifiedHintErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Reason: email_verified claim in upstream ID token has false value.",
"state": happyState,
}
fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Reason: required claim in upstream ID token missing.",
"state": happyState,
}
fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Reason: required claim in upstream ID token is empty.",
"state": happyState,
}
fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Reason: required claim in upstream ID token has invalid format.",
"state": happyState,
}
)
hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") }
@ -201,6 +231,14 @@ func TestAuthorizationEndpoint(t *testing.T) {
WithUpstreamAuthcodeExchangeError(errors.New("should not have tried to exchange upstream authcode on this instance"))
}
happyUpstreamPasswordGrantMockExpectation := &expectedPasswordGrant{
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
},
}
happyLDAPUsername := "some-ldap-user"
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
happyLDAPPassword := "some-ldap-password" //nolint:gosec
@ -434,12 +472,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
@ -518,12 +551,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
@ -627,12 +655,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState,
@ -1038,12 +1061,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
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,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
@ -1085,12 +1103,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
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,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
@ -1132,12 +1145,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
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,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
@ -1179,12 +1187,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
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,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
@ -1230,12 +1233,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
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,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
@ -1285,12 +1283,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
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,
}},
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted
@ -1325,6 +1318,417 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutUsernameClaim().WithoutGroupsClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenGroups: []string{},
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithUsernameClaim("email").
WithIDTokenClaim("email", "joe@whitehouse.gov").Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: "joe@whitehouse.gov",
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithUsernameClaim("email").
WithIDTokenClaim("email", "joe@whitehouse.gov").
WithIDTokenClaim("email_verified", true).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: "joe@whitehouse.gov",
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithUsernameClaim("some-claim").
WithIDTokenClaim("some-claim", "joe").
WithIDTokenClaim("email", "joe@whitehouse.gov").
WithIDTokenClaim("email_verified", false).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: "joe",
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithUsernameClaim("email").
WithIDTokenClaim("email", "joe@whitehouse.gov").
WithIDTokenClaim("email_verified", "supposed to be boolean").Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithInvalidEmailVerifiedHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithUsernameClaim("email").
WithIDTokenClaim("email", "joe@whitehouse.gov").
WithIDTokenClaim("email_verified", false).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithFalseEmailVerifiedHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream IDP provides username claim configuration as `sub`, so the downstream token subject should be exactly what they asked for",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithUsernameClaim("sub").Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamSubject,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token has a non-array value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithIDTokenClaim(oidcUpstreamGroupsClaim, "notAnArrayGroup1 notAnArrayGroup2").Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: []string{"notAnArrayGroup1 notAnArrayGroup2"},
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token is a slice of interfaces",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"group1", "group2"}).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: []string{"group1", "group2"},
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream ID token does not contain requested username claim",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim(oidcUpstreamUsernameClaim).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token does not contain requested groups claim",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim(oidcUpstreamGroupsClaim).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: []string{},
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
},
{
name: "OIDC upstream password grant: upstream ID token contains username claim with weird format",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamUsernameClaim, 42).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token contains username claim with empty string value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamUsernameClaim, "").Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token does not contain iss claim when using default username claim config",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim("iss").WithoutUsernameClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token does has an empty string value for iss claim when using default username claim config",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("iss", "").WithoutUsernameClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token has an non-string iss claim when using default username claim config",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("iss", 42).WithoutUsernameClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token does not contain sub claim when using default username claim config",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim("sub").WithoutUsernameClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token does has an empty string value for sub claim when using default username claim config",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("sub", "").WithoutUsernameClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token has an non-string sub claim when using default username claim config",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("sub", 42).WithoutUsernameClaim().Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token contains groups claim with weird format",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamGroupsClaim, 42).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token contains groups claim where one element is invalid",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"foo", 7}).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
{
name: "OIDC upstream password grant: upstream ID token contains groups claim with invalid null type",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamGroupsClaim, nil).Build(),
),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "",
},
{
name: "downstream state does not have enough entropy using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()),

View File

@ -70,7 +70,7 @@ func NewHandler(
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
if err != nil {
return err
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
}
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups)

View File

@ -803,7 +803,7 @@ func TestCallbackEndpoint(t *testing.T) {
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: groups claim in upstream ID token has invalid format\n",
wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
@ -819,7 +819,7 @@ func TestCallbackEndpoint(t *testing.T) {
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: groups claim in upstream ID token has invalid format\n",
wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
@ -835,7 +835,7 @@ func TestCallbackEndpoint(t *testing.T) {
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: groups claim in upstream ID token has invalid format\n",
wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,

View File

@ -6,7 +6,6 @@ package downstreamsession
import (
"fmt"
"net/http"
"net/url"
"time"
@ -15,7 +14,7 @@ import (
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
@ -27,6 +26,12 @@ const (
// The name of the email_verified claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
emailVerifiedClaimName = "email_verified"
requiredClaimMissingErr = constable.Error("required claim in upstream ID token missing")
requiredClaimInvalidFormatErr = constable.Error("required claim in upstream ID token has invalid format")
requiredClaimEmptyErr = constable.Error("required claim in upstream ID token is empty")
emailVerifiedClaimInvalidFormatErr = constable.Error("email_verified claim in upstream ID token has invalid format")
emailVerifiedClaimFalseErr = constable.Error("email_verified claim in upstream ID token has false value")
)
// MakeDownstreamSession creates a downstream OIDC session.
@ -107,7 +112,7 @@ func getSubjectAndUsernameFromUpstreamIDToken(
"configuredUsernameClaim", usernameClaimName,
"emailVerifiedClaim", emailVerifiedAsInterface,
)
return "", "", httperr.New(http.StatusUnprocessableEntity, "email_verified claim in upstream ID token has invalid format")
return "", "", emailVerifiedClaimInvalidFormatErr
}
if !emailVerified {
plog.Warning(
@ -115,7 +120,7 @@ func getSubjectAndUsernameFromUpstreamIDToken(
"upstreamName", upstreamIDPConfig.GetName(),
"configuredUsernameClaim", usernameClaimName,
)
return "", "", httperr.New(http.StatusUnprocessableEntity, "email_verified claim in upstream ID token has false value")
return "", "", emailVerifiedClaimFalseErr
}
}
@ -135,7 +140,7 @@ func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
"upstreamName", upstreamIDPName,
"claimName", claimName,
)
return "", httperr.New(http.StatusUnprocessableEntity, "required claim in upstream ID token missing")
return "", requiredClaimMissingErr
}
valueAsString, ok := value.(string)
@ -145,7 +150,7 @@ func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
"upstreamName", upstreamIDPName,
"claimName", claimName,
)
return "", httperr.New(http.StatusUnprocessableEntity, "required claim in upstream ID token has invalid format")
return "", requiredClaimInvalidFormatErr
}
if valueAsString == "" {
@ -154,7 +159,7 @@ func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
"upstreamName", upstreamIDPName,
"claimName", claimName,
)
return "", httperr.New(http.StatusUnprocessableEntity, "required claim in upstream ID token is empty")
return "", requiredClaimEmptyErr
}
return valueAsString, nil
@ -190,7 +195,7 @@ func getGroupsFromUpstreamIDToken(
"upstreamName", upstreamIDPConfig.GetName(),
"configuredGroupsClaim", groupsClaimName,
)
return nil, httperr.New(http.StatusUnprocessableEntity, "groups claim in upstream ID token has invalid format")
return nil, requiredClaimInvalidFormatErr
}
return groupsAsArray, nil