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")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -434,12 +472,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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,
|
||||||
@ -518,12 +551,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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,
|
||||||
@ -627,12 +655,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
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,
|
||||||
@ -1038,12 +1061,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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),
|
||||||
@ -1085,12 +1103,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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),
|
||||||
@ -1132,12 +1145,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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),
|
||||||
@ -1179,12 +1187,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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),
|
||||||
@ -1230,12 +1233,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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),
|
||||||
@ -1285,12 +1283,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
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()),
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user