Don't skip upstream group memberships when groups scope is not granted

Background: For dynamic clients, the groups scope is not always allowed
and/or requested by the client, so it will not always be granted by the
Supervisor for an authorization request.

Previously, when the groups scope was not granted, we would skip
searching for upstream groups in some scenarios.

This commit changes the behavior of authorization flows so that even
when the groups scope is not granted we still search for the upstream
group memberships as configured, and we pass the upstream group
memberships into any configured identity transformations. The identity
transformations could potentially reject the user's authentication based
on their upstream group membership.

When the groups scope is not granted, we don't include the groups in
the final Supervisor-issued ID token. This behavior is not changed.
This commit is contained in:
Ryan Richard
2024-02-21 13:12:18 -08:00
parent 5697bb2915
commit 0d31e955ae
15 changed files with 156 additions and 159 deletions

View File

@@ -42,6 +42,7 @@ import (
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
"go.pinniped.dev/internal/celtransformer"
"go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/federationdomain/clientregistry"
"go.pinniped.dev/internal/federationdomain/endpoints/jwks"
@@ -2907,6 +2908,64 @@ func TestRefreshGrant(t *testing.T) {
},
},
},
{
name: "refresh grant when the upstream refresh when groups scope not requested on original request, when using dynamic client, " +
"still runs identity transformations with upstream groups in case transforms want to reject auth based on groups, even though groups would not be included in final ID token",
idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(
upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
IDToken: &oidctypes.IDToken{
Claims: map[string]interface{}{
"sub": goodUpstreamSubject,
"my-groups-claim": []string{"new-group1", "new-group2", "new-group3"}, // refreshed claims includes updated groups
},
},
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).
WithTransformsForFederationDomain(transformtestutil.NewPipeline(t,
[]celtransformer.CELTransformation{
&celtransformer.AllowAuthenticationPolicy{
Expression: `!groups.exists(g, g in ["` + "new-group1" + `"])`, // reject auth for users who belongs to an upstream group
RejectedAuthenticationMessage: `users who belong to certain upstream group are not allowed`,
},
}),
).Build()),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) {
addDynamicClientIDToFormPostBody(r)
r.Form.Set("scope", "openid offline_access username")
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "username"},
wantGrantedScopes: []string{"openid", "offline_access", "username"},
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
wantUsername: goodUsername,
wantGroups: nil,
},
},
refreshRequest: refreshRequestInputs{
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
r.Body = happyRefreshRequestBody(refreshToken).WithClientID("").WithScope("openid offline_access username").ReadCloser()
r.SetBasicAuth(dynamicClientID, testutil.PlaintextPassword1) // Use basic auth header instead.
},
want: tokenEndpointResponseExpectedValues{
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
wantStatus: http.StatusUnauthorized,
// auth was rejected because of the upstream group to which the user belonged, as shown by the configured RejectedAuthenticationMessage appearing here
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Upstream refresh rejected by configured identity policy: users who belong to certain upstream group are not allowed."
}
`),
},
},
},
{
// fosite does not look at the scopes provided in refresh requests, although it is a valid parameter.
// even if 'groups' is not sent in the refresh request, we will send groups all the same.