Refactor some authorize and callback error handling, and add more tests
This commit is contained in:
parent
04b8f0b455
commit
61c21d2977
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -428,18 +466,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantBodyStringWithLocationInHref: true,
|
||||
},
|
||||
{
|
||||
name: "OIDC upstream password grant happy path using GET",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: happyGetRequestPath,
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "OIDC upstream password grant happy path using GET",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: happyGetRequestPath,
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: htmlContentType,
|
||||
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
|
||||
@ -510,20 +543,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
},
|
||||
{
|
||||
name: "OIDC upstream password grant happy path using POST",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodPost,
|
||||
path: "/some/path",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: encodeQuery(happyGetRequestQueryMap),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "OIDC upstream password grant happy path using POST",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodPost,
|
||||
path: "/some/path",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: encodeQuery(happyGetRequestQueryMap),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: htmlContentType,
|
||||
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
|
||||
@ -625,14 +653,9 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
path: modifiedHappyGetRequestPath(map[string]string{
|
||||
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
|
||||
}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: htmlContentType,
|
||||
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState,
|
||||
@ -1032,18 +1055,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantBodyString: "",
|
||||
},
|
||||
{
|
||||
name: "missing PKCE code_challenge in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "missing PKCE code_challenge in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: "application/json; charset=utf-8",
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
|
||||
@ -1079,18 +1097,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantBodyString: "",
|
||||
},
|
||||
{
|
||||
name: "invalid value for PKCE code_challenge_method in request using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "invalid value for PKCE code_challenge_method in request using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: "application/json; charset=utf-8",
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
|
||||
@ -1126,18 +1139,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantBodyString: "",
|
||||
},
|
||||
{
|
||||
name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: "application/json; charset=utf-8",
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
||||
@ -1173,18 +1181,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantBodyString: "",
|
||||
},
|
||||
{
|
||||
name: "missing PKCE code_challenge_method in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "missing PKCE code_challenge_method in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: "application/json; charset=utf-8",
|
||||
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
|
||||
// through that part of the fosite library when using an OIDC upstream password grant.
|
||||
name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream password grant",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream password grant",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||
wantStatus: http.StatusFound,
|
||||
wantContentType: "application/json; charset=utf-8",
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
|
||||
@ -1282,15 +1280,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
method: http.MethodGet,
|
||||
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantPasswordGrantCall: &expectedPasswordGrant{
|
||||
performedByUpstreamName: oidcPasswordGrantUpstreamName,
|
||||
args: &oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs{
|
||||
Username: oidcUpstreamUsername,
|
||||
Password: oidcUpstreamPassword,
|
||||
}},
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(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()),
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user