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") return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
} }
if !authenticated { if !authenticated {
plog.Debug("failed upstream LDAP authentication", "upstreamName", ldapUpstream.GetName()) return writeAuthorizeError(w, oauthHelper, authorizeRequester,
// Return an error according to OIDC spec 3.1.2.6 (second paragraph). fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."))
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
} }
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
@ -130,12 +126,9 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
if !oidcUpstream.AllowsPasswordGrant() { if !oidcUpstream.AllowsPasswordGrant() {
// Return a user-friendly error for this case which is entirely within our control. // Return a user-friendly error for this case which is entirely within our control.
err := errors.WithStack(fosite.ErrAccessDenied. return writeAuthorizeError(w, oauthHelper, authorizeRequester,
WithHint("Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), 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) 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 // 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 // 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.) // 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). return writeAuthorizeError(w, oauthHelper, authorizeRequester,
err := errors.WithStack(fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client 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
} }
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
if err != nil { 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) return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups)
@ -189,9 +182,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
}, },
}) })
if err != nil { if err != nil {
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) return writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
} }
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
@ -258,6 +249,14 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
return nil 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( func makeDownstreamSessionAndReturnAuthcodeRedirect(
r *http.Request, r *http.Request,
w http.ResponseWriter, w http.ResponseWriter,
@ -271,9 +270,7 @@ func makeDownstreamSessionAndReturnAuthcodeRedirect(
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) return writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
} }
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder) oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
@ -285,10 +282,8 @@ func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseW
username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName) username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName)
password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName) password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName)
if username == "" || password == "" { if username == "" || password == "" {
// Return an error according to OIDC spec 3.1.2.6 (second paragraph). _ = writeAuthorizeError(w, oauthHelper, authorizeRequester,
err := errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing or blank username or password.")) 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 "", "", false
} }
return username, password, true 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) { func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider) (fosite.AuthorizeRequester, bool) {
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r) authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
if err != nil { if err != nil {
plog.Info("authorize request error", oidc.FositeErrorForLog(err)...) _ = writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil, false 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.", "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, "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") } 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")) 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" happyLDAPUsername := "some-ldap-user"
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username" happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
happyLDAPPassword := "some-ldap-password" //nolint:gosec happyLDAPPassword := "some-ldap-password" //nolint:gosec
@ -428,18 +466,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
{ {
name: "OIDC upstream password grant happy path using GET", name: "OIDC upstream password grant happy path using GET",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
@ -510,20 +543,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
name: "OIDC upstream password grant happy path using POST", name: "OIDC upstream password grant happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap), body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
@ -625,14 +653,9 @@ func TestAuthorizationEndpoint(t *testing.T) {
path: modifiedHappyGetRequestPath(map[string]string{ path: modifiedHappyGetRequestPath(map[string]string{
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
}), }),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState, wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState,
@ -1032,18 +1055,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantBodyString: "", wantBodyString: "",
}, },
{ {
name: "missing PKCE code_challenge in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 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()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
@ -1079,18 +1097,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantBodyString: "", 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 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()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
@ -1126,18 +1139,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantBodyString: "", 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 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()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
@ -1173,18 +1181,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantBodyString: "", 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 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()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
@ -1224,18 +1227,13 @@ 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 // 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. // 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", 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()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
@ -1282,15 +1280,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
// The following prompt value is illegal when openid is requested, but note that openid is not requested. // 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"}), path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: &expectedPasswordGrant{ wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
performedByUpstreamName: oidcPasswordGrantUpstreamName,
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
Username: oidcUpstreamUsername,
Password: oidcUpstreamPassword,
}},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted
@ -1325,6 +1318,417 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, 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", name: "downstream state does not have enough entropy using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()),

View File

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

View File

@ -803,7 +803,7 @@ func TestCallbackEndpoint(t *testing.T) {
csrfCookie: happyCSRFCookie, csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType, 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{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -819,7 +819,7 @@ func TestCallbackEndpoint(t *testing.T) {
csrfCookie: happyCSRFCookie, csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType, 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{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -835,7 +835,7 @@ func TestCallbackEndpoint(t *testing.T) {
csrfCookie: happyCSRFCookie, csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType, 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{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,

View File

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