Merge pull request #1197 from vmware-tanzu/require-groups-scope
Require groups scope
This commit is contained in:
commit
d576e44f0a
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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{}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user