Merge pull request #1197 from vmware-tanzu/require-groups-scope

Require groups scope
This commit is contained in:
Mo Khan 2022-06-23 14:06:46 -04:00 committed by GitHub
commit d576e44f0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 753 additions and 328 deletions

View File

@ -31,7 +31,7 @@ import (
// See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator // See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator
// interface, as well as the Response type. // interface, as well as the Response type.
type UserAuthenticator interface { type UserAuthenticator interface {
AuthenticateUser(ctx context.Context, username, password string) (*Response, bool, error) AuthenticateUser(ctx context.Context, username, password string, grantedScopes []string) (*Response, bool, error)
} }
type Response struct { type Response struct {

View File

@ -338,7 +338,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){ UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){
"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID"), "objectGUID": microsoftUUIDFromBinaryAttr("objectGUID"),
}, },
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
pwdLastSetAttribute: upstreamldap.AttributeUnchangedSinceLogin(pwdLastSetAttribute), pwdLastSetAttribute: upstreamldap.AttributeUnchangedSinceLogin(pwdLastSetAttribute),
userAccountControlAttribute: validUserAccountControl, userAccountControlAttribute: validUserAccountControl,
userAccountControlComputedAttribute: validComputedUserAccountControl, userAccountControlComputedAttribute: validComputedUserAccountControl,
@ -437,7 +437,7 @@ func getDomainFromDistinguishedName(distinguishedName string) (string, error) {
return strings.Join(domainComponents[1:], "."), nil return strings.Join(domainComponents[1:], "."), nil
} }
func validUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error { func validUserAccountControl(entry *ldap.Entry, _ provider.RefreshAttributes) error {
userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlAttribute)) userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlAttribute))
if err != nil { if err != nil {
return err return err
@ -450,7 +450,7 @@ func validUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttribut
return nil return nil
} }
func validComputedUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error { func validComputedUserAccountControl(entry *ldap.Entry, _ provider.RefreshAttributes) error {
userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlComputedAttribute)) userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlComputedAttribute))
if err != nil { if err != nil {
return err return err

View File

@ -222,7 +222,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -564,7 +564,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -633,7 +633,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: "sAMAccountName", GroupNameAttribute: "sAMAccountName",
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -705,7 +705,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -784,7 +784,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -847,7 +847,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -997,7 +997,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1146,7 +1146,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1217,7 +1217,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1483,7 +1483,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": groupSAMAccountNameWithDomainSuffix}, GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": groupSAMAccountNameWithDomainSuffix},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1542,7 +1542,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1605,7 +1605,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1668,7 +1668,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1879,7 +1879,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName, GroupNameAttribute: testGroupNameAttrName,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -1941,7 +1941,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
SkipGroupRefresh: true, SkipGroupRefresh: true,
}, },
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
"userAccountControl": validUserAccountControl, "userAccountControl": validUserAccountControl,
"msDS-User-Account-Control-Computed": validComputedUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl,
@ -2083,8 +2083,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks
actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks
copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{} copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.RefreshAttributes) error{}
actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{} actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.RefreshAttributes) error{}
require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks)) require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks))
for k, v := range expectedRefreshAttributeChecks { for k, v := range expectedRefreshAttributeChecks {
require.NotNil(t, actualRefreshAttributeChecks[k]) require.NotNil(t, actualRefreshAttributeChecks[k])
@ -2333,7 +2333,7 @@ func TestValidUserAccountControl(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tt := test tt := test
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := validUserAccountControl(tt.entry, provider.StoredRefreshAttributes{}) err := validUserAccountControl(tt.entry, provider.RefreshAttributes{})
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) require.Error(t, err)
@ -2394,7 +2394,7 @@ func TestValidComputedUserAccountControl(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tt := test tt := test
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := validComputedUserAccountControl(tt.entry, provider.StoredRefreshAttributes{}) err := validComputedUserAccountControl(tt.entry, provider.RefreshAttributes{})
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) require.Error(t, err)

View File

@ -131,7 +131,7 @@ func handleAuthRequestForLDAPUpstreamCLIFlow(
return nil return nil
} }
authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password) authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password, authorizeRequester.GetGrantedScopes())
if err != nil { if err != nil {
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName())
return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication") return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
@ -146,7 +146,7 @@ func handleAuthRequestForLDAPUpstreamCLIFlow(
username = authenticateResponse.User.GetName() username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse) customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
return nil return nil
@ -243,7 +243,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
return nil return nil
} }
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
@ -334,7 +334,7 @@ func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fos
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations. // Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
// There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope // There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope
// at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite. // at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite.
downstreamsession.GrantScopesIfRequested(authorizeRequester) downstreamsession.GrantScopesIfRequested(authorizeRequester, []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, oidc.RequestAudienceScope, oidc.DownstreamGroupsScope})
return authorizeRequester, true return authorizeRequester, true
} }

View File

@ -375,8 +375,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
return urlToReturn return urlToReturn
} }
happyDownstreamScopesRequested := []string{"openid", "profile", "email"} happyDownstreamScopesRequested := []string{"openid", "profile", "email", "groups"}
happyDownstreamScopesGranted := []string{"openid"} happyDownstreamScopesGranted := []string{"openid", "groups"}
happyGetRequestQueryMap := map[string]string{ happyGetRequestQueryMap := map[string]string{
"response_type": "code", "response_type": "code",
@ -495,7 +495,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
} }
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyState happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyState
incomingCookieCSRFValue := "csrf-value-from-cookie" incomingCookieCSRFValue := "csrf-value-from-cookie"
encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue) encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue)
@ -957,7 +957,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState, wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid\+groups&state=` + happyState,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
@ -980,7 +980,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState, wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid\+groups&state=` + happyState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamIDTokenGroups: happyLDAPGroups,

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite" "github.com/ory/fosite"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
@ -52,7 +53,7 @@ func NewHandler(
} }
// Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested. // Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested.
downstreamsession.GrantScopesIfRequested(authorizeRequester) downstreamsession.GrantScopesIfRequested(authorizeRequester, []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, oidc.RequestAudienceScope, oidc.DownstreamGroupsScope})
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens( token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
r.Context(), r.Context(),
@ -76,7 +77,7 @@ func NewHandler(
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
} }
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {

View File

@ -62,8 +62,8 @@ const (
var ( var (
oidcUpstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"} oidcUpstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"}
happyDownstreamScopesRequested = []string{"openid"} happyDownstreamScopesRequested = []string{"openid", "groups"}
happyDownstreamScopesGranted = []string{"openid"} happyDownstreamScopesGranted = []string{"openid", "groups"}
happyDownstreamRequestParamsQuery = url.Values{ happyDownstreamRequestParamsQuery = url.Values{
"response_type": []string{"code"}, "response_type": []string{"code"},
@ -133,7 +133,7 @@ func TestCallbackEndpoint(t *testing.T) {
} }
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState
tests := []struct { tests := []struct {
name string name string
@ -236,6 +236,38 @@ func TestCallbackEndpoint(t *testing.T) {
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
}, },
}, },
{
name: "form_post happy path with no groups scope requested",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(
happyUpstreamStateParam().WithAuthorizeRequestParams(
shallowCopyAndModifyQuery(
happyDownstreamRequestParamsQuery,
map[string]string{
"response_mode": "form_post",
"scope": "openid",
},
).Encode(),
).Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusOK,
wantContentType: "text/html;charset=UTF-8",
wantBodyFormResponseRegexp: `<code id="manual-auth-code">(.+)</code>`,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamRequestedScopes: []string{"openid"},
wantDownstreamGrantedScopes: []string{"openid"},
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{ {
name: "GET with authcode exchange that returns an access token but no refresh token but has a short token lifetime which is stored as a warning in the session", name: "GET with authcode exchange that returns an access token but no refresh token but has a short token lifetime which is stored as a warning in the session",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()),
@ -683,6 +715,33 @@ func TestCallbackEndpoint(t *testing.T) {
name: "state's downstream auth params does not contain openid scope", name: "state's downstream auth params does not contain openid scope",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
method: http.MethodGet, method: http.MethodGet,
path: newRequestPath().
WithState(
happyUpstreamStateParam().
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "profile email groups"}).Encode()).
Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusSeeOther,
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=groups&state=` + happyDownstreamState,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamRequestedScopes: []string{"profile", "email", "groups"},
wantDownstreamGrantedScopes: []string{"groups"},
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{
name: "state's downstream auth params does not contain openid or groups scope",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
method: http.MethodGet,
path: newRequestPath(). path: newRequestPath().
WithState( WithState(
happyUpstreamStateParam(). happyUpstreamStateParam().
@ -695,7 +754,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamRequestedScopes: []string{"profile", "email"}, wantDownstreamRequestedScopes: []string{"profile", "email"},
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamGrantedScopes: []string{},
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
@ -712,16 +771,16 @@ func TestCallbackEndpoint(t *testing.T) {
path: newRequestPath(). path: newRequestPath().
WithState( WithState(
happyUpstreamStateParam(). happyUpstreamStateParam().
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access"}).Encode()). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access groups"}).Encode()).
Build(t, happyStateCodec), Build(t, happyStateCodec),
).String(), ).String(),
csrfCookie: happyCSRFCookie, csrfCookie: happyCSRFCookie,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access&state=` + happyDownstreamState, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState,
wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamRequestedScopes: []string{"openid", "offline_access"}, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "groups"},
wantDownstreamGrantedScopes: []string{"openid", "offline_access"}, wantDownstreamGrantedScopes: []string{"openid", "offline_access", "groups"},
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,

View File

@ -1,4 +1,4 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved. // Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package clientregistry defines Pinniped's OAuth2/OIDC clients. // Package clientregistry defines Pinniped's OAuth2/OIDC clients.
@ -85,6 +85,7 @@ func PinnipedCLI() *Client {
"profile", "profile",
"email", "email",
"pinniped:request-audience", "pinniped:request-audience",
"groups",
}, },
Audience: nil, Audience: nil,
Public: true, Public: true,

View File

@ -1,4 +1,4 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved. // Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package clientregistry package clientregistry
@ -50,7 +50,7 @@ func TestPinnipedCLI(t *testing.T) {
require.Equal(t, []string{"http://127.0.0.1/callback"}, c.GetRedirectURIs()) require.Equal(t, []string{"http://127.0.0.1/callback"}, c.GetRedirectURIs())
require.Equal(t, fosite.Arguments{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, c.GetGrantTypes()) require.Equal(t, fosite.Arguments{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, c.GetGrantTypes())
require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes()) require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes())
require.Equal(t, fosite.Arguments{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience"}, c.GetScopes()) require.Equal(t, fosite.Arguments{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience", "groups"}, c.GetScopes())
require.True(t, c.IsPublic()) require.True(t, c.IsPublic())
require.Nil(t, c.GetAudience()) require.Nil(t, c.GetAudience())
require.Nil(t, c.GetRequestURIs()) require.Nil(t, c.GetRequestURIs())
@ -82,7 +82,8 @@ func TestPinnipedCLI(t *testing.T) {
"offline_access", "offline_access",
"profile", "profile",
"email", "email",
"pinniped:request-audience" "pinniped:request-audience",
"groups"
], ],
"audience": null, "audience": null,
"public": true, "public": true,

View File

@ -10,11 +10,11 @@ import (
"net/url" "net/url"
"time" "time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/strings/slices"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
@ -40,7 +40,7 @@ const (
) )
// MakeDownstreamSession creates a downstream OIDC session. // MakeDownstreamSession creates a downstream OIDC session.
func MakeDownstreamSession(subject string, username string, groups []string, custom *psession.CustomSessionData) *psession.PinnipedSession { func MakeDownstreamSession(subject string, username string, groups []string, grantedScopes []string, custom *psession.CustomSessionData) *psession.PinnipedSession {
now := time.Now().UTC() now := time.Now().UTC()
openIDSession := &psession.PinnipedSession{ openIDSession := &psession.PinnipedSession{
Fosite: &openid.DefaultSession{ Fosite: &openid.DefaultSession{
@ -57,7 +57,9 @@ func MakeDownstreamSession(subject string, username string, groups []string, cus
} }
openIDSession.IDTokenClaims().Extra = map[string]interface{}{ openIDSession.IDTokenClaims().Extra = map[string]interface{}{
oidc.DownstreamUsernameClaim: username, oidc.DownstreamUsernameClaim: username,
oidc.DownstreamGroupsClaim: groups, }
if slices.Contains(grantedScopes, oidc.DownstreamGroupsScope) {
openIDSession.IDTokenClaims().Extra[oidc.DownstreamGroupsClaim] = groups
} }
return openIDSession return openIDSession
} }
@ -147,10 +149,10 @@ func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdent
} }
// GrantScopesIfRequested auto-grants the scopes for which we do not require end-user approval, if they were requested. // GrantScopesIfRequested auto-grants the scopes for which we do not require end-user approval, if they were requested.
func GrantScopesIfRequested(authorizeRequester fosite.AuthorizeRequester) { func GrantScopesIfRequested(authorizeRequester fosite.AuthorizeRequester, scopes []string) {
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID) for _, scope := range scopes {
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess) oidc.GrantScopeIfRequested(authorizeRequester, scope)
oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience") }
} }
// GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order. // GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order.

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite" "github.com/ory/fosite"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
@ -44,8 +45,8 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
return httperr.New(http.StatusBadRequest, "error using state downstream auth params") return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
} }
// Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested. // Automatically grant the openid, offline_access, pinniped:request-audience and groups scopes, but only if they were requested.
downstreamsession.GrantScopesIfRequested(authorizeRequester) downstreamsession.GrantScopesIfRequested(authorizeRequester, []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, oidc.RequestAudienceScope, oidc.DownstreamGroupsScope})
// Get the username and password form params from the POST body. // Get the username and password form params from the POST body.
username := r.PostFormValue(usernameParamName) username := r.PostFormValue(usernameParamName)
@ -59,7 +60,7 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
} }
// Attempt to authenticate the user with the upstream IDP. // Attempt to authenticate the user with the upstream IDP.
authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password) authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password, authorizeRequester.GetGrantedScopes())
if err != nil { if err != nil {
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName())
// There was some problem during authentication with the upstream, aside from bad username/password. // There was some problem during authentication with the upstream, aside from bad username/password.
@ -80,7 +81,7 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
username = authenticateResponse.User.GetName() username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse) customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
return nil return nil

View File

@ -82,8 +82,8 @@ func TestPostLoginEndpoint(t *testing.T) {
} }
) )
happyDownstreamScopesRequested := []string{"openid"} happyDownstreamScopesRequested := []string{"openid", "groups"}
happyDownstreamScopesGranted := []string{"openid"} happyDownstreamScopesGranted := []string{"openid", "groups"}
happyDownstreamRequestParamsQuery := url.Values{ happyDownstreamRequestParamsQuery := url.Values{
"response_type": []string{"code"}, "response_type": []string{"code"},
@ -211,7 +211,7 @@ func TestPostLoginEndpoint(t *testing.T) {
} }
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState
happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}} happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}}
@ -348,7 +348,7 @@ func TestPostLoginEndpoint(t *testing.T) {
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantBodyString: "", wantBodyString: "",
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState, wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamIDTokenGroups: happyLDAPGroups,
@ -410,6 +410,31 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{
name: "happy LDAP login when groups scope is not requested",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"openid"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamRequestedScopes: []string{"openid"},
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{"openid"},
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{ {
name: "bad username LDAP login", name: "bad username LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),

View File

@ -76,6 +76,14 @@ const (
// information. // information.
DownstreamGroupsClaim = "groups" DownstreamGroupsClaim = "groups"
// DownstreamGroupsScope is a custom scope that determines whether the
// groups claim will be returned in ID tokens.
DownstreamGroupsScope = "groups"
// RequestAudienceScope is a custom scope that determines whether a RFC8693 token
// exchange is allowed to request a different audience.
RequestAudienceScope = "pinniped:request-audience"
// CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the // CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the
// Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to // Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to
// a week so that it is unlikely to expire during a login. // a week so that it is unlikely to expire during a login.

View File

@ -108,15 +108,18 @@ type UpstreamLDAPIdentityProviderI interface {
authenticators.UserAuthenticator authenticators.UserAuthenticator
// PerformRefresh performs a refresh against the upstream LDAP identity provider // PerformRefresh performs a refresh against the upstream LDAP identity provider
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) (groups []string, err error) PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes) (groups []string, err error)
} }
type StoredRefreshAttributes struct { // RefreshAttributes contains information about the user from the original login request
// and previous refreshes.
type RefreshAttributes struct {
Username string Username string
Subject string Subject string
DN string DN string
Groups []string Groups []string
AdditionalAttributes map[string]string AdditionalAttributes map[string]string
GrantedScopes []string
} }
type DynamicUpstreamIDPProvider interface { type DynamicUpstreamIDPProvider interface {

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/warning" "k8s.io/apiserver/pkg/warning"
"k8s.io/utils/strings/slices"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
@ -106,19 +107,21 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
return errorsx.WithStack(errMissingUpstreamSessionInternalError()) return errorsx.WithStack(errMissingUpstreamSessionInternalError())
} }
grantedScopes := accessRequest.GetGrantedScopes()
switch customSessionData.ProviderType { switch customSessionData.ProviderType {
case psession.ProviderTypeOIDC: case psession.ProviderTypeOIDC:
return upstreamOIDCRefresh(ctx, session, providerCache) return upstreamOIDCRefresh(ctx, session, providerCache, grantedScopes)
case psession.ProviderTypeLDAP: case psession.ProviderTypeLDAP:
return upstreamLDAPRefresh(ctx, providerCache, session) return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes)
case psession.ProviderTypeActiveDirectory: case psession.ProviderTypeActiveDirectory:
return upstreamLDAPRefresh(ctx, providerCache, session) return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes)
default: default:
return errorsx.WithStack(errMissingUpstreamSessionInternalError()) return errorsx.WithStack(errMissingUpstreamSessionInternalError())
} }
} }
func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession, providerCache oidc.UpstreamIdentityProvidersLister) error { func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession, providerCache oidc.UpstreamIdentityProvidersLister, grantedScopes []string) error {
s := session.Custom s := session.Custom
if s.OIDC == nil { if s.OIDC == nil {
return errorsx.WithStack(errMissingUpstreamSessionInternalError()) return errorsx.WithStack(errMissingUpstreamSessionInternalError())
@ -177,6 +180,8 @@ func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession,
return err return err
} }
groupsScope := slices.Contains(grantedScopes, oidc.DownstreamGroupsScope)
if groupsScope { //nolint:nestif
// If possible, update the user's group memberships. The configured groups claim name (if there is one) may or // If possible, update the user's group memberships. The configured groups claim name (if there is one) may or
// may not be included in the newly fetched and merged claims. It could be missing due to a misconfiguration of the // may not be included in the newly fetched and merged claims. It could be missing due to a misconfiguration of the
// claim name. It could also be missing because the claim was originally found in the ID token during login, but // claim name. It could also be missing because the claim was originally found in the ID token during login, but
@ -202,6 +207,7 @@ func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession,
warnIfGroupsChanged(ctx, oldGroups, refreshedGroups, username) warnIfGroupsChanged(ctx, oldGroups, refreshedGroups, username)
session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = refreshedGroups session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = refreshedGroups
} }
}
// Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in // Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in
// the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding // the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding
@ -291,16 +297,19 @@ func findOIDCProviderByNameAndValidateUID(
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)) WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
} }
func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession) error { func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession, grantedScopes []string) error {
username, err := getDownstreamUsernameFromPinnipedSession(session) username, err := getDownstreamUsernameFromPinnipedSession(session)
if err != nil { if err != nil {
return err return err
} }
subject := session.Fosite.Claims.Subject subject := session.Fosite.Claims.Subject
oldGroups, err := getDownstreamGroupsFromPinnipedSession(session) var oldGroups []string
if slices.Contains(grantedScopes, oidc.DownstreamGroupsScope) {
oldGroups, err = getDownstreamGroupsFromPinnipedSession(session)
if err != nil { if err != nil {
return err return err
} }
}
s := session.Custom s := session.Custom
@ -327,22 +336,26 @@ func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentit
return errorsx.WithStack(errMissingUpstreamSessionInternalError()) return errorsx.WithStack(errMissingUpstreamSessionInternalError())
} }
// run PerformRefresh // run PerformRefresh
groups, err := p.PerformRefresh(ctx, provider.StoredRefreshAttributes{ groups, err := p.PerformRefresh(ctx, provider.RefreshAttributes{
Username: username, Username: username,
Subject: subject, Subject: subject,
DN: dn, DN: dn,
Groups: oldGroups, Groups: oldGroups,
AdditionalAttributes: additionalAttributes, AdditionalAttributes: additionalAttributes,
GrantedScopes: grantedScopes,
}) })
if err != nil { if err != nil {
return errUpstreamRefreshError().WithHint( return errUpstreamRefreshError().WithHint(
"Upstream refresh failed.").WithTrace(err). "Upstream refresh failed.").WithTrace(err).
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType) WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)
} }
groupsScope := slices.Contains(grantedScopes, oidc.DownstreamGroupsScope)
if groupsScope {
// Replace the old value with the new value. // Replace the old value with the new value.
session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = groups session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = groups
warnIfGroupsChanged(ctx, oldGroups, groups, username) warnIfGroupsChanged(ctx, oldGroups, groups, username)
}
return nil return nil
} }

View File

@ -200,7 +200,7 @@ var (
happyAuthRequest = &http.Request{ happyAuthRequest = &http.Request{
Form: url.Values{ Form: url.Values{
"response_type": {"code"}, "response_type": {"code"},
"scope": {"openid profile email"}, "scope": {"openid profile email groups"},
"client_id": {goodClient}, "client_id": {goodClient},
"state": {"some-state-value-with-enough-bytes-to-exceed-min-allowed"}, "state": {"some-state-value-with-enough-bytes-to-exceed-min-allowed"},
"nonce": {goodNonce}, "nonce": {goodNonce},
@ -268,11 +268,12 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
{ {
name: "request is valid and tokens are issued", name: "request is valid and tokens are issued",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid profile email groups") },
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "profile", "email"}, wantRequestedScopes: []string{"openid", "profile", "email", "groups"},
wantGrantedScopes: []string{"openid"}, wantGrantedScopes: []string{"openid", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
}, },
}, },
@ -299,7 +300,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access"},
wantGroups: goodGroups, wantGroups: nil,
}, },
}, },
}, },
@ -316,6 +317,19 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
}, },
}, },
}, },
{
name: "groups scope is requested",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid profile email groups") },
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "profile", "email", "groups"},
wantGrantedScopes: []string{"openid", "groups"},
wantGroups: goodGroups,
},
},
},
// sad path // sad path
{ {
@ -566,12 +580,12 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
{ {
name: "authcode exchange succeeds once and then fails when the same authcode is used again", name: "authcode exchange succeeds once and then fails when the same authcode is used again",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access profile email") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access profile email groups") },
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "profile", "email"}, wantRequestedScopes: []string{"openid", "offline_access", "profile", "email", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
}, },
}, },
@ -630,14 +644,14 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
successfulAuthCodeExchange := tokenEndpointResponseExpectedValues{ successfulAuthCodeExchange := tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "pinniped:request-audience"}, wantRequestedScopes: []string{"openid", "pinniped:request-audience", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience"}, wantGrantedScopes: []string{"openid", "pinniped:request-audience", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
} }
doValidAuthCodeExchange := authcodeExchangeInputs{ doValidAuthCodeExchange := authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid pinniped:request-audience") authRequest.Form.Set("scope", "openid pinniped:request-audience groups")
}, },
want: successfulAuthCodeExchange, want: successfulAuthCodeExchange,
} }
@ -753,13 +767,13 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
name: "access token missing pinniped:request-audience scope", name: "access token missing pinniped:request-audience scope",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid") authRequest.Form.Set("scope", "openid groups")
}, },
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid"}, wantRequestedScopes: []string{"openid", "groups"},
wantGrantedScopes: []string{"openid"}, wantGrantedScopes: []string{"openid", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
}, },
}, },
@ -771,13 +785,13 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
name: "access token missing openid scope", name: "access token missing openid scope",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "pinniped:request-audience") authRequest.Form.Set("scope", "pinniped:request-audience groups")
}, },
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"pinniped:request-audience"}, wantRequestedScopes: []string{"pinniped:request-audience", "groups"},
wantGrantedScopes: []string{"pinniped:request-audience"}, wantGrantedScopes: []string{"pinniped:request-audience", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
}, },
}, },
@ -786,11 +800,28 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
wantResponseBodyContains: `missing the 'openid' scope`, wantResponseBodyContains: `missing the 'openid' scope`,
}, },
{ {
name: "token minting failure", name: "access token missing groups scope",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid pinniped:request-audience") authRequest.Form.Set("scope", "openid pinniped:request-audience")
}, },
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"access_token", "token_type", "expires_in", "scope", "id_token"},
wantRequestedScopes: []string{"openid", "pinniped:request-audience"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience"},
wantGroups: nil,
},
},
requestedAudience: "some-workload-cluster",
wantStatus: http.StatusOK,
},
{
name: "token minting failure",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid pinniped:request-audience groups")
},
// Fail to fetch a JWK signing key after the authcode exchange has happened. // Fail to fetch a JWK signing key after the authcode exchange has happened.
makeOathHelper: makeOauthHelperWithJWTKeyThatWorksOnlyOnce, makeOathHelper: makeOauthHelperWithJWTKeyThatWorksOnlyOnce,
want: successfulAuthCodeExchange, want: successfulAuthCodeExchange,
@ -866,7 +897,10 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims)) require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims))
// Make sure that these are the only fields in the token. // Make sure that these are the only fields in the token.
idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "groups", "username"} idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "username"}
if test.authcodeExchange.want.wantGroups != nil {
idTokenFields = append(idTokenFields, "groups")
}
require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims)) require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims))
// Assert that the returned token has expected claims values. // Assert that the returned token has expected claims values.
@ -880,7 +914,11 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
require.Equal(t, goodSubject, tokenClaims["sub"]) require.Equal(t, goodSubject, tokenClaims["sub"])
require.Equal(t, goodIssuer, tokenClaims["iss"]) require.Equal(t, goodIssuer, tokenClaims["iss"])
require.Equal(t, goodUsername, tokenClaims["username"]) require.Equal(t, goodUsername, tokenClaims["username"])
if test.authcodeExchange.want.wantGroups != nil {
require.Equal(t, toSliceOfInterface(test.authcodeExchange.want.wantGroups), tokenClaims["groups"]) require.Equal(t, toSliceOfInterface(test.authcodeExchange.want.wantGroups), tokenClaims["groups"])
} else {
require.Nil(t, tokenClaims["groups"])
}
// Also assert that some are the same as the original downstream ID token. // Also assert that some are the same as the original downstream ID token.
requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer
@ -1024,8 +1062,8 @@ func TestRefreshGrant(t *testing.T) {
want := tokenEndpointResponseExpectedValues{ want := tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantCustomSessionDataStored: wantCustomSessionDataStored, wantCustomSessionDataStored: wantCustomSessionDataStored,
wantGroups: goodGroups, wantGroups: goodGroups,
} }
@ -1111,7 +1149,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1135,7 +1173,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1163,15 +1201,15 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCAccessTokenCustomSessionData(), customSessionData: initialUpstreamOIDCAccessTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCAccessTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCAccessTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "id_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
wantUpstreamOIDCValidateTokenCall: &expectedUpstreamValidateTokens{ wantUpstreamOIDCValidateTokenCall: &expectedUpstreamValidateTokens{
oidcUpstreamName, oidcUpstreamName,
@ -1228,15 +1266,15 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false),
@ -1257,15 +1295,15 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: []string{"new-group1", "new-group2", "new-group3"}, wantGroups: []string{"new-group1", "new-group2", "new-group3"},
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
@ -1286,15 +1324,15 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: []string{"new-group1", "new-group2", "new-group3"}, wantGroups: []string{"new-group1", "new-group2", "new-group3"},
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
@ -1315,15 +1353,15 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: []string{}, // the user no longer belongs to any groups wantGroups: []string{}, // the user no longer belongs to any groups
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
@ -1344,15 +1382,15 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: goodGroups, // the same groups as from the initial login wantGroups: goodGroups, // the same groups as from the initial login
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
@ -1369,7 +1407,7 @@ func TestRefreshGrant(t *testing.T) {
PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"}, PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"},
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -1379,8 +1417,8 @@ func TestRefreshGrant(t *testing.T) {
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: []string{"new-group1", "new-group2", "new-group3"}, wantGroups: []string{"new-group1", "new-group2", "new-group3"},
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
wantCustomSessionDataStored: happyLDAPCustomSessionData, wantCustomSessionDataStored: happyLDAPCustomSessionData,
@ -1396,7 +1434,7 @@ func TestRefreshGrant(t *testing.T) {
PerformRefreshGroups: []string{}, PerformRefreshGroups: []string{},
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -1406,9 +1444,120 @@ func TestRefreshGrant(t *testing.T) {
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: []string{},
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
wantCustomSessionDataStored: happyLDAPCustomSessionData,
},
},
},
{
name: "ldap refresh grant when the upstream refresh when groups scope not requested on original request or refresh",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
PerformRefreshGroups: []string{},
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access"},
wantGrantedScopes: []string{"openid", "offline_access"}, wantGrantedScopes: []string{"openid", "offline_access"},
wantGroups: []string{}, wantCustomSessionDataStored: happyLDAPCustomSessionData,
wantGroups: nil,
},
},
refreshRequest: refreshRequestInputs{
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid offline_access").ReadCloser()
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"},
wantGrantedScopes: []string{"openid", "offline_access"},
wantGroups: nil,
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
wantCustomSessionDataStored: happyLDAPCustomSessionData,
},
},
},
{
name: "oidc refresh grant when the upstream refresh when groups scope not requested on original request or refresh",
idps: oidctestutil.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()).Build()),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"},
wantGrantedScopes: []string{"openid", "offline_access"},
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
wantGroups: nil,
},
},
refreshRequest: refreshRequestInputs{
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid offline_access").ReadCloser()
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access"},
wantGrantedScopes: []string{"openid", "offline_access"},
wantGroups: nil,
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
},
},
},
{
// 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.
name: "refresh grant when the upstream refresh when groups scope requested on original request but not refresh refresh",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
PerformRefreshGroups: []string{"new-group1", "new-group2", "new-group3"},
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData,
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantCustomSessionDataStored: happyLDAPCustomSessionData,
wantGroups: goodGroups,
},
},
refreshRequest: refreshRequestInputs{
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid offline_access").ReadCloser()
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "groups"},
wantGroups: []string{"new-group1", "new-group2", "new-group3"}, // groups are updated even though the scope was not included
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
wantCustomSessionDataStored: happyLDAPCustomSessionData, wantCustomSessionDataStored: happyLDAPCustomSessionData,
}, },
@ -1427,7 +1576,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1451,7 +1600,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDTokenWithoutRefreshToken()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDTokenWithoutRefreshToken()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1473,7 +1622,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1498,12 +1647,12 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped:request-audience") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped:request-audience groups") },
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(), wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
}, },
@ -1515,8 +1664,8 @@ func TestRefreshGrant(t *testing.T) {
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience", "groups"},
wantGroups: goodGroups, wantGroups: goodGroups,
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
@ -1536,7 +1685,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1626,7 +1775,7 @@ func TestRefreshGrant(t *testing.T) {
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: nil, // this should not happen in practice customSessionData: nil, // this should not happen in practice
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(nil), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(nil),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1646,7 +1795,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: oidcUpstreamType, ProviderType: oidcUpstreamType,
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken},
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: "", // this should not happen in practice ProviderName: "", // this should not happen in practice
@ -1673,7 +1822,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: oidcUpstreamType, ProviderType: oidcUpstreamType,
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken},
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: oidcUpstreamName, ProviderName: oidcUpstreamName,
@ -1700,7 +1849,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: "", // this should not happen in practice ProviderType: "", // this should not happen in practice
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken},
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: oidcUpstreamName, ProviderName: oidcUpstreamName,
@ -1727,7 +1876,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: "not-an-allowed-provider-type", // this should not happen in practice ProviderType: "not-an-allowed-provider-type", // this should not happen in practice
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken},
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: oidcUpstreamName, ProviderName: oidcUpstreamName,
@ -1754,7 +1903,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: oidcUpstreamType, ProviderType: oidcUpstreamType,
OIDC: nil, // this should not happen in practice OIDC: nil, // this should not happen in practice
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: oidcUpstreamName, ProviderName: oidcUpstreamName,
@ -1784,7 +1933,7 @@ func TestRefreshGrant(t *testing.T) {
UpstreamAccessToken: "", UpstreamAccessToken: "",
}, },
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: oidcUpstreamName, ProviderName: oidcUpstreamName,
@ -1814,7 +1963,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: oidcUpstreamType, ProviderType: oidcUpstreamType,
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken},
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: "this-name-will-not-be-found", // this could happen if the OIDCIdentityProvider was deleted since original login ProviderName: "this-name-will-not-be-found", // this could happen if the OIDCIdentityProvider was deleted since original login
@ -1846,7 +1995,7 @@ func TestRefreshGrant(t *testing.T) {
ProviderType: oidcUpstreamType, ProviderType: oidcUpstreamType,
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken},
}, },
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{ // want the initial customSessionData to be unmodified &psession.CustomSessionData{ // want the initial customSessionData to be unmodified
ProviderName: oidcUpstreamName, ProviderName: oidcUpstreamName,
@ -1874,7 +2023,7 @@ func TestRefreshGrant(t *testing.T) {
WithPerformRefreshError(errors.New("some upstream refresh error")).Build()), WithPerformRefreshError(errors.New("some upstream refresh error")).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1899,7 +2048,7 @@ func TestRefreshGrant(t *testing.T) {
Build()), Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1931,7 +2080,7 @@ func TestRefreshGrant(t *testing.T) {
Build()), Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1960,7 +2109,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -1991,7 +2140,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -2022,7 +2171,7 @@ func TestRefreshGrant(t *testing.T) {
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
@ -2048,7 +2197,7 @@ func TestRefreshGrant(t *testing.T) {
PerformRefreshGroups: goodGroups, PerformRefreshGroups: goodGroups,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2069,7 +2218,7 @@ func TestRefreshGrant(t *testing.T) {
PerformRefreshGroups: goodGroups, PerformRefreshGroups: goodGroups,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyActiveDirectoryCustomSessionData, customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData, happyActiveDirectoryCustomSessionData,
@ -2089,7 +2238,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: &psession.CustomSessionData{ customSessionData: &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID, ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName, ProviderName: ldapUpstreamName,
@ -2125,7 +2274,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: &psession.CustomSessionData{ customSessionData: &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID, ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName, ProviderName: activeDirectoryUpstreamName,
@ -2161,7 +2310,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: &psession.CustomSessionData{ customSessionData: &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID, ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName, ProviderName: ldapUpstreamName,
@ -2201,7 +2350,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: &psession.CustomSessionData{ customSessionData: &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID, ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName, ProviderName: ldapUpstreamName,
@ -2242,7 +2391,7 @@ func TestRefreshGrant(t *testing.T) {
PerformRefreshErr: errors.New("Some error performing upstream refresh"), PerformRefreshErr: errors.New("Some error performing upstream refresh"),
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2270,7 +2419,7 @@ func TestRefreshGrant(t *testing.T) {
PerformRefreshErr: errors.New("Some error performing upstream refresh"), PerformRefreshErr: errors.New("Some error performing upstream refresh"),
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyActiveDirectoryCustomSessionData, customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData, happyActiveDirectoryCustomSessionData,
@ -2293,7 +2442,7 @@ func TestRefreshGrant(t *testing.T) {
name: "upstream ldap idp not found", name: "upstream ldap idp not found",
idps: oidctestutil.NewUpstreamIDPListerBuilder(), idps: oidctestutil.NewUpstreamIDPListerBuilder(),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2315,7 +2464,7 @@ func TestRefreshGrant(t *testing.T) {
name: "upstream active directory idp not found", name: "upstream active directory idp not found",
idps: oidctestutil.NewUpstreamIDPListerBuilder(), idps: oidctestutil.NewUpstreamIDPListerBuilder(),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyActiveDirectoryCustomSessionData, customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData, happyActiveDirectoryCustomSessionData,
@ -2341,7 +2490,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2378,7 +2527,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
//fositeSessionData: &openid.DefaultSession{}, //fositeSessionData: &openid.DefaultSession{},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
@ -2420,7 +2569,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
//fositeSessionData: &openid.DefaultSession{}, //fositeSessionData: &openid.DefaultSession{},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
@ -2462,7 +2611,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
//fositeSessionData: &openid.DefaultSession{}, //fositeSessionData: &openid.DefaultSession{},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
@ -2504,7 +2653,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2530,7 +2679,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyActiveDirectoryCustomSessionData, customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData, happyActiveDirectoryCustomSessionData,
@ -2552,7 +2701,7 @@ func TestRefreshGrant(t *testing.T) {
name: "upstream ldap idp not found", name: "upstream ldap idp not found",
idps: oidctestutil.NewUpstreamIDPListerBuilder(), idps: oidctestutil.NewUpstreamIDPListerBuilder(),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2574,7 +2723,7 @@ func TestRefreshGrant(t *testing.T) {
name: "upstream active directory idp not found", name: "upstream active directory idp not found",
idps: oidctestutil.NewUpstreamIDPListerBuilder(), idps: oidctestutil.NewUpstreamIDPListerBuilder(),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyActiveDirectoryCustomSessionData, customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData, happyActiveDirectoryCustomSessionData,
@ -2600,7 +2749,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2637,7 +2786,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2678,7 +2827,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2717,7 +2866,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyLDAPCustomSessionData, customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData, happyLDAPCustomSessionData,
@ -2743,7 +2892,7 @@ func TestRefreshGrant(t *testing.T) {
URL: ldapUpstreamURL, URL: ldapUpstreamURL,
}), }),
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access groups") },
customSessionData: happyActiveDirectoryCustomSessionData, customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData, happyActiveDirectoryCustomSessionData,
@ -2963,7 +3112,7 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs, idps p
requireTokenEndpointBehavior(t, requireTokenEndpointBehavior(t,
test.want, test.want,
goodGroups, // the old groups from the initial login test.want.wantGroups, // the old groups from the initial login
test.customSessionData, // the old custom session data from the initial login test.customSessionData, // the old custom session data from the initial login
wantAtHashClaimInIDToken, wantAtHashClaimInIDToken,
wantNonceValueInIDToken, wantNonceValueInIDToken,
@ -3195,7 +3344,6 @@ func simulateAuthEndpointHavingAlreadyRun(
AuthTime: goodAuthTime, AuthTime: goodAuthTime,
Extra: map[string]interface{}{ Extra: map[string]interface{}{
oidc.DownstreamUsernameClaim: goodUsername, oidc.DownstreamUsernameClaim: goodUsername,
oidc.DownstreamGroupsClaim: goodGroups,
}, },
}, },
Subject: "", // not used, note that callback_handler.go does not set this Subject: "", // not used, note that callback_handler.go does not set this
@ -3214,6 +3362,10 @@ func simulateAuthEndpointHavingAlreadyRun(
if strings.Contains(authRequest.Form.Get("scope"), "pinniped:request-audience") { if strings.Contains(authRequest.Form.Get("scope"), "pinniped:request-audience") {
authRequester.GrantScope("pinniped:request-audience") authRequester.GrantScope("pinniped:request-audience")
} }
if strings.Contains(authRequest.Form.Get("scope"), "groups") {
authRequester.GrantScope("groups")
session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = goodGroups
}
authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session) authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session)
require.NoError(t, err) require.NoError(t, err)
return authResponder return authResponder
@ -3450,10 +3602,13 @@ func requireValidStoredRequest(
require.Equal(t, goodSubject, claims.Subject) require.Equal(t, goodSubject, claims.Subject)
// Our custom claims from the authorize endpoint should still be set. // Our custom claims from the authorize endpoint should still be set.
require.Equal(t, map[string]interface{}{ expectedExtra := map[string]interface{}{
"username": goodUsername, "username": goodUsername,
"groups": toSliceOfInterface(wantGroups), }
}, claims.Extra) if wantGroups != nil {
expectedExtra["groups"] = toSliceOfInterface(wantGroups)
}
require.Equal(t, expectedExtra, claims.Extra)
// We are in charge of setting these fields. For the purpose of testing, we ensure that the // We are in charge of setting these fields. For the purpose of testing, we ensure that the
// sentinel test value is set correctly. // sentinel test value is set correctly.
@ -3572,13 +3727,16 @@ func requireValidIDToken(
// Note that there is a bug in fosite which prevents the `at_hash` claim from appearing in this ID token // Note that there is a bug in fosite which prevents the `at_hash` claim from appearing in this ID token
// during the initial authcode exchange, but does not prevent `at_hash` from appearing in the refreshed ID token. // during the initial authcode exchange, but does not prevent `at_hash` from appearing in the refreshed ID token.
// We can add a workaround for this later. // We can add a workaround for this later.
idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "groups", "username"} idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "username"}
if wantAtHashClaimInIDToken { if wantAtHashClaimInIDToken {
idTokenFields = append(idTokenFields, "at_hash") idTokenFields = append(idTokenFields, "at_hash")
} }
if wantNonceValueInIDToken { if wantNonceValueInIDToken {
idTokenFields = append(idTokenFields, "nonce") idTokenFields = append(idTokenFields, "nonce")
} }
if wantGroupsInIDToken != nil {
idTokenFields = append(idTokenFields, "groups")
}
// 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{}

View File

@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/utils/strings/slices"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
@ -91,7 +92,7 @@ type ValidateTokenAndMergeWithUserInfoArgs struct {
type ValidateRefreshArgs struct { type ValidateRefreshArgs struct {
Ctx context.Context Ctx context.Context
Tok *oauth2.Token Tok *oauth2.Token
StoredAttributes provider.StoredRefreshAttributes StoredAttributes provider.RefreshAttributes
} }
type TestUpstreamLDAPIdentityProvider struct { type TestUpstreamLDAPIdentityProvider struct {
@ -115,7 +116,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetName() string {
return u.Name return u.Name
} }
func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string, grantedScopes []string) (*authenticators.Response, bool, error) {
return u.AuthenticateFunc(ctx, username, password) return u.AuthenticateFunc(ctx, username, password)
} }
@ -123,7 +124,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
return u.URL return u.URL
} }
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) ([]string, error) { func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.RefreshAttributes) ([]string, error) {
if u.performRefreshArgs == nil { if u.performRefreshArgs == nil {
u.performRefreshArgs = make([]*PerformRefreshArgs, 0) u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
} }
@ -1063,10 +1064,16 @@ func validateAuthcodeStorage(
// Check the user's identity, which are put into the downstream ID token's subject, username and groups claims. // Check the user's identity, which are put into the downstream ID token's subject, username and groups claims.
require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject)
require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"]) require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"])
if slices.Contains(wantDownstreamGrantedScopes, "groups") {
require.Len(t, actualClaims.Extra, 2) require.Len(t, actualClaims.Extra, 2)
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
require.NotNil(t, actualDownstreamIDTokenGroups) require.NotNil(t, actualDownstreamIDTokenGroups)
require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups) require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups)
} else {
require.Len(t, actualClaims.Extra, 1)
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
require.Nil(t, actualDownstreamIDTokenGroups)
}
// Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time). // Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time).
testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.RequestedAt, timeComparisonFudgeFactor) testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.RequestedAt, timeComparisonFudgeFactor)

View File

@ -20,11 +20,13 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/utils/strings/slices"
"k8s.io/utils/trace" "k8s.io/utils/trace"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/crypto/ptls"
"go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/endpointaddr"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
@ -118,7 +120,7 @@ type ProviderConfig struct {
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error) GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
// RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected. // RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected.
RefreshAttributeChecks map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error RefreshAttributeChecks map[string]func(*ldap.Entry, provider.RefreshAttributes) error
} }
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. // UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
@ -175,7 +177,7 @@ func (p *Provider) GetConfig() ProviderConfig {
return p.c return p.c
} }
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) ([]string, error) { func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.RefreshAttributes) ([]string, error) {
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()}) t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
userDN := storedRefreshAttributes.DN userDN := storedRefreshAttributes.DN
@ -238,6 +240,10 @@ func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes p
if p.c.GroupSearch.SkipGroupRefresh { if p.c.GroupSearch.SkipGroupRefresh {
return storedRefreshAttributes.Groups, nil return storedRefreshAttributes.Groups, nil
} }
// if we were not granted the groups scope, we should not search for groups or return any.
if !slices.Contains(storedRefreshAttributes.GrantedScopes, oidc.DownstreamGroupsScope) {
return nil, nil
}
mappedGroupNames, err := p.searchGroupsForUserDN(conn, userDN) mappedGroupNames, err := p.searchGroupsForUserDN(conn, userDN)
if err != nil { if err != nil {
@ -398,23 +404,23 @@ func (p *Provider) TestConnection(ctx context.Context) error {
// authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does // authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does
// not bind as that user, so it does not test their password. It returns the same values that a real call to // not bind as that user, so it does not test their password. It returns the same values that a real call to
// AuthenticateUser with the correct password would return. // AuthenticateUser with the correct password would return.
func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticators.Response, bool, error) { func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string, grantedScopes []string) (*authenticators.Response, bool, error) {
endUserBindFunc := func(conn Conn, foundUserDN string) error { endUserBindFunc := func(conn Conn, foundUserDN string) error {
// Act as if the end user bind always succeeds. // Act as if the end user bind always succeeds.
return nil return nil
} }
return p.authenticateUserImpl(ctx, username, endUserBindFunc) return p.authenticateUserImpl(ctx, username, grantedScopes, endUserBindFunc)
} }
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator. // Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { func (p *Provider) AuthenticateUser(ctx context.Context, username, password string, grantedScopes []string) (*authenticators.Response, bool, error) {
endUserBindFunc := func(conn Conn, foundUserDN string) error { endUserBindFunc := func(conn Conn, foundUserDN string) error {
return conn.Bind(foundUserDN, password) return conn.Bind(foundUserDN, password)
} }
return p.authenticateUserImpl(ctx, username, endUserBindFunc) return p.authenticateUserImpl(ctx, username, grantedScopes, endUserBindFunc)
} }
func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, bool, error) { func (p *Provider) authenticateUserImpl(ctx context.Context, username string, grantedScopes []string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, bool, error) {
t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()}) t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()})
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
@ -443,7 +449,7 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi
return nil, false, fmt.Errorf(`error binding as %q before user search: %w`, p.c.BindUsername, err) return nil, false, fmt.Errorf(`error binding as %q before user search: %w`, p.c.BindUsername, err)
} }
response, err := p.searchAndBindUser(conn, username, bindFunc) response, err := p.searchAndBindUser(conn, username, grantedScopes, bindFunc)
if err != nil { if err != nil {
p.traceAuthFailure(t, err) p.traceAuthFailure(t, err)
return nil, false, err return nil, false, err
@ -540,7 +546,7 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e
return searchBase, nil return searchBase, nil
} }
func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, error) { func (p *Provider) searchAndBindUser(conn Conn, username string, grantedScopes []string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, error) {
searchResult, err := conn.Search(p.userSearchRequest(username)) searchResult, err := conn.Search(p.userSearchRequest(username))
if err != nil { if err != nil {
plog.All(`error searching for user`, plog.All(`error searching for user`,
@ -586,10 +592,13 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
return nil, err return nil, err
} }
mappedGroupNames, err := p.searchGroupsForUserDN(conn, userEntry.DN) var mappedGroupNames []string
if slices.Contains(grantedScopes, oidc.DownstreamGroupsScope) {
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
mappedRefreshAttributes := make(map[string]string) mappedRefreshAttributes := make(map[string]string)
for k := range p.c.RefreshAttributeChecks { for k := range p.c.RefreshAttributeChecks {
@ -822,8 +831,8 @@ func (p *Provider) traceRefreshFailure(t *trace.Trace, err error) {
) )
} }
func AttributeUnchangedSinceLogin(attribute string) func(*ldap.Entry, provider.StoredRefreshAttributes) error { func AttributeUnchangedSinceLogin(attribute string) func(*ldap.Entry, provider.RefreshAttributes) error {
return func(entry *ldap.Entry, storedAttributes provider.StoredRefreshAttributes) error { return func(entry *ldap.Entry, storedAttributes provider.RefreshAttributes) error {
prevAttributeValue := storedAttributes.AdditionalAttributes[attribute] prevAttributeValue := storedAttributes.AdditionalAttributes[attribute]
newValues := entry.GetRawAttributeValues(attribute) newValues := entry.GetRawAttributeValues(attribute)

View File

@ -174,6 +174,7 @@ func TestEndUserAuthentication(t *testing.T) {
name string name string
username string username string
password string password string
grantedScopes []string
providerConfig *ProviderConfig providerConfig *ProviderConfig
searchMocks func(conn *mockldapconn.MockConn) searchMocks func(conn *mockldapconn.MockConn)
bindEndUserMocks func(conn *mockldapconn.MockConn) bindEndUserMocks func(conn *mockldapconn.MockConn)
@ -286,6 +287,25 @@ func TestEndUserAuthentication(t *testing.T) {
info.Groups = []string{} info.Groups = []string{}
}), }),
}, },
{
name: "when groups scope isn't granted, don't do group search",
username: testUpstreamUsername,
password: testUpstreamPassword,
grantedScopes: []string{},
providerConfig: providerConfig(nil),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
info := r.User.(*user.DefaultInfo)
info.Groups = nil
}),
},
{ {
name: "when the UsernameAttribute is dn and there is a user search filter provided", name: "when the UsernameAttribute is dn and there is a user search filter provided",
username: testUpstreamUsername, username: testUpstreamUsername,
@ -638,8 +658,8 @@ func TestEndUserAuthentication(t *testing.T) {
username: testUpstreamUsername, username: testUpstreamUsername,
password: testUpstreamPassword, password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) { providerConfig: providerConfig(func(p *ProviderConfig) {
p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{ p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.RefreshAttributes) error{
"some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error { "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.RefreshAttributes) error {
return nil return nil
}, },
} }
@ -676,8 +696,8 @@ func TestEndUserAuthentication(t *testing.T) {
username: testUpstreamUsername, username: testUpstreamUsername,
password: testUpstreamPassword, password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) { providerConfig: providerConfig(func(p *ProviderConfig) {
p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{ p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.RefreshAttributes) error{
"some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error { "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.RefreshAttributes) error {
return nil return nil
}, },
} }
@ -1167,7 +1187,11 @@ func TestEndUserAuthentication(t *testing.T) {
ldapProvider := New(*tt.providerConfig) ldapProvider := New(*tt.providerConfig)
authResponse, authenticated, err := ldapProvider.AuthenticateUser(context.Background(), tt.username, tt.password) if tt.grantedScopes == nil {
tt.grantedScopes = []string{"groups"}
}
authResponse, authenticated, err := ldapProvider.AuthenticateUser(context.Background(), tt.username, tt.password, tt.grantedScopes)
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
switch { switch {
case tt.wantError != "": case tt.wantError != "":
@ -1199,7 +1223,7 @@ func TestEndUserAuthentication(t *testing.T) {
} }
// Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user. // Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user.
authResponse, authenticated, err = ldapProvider.DryRunAuthenticateUser(context.Background(), tt.username) authResponse, authenticated, err = ldapProvider.DryRunAuthenticateUser(context.Background(), tt.username, tt.grantedScopes)
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
switch { switch {
case tt.wantError != "": case tt.wantError != "":
@ -1318,7 +1342,7 @@ func TestUpstreamRefresh(t *testing.T) {
Filter: testGroupSearchFilter, Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupSearchGroupNameAttribute, GroupNameAttribute: testGroupSearchGroupNameAttribute,
}, },
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.RefreshAttributes) error{
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
}, },
} }
@ -1331,6 +1355,7 @@ func TestUpstreamRefresh(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
providerConfig *ProviderConfig providerConfig *ProviderConfig
grantedScopes []string
setupMocks func(conn *mockldapconn.MockConn) setupMocks func(conn *mockldapconn.MockConn)
refreshUserDN string refreshUserDN string
dialError error dialError error
@ -1465,6 +1490,17 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
wantGroups: nil, // do not update groups wantGroups: nil, // do not update groups
}, },
{
name: "happy path where group search is configured but groups scope isn't included",
providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
grantedScopes: []string{},
wantGroups: nil,
},
{ {
name: "error where dial fails", name: "error where dial fails",
providerConfig: providerConfig(nil), providerConfig: providerConfig(nil),
@ -1769,14 +1805,18 @@ func TestUpstreamRefresh(t *testing.T) {
tt.refreshUserDN = testUserSearchResultDNValue // default for all tests tt.refreshUserDN = testUserSearchResultDNValue // default for all tests
} }
if tt.grantedScopes == nil {
tt.grantedScopes = []string{"groups"}
}
initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000")) initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000"))
ldapProvider := New(*tt.providerConfig) ldapProvider := New(*tt.providerConfig)
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
groups, err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{ groups, err := ldapProvider.PerformRefresh(context.Background(), provider.RefreshAttributes{
Username: testUserSearchResultUsernameAttributeValue, Username: testUserSearchResultUsernameAttributeValue,
Subject: subject, Subject: subject,
DN: tt.refreshUserDN, DN: tt.refreshUserDN,
AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded}, AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded},
GrantedScopes: tt.grantedScopes,
}) })
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) require.Error(t, err)
@ -2149,7 +2189,7 @@ func TestAttributeUnchangedSinceLogin(t *testing.T) {
tt := test tt := test
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
initialValRawEncoded := base64.RawURLEncoding.EncodeToString([]byte(initialVal)) initialValRawEncoded := base64.RawURLEncoding.EncodeToString([]byte(initialVal))
err := AttributeUnchangedSinceLogin(attributeName)(tt.entry, provider.StoredRefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}}) err := AttributeUnchangedSinceLogin(attributeName)(tt.entry, provider.RefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}})
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) require.Error(t, err)
require.Equal(t, tt.wantErr, err.Error()) require.Equal(t, tt.wantErr, err.Error())

View File

@ -170,6 +170,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--oidc-skip-browser", "--oidc-skip-browser",
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a browser login via the plugin. // Run "kubectl get namespaces" which should trigger a browser login via the plugin.
@ -192,14 +193,85 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream, })
kubeconfigPath,
sessionCachePath, // If scopes aren't specified, we don't request the groups scope, which means we won't get any groups back in our token.
pinnipedExe, t.Run("with Supervisor OIDC upstream IDP and browser flow, scopes not specified", func(t *testing.T) {
expectedUsername, testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
expectedGroups, t.Cleanup(cancel)
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
expectedUsername := env.SupervisorUpstreamOIDC.Username
// Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster.
testlib.CreateTestClusterRoleBinding(t,
rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedUsername},
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"},
) )
testlib.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{
Verb: "get",
Group: "",
Version: "v1",
Resource: "namespaces",
})
// Create upstream OIDC provider and wait for it to become ready.
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
},
Claims: idpv1alpha1.OIDCClaims{
Username: env.SupervisorUpstreamOIDC.UsernameClaim,
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
},
Client: idpv1alpha1.OIDCClient{
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
},
}, idpv1alpha1.PhaseReady)
// Use a specific session cache for this test.
sessionCachePath := tempDir + "/test-sessions.yaml"
kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{
"get", "kubeconfig",
"--concierge-api-group-suffix", env.APIGroupSuffix,
"--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", authenticator.Name,
"--oidc-skip-browser",
"--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath,
})
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6")
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page)
// Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
t.Logf("waiting for response page %s", downstream.Spec.Issuer)
browsertest.WaitForURL(t, page, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer)))
// The response page should have done the background fetch() and POST'ed to the CLI's callback.
// It should now be in the "success" state.
formpostExpectSuccessState(t, page)
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, []string{}, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience"})
}) })
t.Run("with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) { t.Run("with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) {
@ -256,6 +328,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--oidc-skip-listen", "--oidc-skip-listen",
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a browser login via the plugin. // Run "kubectl get namespaces" which should trigger a browser login via the plugin.
@ -309,14 +382,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
t.Logf("first kubectl command took %s", time.Since(start).String()) t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
t.Run("access token based refresh with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) { t.Run("access token based refresh with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) {
@ -381,6 +447,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--oidc-skip-listen", "--oidc-skip-listen",
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a browser login via the plugin. // Run "kubectl get namespaces" which should trigger a browser login via the plugin.
@ -451,14 +518,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
t.Logf("first kubectl command took %s", time.Since(start).String()) t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
t.Run("with Supervisor OIDC upstream IDP and CLI password flow without web browser", func(t *testing.T) { t.Run("with Supervisor OIDC upstream IDP and CLI password flow without web browser", func(t *testing.T) {
@ -514,6 +574,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow "--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a browser-less CLI prompt login via the plugin. // Run "kubectl get namespaces" which should trigger a browser-less CLI prompt login via the plugin.
@ -540,14 +601,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
t.Logf("first kubectl command took %s", time.Since(start).String()) t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
t.Run("with Supervisor OIDC upstream IDP and CLI password flow when OIDCIdentityProvider disallows it", func(t *testing.T) { t.Run("with Supervisor OIDC upstream IDP and CLI password flow when OIDCIdentityProvider disallows it", func(t *testing.T) {
@ -594,6 +648,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--upstream-identity-provider-flow", "cli_password", "--upstream-identity-provider-flow", "cli_password",
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get --raw /healthz" which should trigger a browser-less CLI prompt login via the plugin. // Run "kubectl get --raw /healthz" which should trigger a browser-less CLI prompt login via the plugin.
@ -655,6 +710,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--concierge-authenticator-type", "jwt", "--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", authenticator.Name, "--concierge-authenticator-name", authenticator.Name,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin. // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
@ -681,14 +737,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
t.Logf("first kubectl command took %s", time.Since(start).String()) t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands
@ -715,6 +764,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--concierge-authenticator-type", "jwt", "--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", authenticator.Name, "--concierge-authenticator-name", authenticator.Name,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Set up the username and password env vars to avoid the interactive prompts. // Set up the username and password env vars to avoid the interactive prompts.
@ -753,14 +803,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
require.NoError(t, os.Unsetenv(usernameEnvVar)) require.NoError(t, os.Unsetenv(usernameEnvVar))
require.NoError(t, os.Unsetenv(passwordEnvVar)) require.NoError(t, os.Unsetenv(passwordEnvVar))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands
@ -787,6 +830,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--concierge-authenticator-type", "jwt", "--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", authenticator.Name, "--concierge-authenticator-name", authenticator.Name,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin. // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
@ -813,14 +857,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
t.Logf("first kubectl command took %s", time.Since(start).String()) t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
// Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands // Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands
@ -847,6 +884,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--concierge-authenticator-type", "jwt", "--concierge-authenticator-type", "jwt",
"--concierge-authenticator-name", authenticator.Name, "--concierge-authenticator-name", authenticator.Name,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Set up the username and password env vars to avoid the interactive prompts. // Set up the username and password env vars to avoid the interactive prompts.
@ -885,14 +923,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
require.NoError(t, os.Unsetenv(usernameEnvVar)) require.NoError(t, os.Unsetenv(usernameEnvVar))
require.NoError(t, os.Unsetenv(passwordEnvVar)) require.NoError(t, os.Unsetenv(passwordEnvVar))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
@ -924,6 +955,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--upstream-identity-provider-flow", "browser_authcode", "--upstream-identity-provider-flow", "browser_authcode",
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a browser login via the plugin. // Run "kubectl get namespaces" which should trigger a browser login via the plugin.
@ -941,14 +973,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
@ -980,6 +1005,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--upstream-identity-provider-flow", "browser_authcode", "--upstream-identity-provider-flow", "browser_authcode",
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a browser login via the plugin. // Run "kubectl get namespaces" which should trigger a browser login via the plugin.
@ -997,14 +1023,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow. // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow.
@ -1036,6 +1055,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var "--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Override the --upstream-identity-provider-flow flag from the kubeconfig using the env var. // Override the --upstream-identity-provider-flow flag from the kubeconfig using the env var.
@ -1059,14 +1079,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
}) })
} }
@ -1296,6 +1309,7 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain(
pinnipedExe string, pinnipedExe string,
expectedUsername string, expectedUsername string,
expectedGroups []string, expectedGroups []string,
downstreamScopes []string,
) { ) {
// Run kubectl, which should work without any prompting for authentication. // Run kubectl, which should work without any prompting for authentication.
kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath) kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath)
@ -1311,7 +1325,6 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain(
require.NoError(t, err) require.NoError(t, err)
})) }))
downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience"}
sort.Strings(downstreamScopes) sort.Strings(downstreamScopes)
token := cache.GetToken(oidcclient.SessionCacheKey{ token := cache.GetToken(oidcclient.SessionCacheKey{
Issuer: downstream.Spec.Issuer, Issuer: downstream.Spec.Issuer,
@ -1326,12 +1339,16 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain(
idTokenClaims := token.IDToken.Claims idTokenClaims := token.IDToken.Claims
require.Equal(t, expectedUsername, idTokenClaims[oidc.DownstreamUsernameClaim]) require.Equal(t, expectedUsername, idTokenClaims[oidc.DownstreamUsernameClaim])
if expectedGroups == nil {
require.Nil(t, idTokenClaims[oidc.DownstreamGroupsClaim])
} else {
// The groups claim in the file ends up as an []interface{}, so adjust our expectation to match. // The groups claim in the file ends up as an []interface{}, so adjust our expectation to match.
expectedGroupsAsEmptyInterfaces := make([]interface{}, 0, len(expectedGroups)) expectedGroupsAsEmptyInterfaces := make([]interface{}, 0, len(expectedGroups))
for _, g := range expectedGroups { for _, g := range expectedGroups {
expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g) expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g)
} }
require.ElementsMatch(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim]) require.ElementsMatch(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim])
}
expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...) expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...)
expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated") expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated")

View File

@ -73,6 +73,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
name string name string
username string username string
password string password string
grantedScopes []string
provider *upstreamldap.Provider provider *upstreamldap.Provider
wantError string wantError string
wantAuthResponse *authenticators.Response wantAuthResponse *authenticators.Response
@ -114,6 +115,18 @@ func TestLDAPSearch_Parallel(t *testing.T) {
ExtraRefreshAttributes: map[string]string{}, ExtraRefreshAttributes: map[string]string{},
}, },
}, },
{
name: "groups scope not in granted scopes",
username: "pinny",
password: pinnyPassword,
grantedScopes: []string{},
provider: upstreamldap.New(*providerConfig(nil)),
wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
ExtraRefreshAttributes: map[string]string{},
},
},
{ {
name: "when the user search filter is already wrapped by parenthesis", name: "when the user search filter is already wrapped by parenthesis",
username: "pinny", username: "pinny",
@ -636,7 +649,10 @@ func TestLDAPSearch_Parallel(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tt := test tt := test
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
authResponse, authenticated, err := tt.provider.AuthenticateUser(ctx, tt.username, tt.password) if tt.grantedScopes == nil {
tt.grantedScopes = []string{"groups"}
}
authResponse, authenticated, err := tt.provider.AuthenticateUser(ctx, tt.username, tt.password, tt.grantedScopes)
switch { switch {
case tt.wantError != "": case tt.wantError != "":
@ -694,9 +710,7 @@ func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
authUserCtx, authUserCtxCancelFunc := context.WithTimeout(context.Background(), 2*time.Minute) authUserCtx, authUserCtxCancelFunc := context.WithTimeout(context.Background(), 2*time.Minute)
defer authUserCtxCancelFunc() defer authUserCtxCancelFunc()
authResponse, authenticated, err := provider.AuthenticateUser(authUserCtx, authResponse, authenticated, err := provider.AuthenticateUser(authUserCtx, env.SupervisorUpstreamLDAP.TestUserCN, env.SupervisorUpstreamLDAP.TestUserPassword, []string{"groups"})
env.SupervisorUpstreamLDAP.TestUserCN, env.SupervisorUpstreamLDAP.TestUserPassword,
)
resultCh <- authUserResult{ resultCh <- authUserResult{
response: authResponse, response: authResponse,
authenticated: authenticated, authenticated: authenticated,

View File

@ -25,6 +25,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/strings/slices"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
@ -163,6 +164,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
createIDP func(t *testing.T) string createIDP func(t *testing.T) string
requestTokenExchangeAud string requestTokenExchangeAud string
downstreamScopes []string
wantLocalhostCallbackToNeverHappen bool wantLocalhostCallbackToNeverHappen bool
wantDownstreamIDTokenSubjectToMatch string wantDownstreamIDTokenSubjectToMatch string
wantDownstreamIDTokenUsernameToMatch func(username string) string wantDownstreamIDTokenUsernameToMatch func(username string) string
@ -331,6 +333,55 @@ func TestSupervisorLogin_Browser(t *testing.T) {
}, },
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
}, },
{
name: "ldap without requesting groups scope",
maybeSkip: skipLDAPTests,
createIDP: func(t *testing.T) string {
idp, _ := createLDAPIdentityProvider(t, nil)
return idp.Name
},
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"},
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t,
downstreamAuthorizeURL,
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
httpClient,
false,
)
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
) + "$",
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
},
wantDownstreamIDTokenGroups: []string{},
},
{
name: "oidc without requesting groups scope",
maybeSkip: skipNever,
createIDP: func(t *testing.T) string {
spec := basicOIDCIdentityProviderSpec()
spec.Claims = idpv1alpha1.OIDCClaims{
Username: env.SupervisorUpstreamOIDC.UsernameClaim,
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
}
spec.AuthorizationConfig = idpv1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
}
return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name
},
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"},
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
wantDownstreamIDTokenGroups: nil,
},
{ {
name: "ldap with browser flow", name: "ldap with browser flow",
maybeSkip: skipLDAPTests, maybeSkip: skipLDAPTests,
@ -1188,6 +1239,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
tt.breakRefreshSessionData, tt.breakRefreshSessionData,
tt.createTestUser, tt.createTestUser,
tt.deleteTestUser, tt.deleteTestUser,
tt.downstreamScopes,
tt.requestTokenExchangeAud, tt.requestTokenExchangeAud,
tt.wantLocalhostCallbackToNeverHappen, tt.wantLocalhostCallbackToNeverHappen,
tt.wantDownstreamIDTokenSubjectToMatch, tt.wantDownstreamIDTokenSubjectToMatch,
@ -1327,6 +1379,7 @@ func testSupervisorLogin(
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string), breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
createTestUser func(t *testing.T) (string, string), createTestUser func(t *testing.T) (string, string),
deleteTestUser func(t *testing.T, username string), deleteTestUser func(t *testing.T, username string),
downstreamScopes []string,
requestTokenExchangeAud string, requestTokenExchangeAud string,
wantLocalhostCallbackToNeverHappen bool, wantLocalhostCallbackToNeverHappen bool,
wantDownstreamIDTokenSubjectToMatch string, wantDownstreamIDTokenSubjectToMatch string,
@ -1441,6 +1494,10 @@ func testSupervisorLogin(
// Start a callback server on localhost. // Start a callback server on localhost.
localCallbackServer := startLocalCallbackServer(t) localCallbackServer := startLocalCallbackServer(t)
if downstreamScopes == nil {
downstreamScopes = []string{"openid", "pinniped:request-audience", "offline_access", "groups"}
}
// Form the OAuth2 configuration corresponding to our CLI client. // Form the OAuth2 configuration corresponding to our CLI client.
// Note that this is not using response_type=form_post, so the Supervisor will redirect to the callback endpoint // Note that this is not using response_type=form_post, so the Supervisor will redirect to the callback endpoint
// directly, without using the Javascript form_post HTML page to POST back to the callback endpoint. The e2e // directly, without using the Javascript form_post HTML page to POST back to the callback endpoint. The e2e
@ -1450,7 +1507,7 @@ func testSupervisorLogin(
ClientID: "pinniped-cli", ClientID: "pinniped-cli",
Endpoint: discovery.Endpoint(), Endpoint: discovery.Endpoint(),
RedirectURL: localCallbackServer.URL, RedirectURL: localCallbackServer.URL,
Scopes: []string{"openid", "pinniped:request-audience", "offline_access"}, Scopes: downstreamScopes,
} }
// Build a valid downstream authorize URL for the supervisor. // Build a valid downstream authorize URL for the supervisor.
@ -1483,9 +1540,9 @@ func testSupervisorLogin(
require.NoError(t, err) require.NoError(t, err)
t.Logf("got callback request: %s", testlib.MaskTokens(callback.URL.String())) t.Logf("got callback request: %s", testlib.MaskTokens(callback.URL.String()))
if wantErrorType == "" { if wantErrorType == "" { // nolint:nestif
require.Equal(t, stateParam.String(), callback.URL.Query().Get("state")) require.Equal(t, stateParam.String(), callback.URL.Query().Get("state"))
require.ElementsMatch(t, []string{"openid", "pinniped:request-audience", "offline_access"}, strings.Split(callback.URL.Query().Get("scope"), " ")) require.ElementsMatch(t, downstreamScopes, strings.Split(callback.URL.Query().Get("scope"), " "))
authcode := callback.URL.Query().Get("code") authcode := callback.URL.Query().Get("code")
require.NotEmpty(t, authcode) require.NotEmpty(t, authcode)
@ -1496,7 +1553,10 @@ func testSupervisorLogin(
tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier())
require.NoError(t, err) require.NoError(t, err)
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"} expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username"}
if slices.Contains(downstreamScopes, "groups") {
expectedIDTokenClaims = append(expectedIDTokenClaims, "groups")
}
verifyTokenResponse(t, verifyTokenResponse(t,
tokenResponse, discovery, downstreamOAuth2Config, nonceParam, tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups) expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
@ -1536,7 +1596,10 @@ func testSupervisorLogin(
require.NoError(t, err) require.NoError(t, err)
// When refreshing, expect to get an "at_hash" claim, but no "nonce" claim. // When refreshing, expect to get an "at_hash" claim, but no "nonce" claim.
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"} expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "at_hash"}
if slices.Contains(downstreamScopes, "groups") {
expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "groups")
}
verifyTokenResponse(t, verifyTokenResponse(t,
refreshedTokenResponse, discovery, downstreamOAuth2Config, "", refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), refreshedGroups) expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), refreshedGroups)

View File

@ -119,6 +119,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
"--concierge-authenticator-name", authenticator.Name, "--concierge-authenticator-name", authenticator.Name,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--credential-cache", credentialCachePath, "--credential-cache", credentialCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a cli-based login. // Run "kubectl get namespaces" which should trigger a cli-based login.
@ -171,7 +172,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
})) }))
// construct the cache key // construct the cache key
downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience"} downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"}
sort.Strings(downstreamScopes) sort.Strings(downstreamScopes)
sessionCacheKey := oidcclient.SessionCacheKey{ sessionCacheKey := oidcclient.SessionCacheKey{
Issuer: downstream.Spec.Issuer, Issuer: downstream.Spec.Issuer,
@ -262,6 +263,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
"--concierge-authenticator-name", authenticator.Name, "--concierge-authenticator-name", authenticator.Name,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--credential-cache", credentialCachePath, "--credential-cache", credentialCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
}) })
// Run "kubectl get namespaces" which should trigger a cli-based login. // Run "kubectl get namespaces" which should trigger a cli-based login.
@ -405,6 +407,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
"--oidc-skip-listen", "--oidc-skip-listen",
"--oidc-ca-bundle", testCABundlePath, "--oidc-ca-bundle", testCABundlePath,
"--oidc-session-cache", sessionCachePath, "--oidc-session-cache", sessionCachePath,
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
"--credential-cache", credentialCachePath, "--credential-cache", credentialCachePath,
}) })