This commit is contained in:
Joshua Casey 2023-01-11 17:45:16 -06:00
parent d7b5f4d4ea
commit bfe8dc11ce
3 changed files with 66 additions and 18 deletions

View File

@ -74,6 +74,7 @@ func MakeDownstreamSession(
extras[oidcapi.IDTokenClaimGroups] = groups extras[oidcapi.IDTokenClaimGroups] = groups
} }
if len(additionalClaims) > 0 { if len(additionalClaims) > 0 {
// TODO: make "additionalClaims" a string constant, possibly in oidcapi?
extras["additionalClaims"] = additionalClaims extras["additionalClaims"] = additionalClaims
} }
openIDSession.IDTokenClaims().Extra = extras openIDSession.IDTokenClaims().Extra = extras

View File

@ -285,6 +285,7 @@ type tokenEndpointResponseExpectedValues struct {
wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens
wantCustomSessionDataStored *psession.CustomSessionData wantCustomSessionDataStored *psession.CustomSessionData
wantWarnings []RecordedWarning wantWarnings []RecordedWarning
wantAdditionalClaims map[string]interface{}
} }
type authcodeExchangeInputs struct { type authcodeExchangeInputs struct {
@ -297,6 +298,7 @@ type authcodeExchangeInputs struct {
) )
makeOathHelper OauthHelperFactoryFunc makeOathHelper OauthHelperFactoryFunc
customSessionData *psession.CustomSessionData customSessionData *psession.CustomSessionData
modifySession func(*psession.PinnipedSession)
want tokenEndpointResponseExpectedValues want tokenEndpointResponseExpectedValues
} }
@ -344,6 +346,33 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
}, },
}, },
}, },
{
name: "request is valid and tokens are issued with additional claims",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid profile email username groups") },
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra["additionalClaims"] = map[string]interface{}{
"upstream1": "value1",
"upstream2": "value2",
"upstream3": "value3",
}
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: pinnipedCLIClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "profile", "email", "username", "groups"},
wantGrantedScopes: []string{"openid", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstream1": "value1",
"upstream2": "value2",
"upstream3": "value3",
},
},
},
},
{ {
name: "request is valid and tokens are issued for dynamic client", name: "request is valid and tokens are issued for dynamic client",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
@ -3853,10 +3882,10 @@ func exchangeAuthcodeForTokens(
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
oauthStore = oidc.NewKubeStorage(secrets, oidcClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), bcrypt.MinCost) oauthStore = oidc.NewKubeStorage(secrets, oidcClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), bcrypt.MinCost)
if test.makeOathHelper != nil { if test.makeOathHelper != nil {
oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore, test.customSessionData) oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore, test.customSessionData, test.modifySession)
} else { } else {
// Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage. // Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage.
oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.customSessionData) oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.customSessionData, test.modifySession)
} }
if test.modifyStorage != nil { if test.modifyStorage != nil {
@ -3948,7 +3977,7 @@ func requireTokenEndpointBehavior(
expectedNumberOfIDSessionsStored := 0 expectedNumberOfIDSessionsStored := 0
if wantIDToken { if wantIDToken {
expectedNumberOfIDSessionsStored = 1 expectedNumberOfIDSessionsStored = 1
requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, parsedResponseBody["access_token"].(string), requestTime) requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, parsedResponseBody["access_token"].(string), requestTime)
} }
if wantRefreshToken { if wantRefreshToken {
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, secrets, requestTime) requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, secrets, requestTime)
@ -4058,6 +4087,7 @@ type OauthHelperFactoryFunc func(
authRequest *http.Request, authRequest *http.Request,
store fositestoragei.AllFositeStorage, store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
sessionModifier func(session *psession.PinnipedSession),
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey)
func makeHappyOauthHelper( func makeHappyOauthHelper(
@ -4065,12 +4095,13 @@ func makeHappyOauthHelper(
authRequest *http.Request, authRequest *http.Request,
store fositestoragei.AllFositeStorage, store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
sessionModifier func(session *psession.PinnipedSession),
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) {
t.Helper() t.Helper()
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration())
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData, sessionModifier)
return oauthHelper, authResponder.GetCode(), jwtSigningKey return oauthHelper, authResponder.GetCode(), jwtSigningKey
} }
@ -4092,12 +4123,13 @@ func makeOauthHelperWithJWTKeyThatWorksOnlyOnce(
authRequest *http.Request, authRequest *http.Request,
store fositestoragei.AllFositeStorage, store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
modifySession func(session *psession.PinnipedSession),
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) {
t.Helper() t.Helper()
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}, oidc.DefaultOIDCTimeoutsConfiguration()) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}, oidc.DefaultOIDCTimeoutsConfiguration())
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData, modifySession)
return oauthHelper, authResponder.GetCode(), jwtSigningKey return oauthHelper, authResponder.GetCode(), jwtSigningKey
} }
@ -4106,12 +4138,13 @@ func makeOauthHelperWithNilPrivateJWTSigningKey(
authRequest *http.Request, authRequest *http.Request,
store fositestoragei.AllFositeStorage, store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
modifySession func(session *psession.PinnipedSession),
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) {
t.Helper() t.Helper()
jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration())
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData, modifySession)
return oauthHelper, authResponder.GetCode(), nil return oauthHelper, authResponder.GetCode(), nil
} }
@ -4121,6 +4154,7 @@ func simulateAuthEndpointHavingAlreadyRun(
authRequest *http.Request, authRequest *http.Request,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
modifySession func(session *psession.PinnipedSession),
) fosite.AuthorizeResponder { ) fosite.AuthorizeResponder {
// We only set the fields in the session that Fosite wants us to set. // We only set the fields in the session that Fosite wants us to set.
ctx := context.Background() ctx := context.Background()
@ -4137,6 +4171,10 @@ func simulateAuthEndpointHavingAlreadyRun(
}, },
Custom: initialCustomSessionData, Custom: initialCustomSessionData,
} }
if modifySession != nil {
modifySession(session)
}
authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest) authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest)
require.NoError(t, err) require.NoError(t, err)
if strings.Contains(authRequest.Form.Get("scope"), "openid") { if strings.Contains(authRequest.Form.Get("scope"), "openid") {
@ -4518,6 +4556,7 @@ func requireValidIDToken(
wantNonceValueInIDToken bool, wantNonceValueInIDToken bool,
wantUsernameInIDToken string, wantUsernameInIDToken string,
wantGroupsInIDToken []string, wantGroupsInIDToken []string,
wantAdditionalClaims map[string]interface{},
actualAccessToken string, actualAccessToken string,
requestTime time.Time, requestTime time.Time,
) { ) {
@ -4532,18 +4571,19 @@ func requireValidIDToken(
token := oidctestutil.VerifyECDSAIDToken(t, goodIssuer, wantClientID, jwtSigningKey, idTokenString) token := oidctestutil.VerifyECDSAIDToken(t, goodIssuer, wantClientID, jwtSigningKey, idTokenString)
var claims struct { var claims struct {
Subject string `json:"sub"` Subject string `json:"sub"`
Audience []string `json:"aud"` Audience []string `json:"aud"`
Issuer string `json:"iss"` Issuer string `json:"iss"`
JTI string `json:"jti"` JTI string `json:"jti"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
AccessTokenHash string `json:"at_hash"` AccessTokenHash string `json:"at_hash"`
ExpiresAt int64 `json:"exp"` ExpiresAt int64 `json:"exp"`
IssuedAt int64 `json:"iat"` IssuedAt int64 `json:"iat"`
RequestedAt int64 `json:"rat"` RequestedAt int64 `json:"rat"`
AuthTime int64 `json:"auth_time"` AuthTime int64 `json:"auth_time"`
Groups []string `json:"groups"` Groups []string `json:"groups"`
Username string `json:"username"` Username string `json:"username"`
AdditionalClaims map[string]interface{} `json:"additionalClaims"`
} }
idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "azp", "at_hash"} idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "azp", "at_hash"}
@ -4556,6 +4596,9 @@ func requireValidIDToken(
if wantGroupsInIDToken != nil { if wantGroupsInIDToken != nil {
idTokenFields = append(idTokenFields, "groups") idTokenFields = append(idTokenFields, "groups")
} }
if len(wantAdditionalClaims) > 0 {
idTokenFields = append(idTokenFields, "additionalClaims")
}
// make sure that these are the only fields in the token // make sure that these are the only fields in the token
var m map[string]interface{} var m map[string]interface{}
@ -4573,6 +4616,8 @@ func requireValidIDToken(
require.Equal(t, wantClientID, m["azp"]) require.Equal(t, wantClientID, m["azp"])
require.Equal(t, goodIssuer, claims.Issuer) require.Equal(t, goodIssuer, claims.Issuer)
require.NotEmpty(t, claims.JTI) require.NotEmpty(t, claims.JTI)
require.Equal(t, wantAdditionalClaims, claims.AdditionalClaims)
require.NotEqual(t, map[string]interface{}{}, claims.AdditionalClaims, "additionalClaims may never be present and empty in the id token")
if wantNonceValueInIDToken { if wantNonceValueInIDToken {
require.Equal(t, goodNonce, claims.Nonce) require.Equal(t, goodNonce, claims.Nonce)

View File

@ -928,6 +928,7 @@ func VerifyECDSAIDToken(
return token return token
} }
// RequireAuthCodeRegexpMatch TODO (jtc): rename me?
func RequireAuthCodeRegexpMatch( func RequireAuthCodeRegexpMatch(
t *testing.T, t *testing.T,
actualContent string, actualContent string,
@ -1109,6 +1110,7 @@ func validateAuthcodeStorage(
require.True(t, ok, "expected additionalClaims to be a map[string]interface{}") require.True(t, ok, "expected additionalClaims to be a map[string]interface{}")
require.Equal(t, wantAdditionalClaims, actualAdditionalClaims) require.Equal(t, wantAdditionalClaims, actualAdditionalClaims)
} else { } else {
// TODO: change assertion to verify that key `additionalClaims` DNE in actualClaims
require.Nil(t, actualClaims.Get("additionalClaims"), "additionalClaims must be nil when there are no wanted additional claims") require.Nil(t, actualClaims.Get("additionalClaims"), "additionalClaims must be nil when there are no wanted additional claims")
} }