diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 970c895e..bc89115b 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -318,7 +318,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, Dialer: c.ldapDialer, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - upstreamldap.PwdLastSetAttribute: upstreamldap.PwdUnchangedSinceLogin, + upstreamldap.PwdLastSetAttribute: upstreamldap.AttributeUnchangedSinceLogin(upstreamldap.PwdLastSetAttribute), upstreamldap.UserAccountControlAttribute: upstreamldap.ValidUserAccountControl, upstreamldap.UserAccountControlComputedAttribute: upstreamldap.ValidComputedUserAccountControl, }, diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index a4cec598..6b98390e 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -222,7 +222,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -543,7 +543,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -604,7 +604,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -668,7 +668,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -732,7 +732,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -795,7 +795,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -929,7 +929,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -1058,7 +1058,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }}, @@ -1113,7 +1113,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -1318,7 +1318,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": upstreamldap.GroupSAMAccountNameWithDomainSuffix}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -1375,7 +1375,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -1436,7 +1436,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -1491,7 +1491,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, @@ -1692,7 +1692,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": upstreamldap.ValidUserAccountControl, "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, }, diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index 50c35788..02d3c761 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -348,20 +348,47 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ "upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬" }, "ldap": { - "userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾" + "userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾", + "extraRefreshAttributes": { + "ĝ": [ + "IȽ齤士bEǎ", + "跞@)¿,ɭS隑ip偶宾儮猷V麹" + ], + "齁š%OpKȱ藚ɏ¬Ê蒭堜]ȗ韚ʫ": [ + "aŚB碠k9帴ʘ赱ŕ瑹xȢ~1Į", + "睋邔\u0026Ű惫蜀Ģ¡圔鎥墀jMʥ", + "+î艔垎0" + ] + } }, "activedirectory": { - "userDN": "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞" + "userDN": "ȝƋ鬯犦獢9c5¤.岵", + "extraRefreshAttributes": { + "\u0026錝D肁Ŷɽ蔒PR}Ųʓ": [ + "_º$+溪ŸȢŒų崓ļ" + ], + "P姧骦:駝重Eȫ": [ + "ɂ/", + "Ƀɫ囤", + "鉌X縆跣ŠɞɮƎ賿礣©硇" + ], + "a齙\\蹼偦歛ơ": [ + "y衑拁Ȃ縅", + "Vƅȭǝ*擦28Dž ", + "ã置bņ抰蛖" + ] + } } } }, "requestedAudience": [ - "ŚB碠k9" + "õC嶃", + "ɣƜ/気ū齢q萮左/篣AÚƄŕ~čfV", + "x荃墎]ac[" ], "grantedAudience": [ - "ʘ赱", - "ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔", - "墀jMʥ" + "XôĖ给溬d鞕", + "腿tʏƲ%}ſ¯Ɣ 籌Tǘ乚Ȥ2Ķě" ] }, "version": "2" diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index fdb9653e..e1f6a5c9 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -396,6 +396,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { // while the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed, // if it adds a new field that can be fuzzed, this check will fail // thus if AuthorizeRequest changes, we will detect it here (though we could possibly miss an omitempty field) + t.Log(authorizeCodeSessionJSONFromFuzzing) require.JSONEq(t, ExpectedAuthorizeCodeSessionJSONFromFuzzing, authorizeCodeSessionJSONFromFuzzing) } diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index bbfdba75..d0023362 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -122,12 +122,14 @@ func handleAuthRequestForLDAPUpstream( if idpType == psession.ProviderTypeLDAP { customSessionData.LDAP = &psession.LDAPSessionData{ - UserDN: dn, + UserDN: dn, + ExtraRefreshAttributes: authenticateResponse.User.GetExtra(), } } if idpType == psession.ProviderTypeActiveDirectory { customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{ - UserDN: dn, + UserDN: dn, + ExtraRefreshAttributes: authenticateResponse.User.GetExtra(), } } diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 564ef380..47c69c9c 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -283,6 +283,7 @@ func TestAuthorizationEndpoint(t *testing.T) { Name: happyLDAPUsernameFromAuthenticator, UID: happyLDAPUID, Groups: happyLDAPGroups, + Extra: map[string][]string{"some-refresh-attribute": {"some-refresh-attribute-value"}}, }, DN: happyLDAPUserDN, }, true, nil @@ -442,7 +443,8 @@ func TestAuthorizationEndpoint(t *testing.T) { OIDC: nil, LDAP: nil, ActiveDirectory: &psession.ActiveDirectorySessionData{ - UserDN: happyLDAPUserDN, + UserDN: happyLDAPUserDN, + ExtraRefreshAttributes: map[string][]string{"some-refresh-attribute": {"some-refresh-attribute-value"}}, }, } @@ -452,7 +454,8 @@ func TestAuthorizationEndpoint(t *testing.T) { ProviderType: psession.ProviderTypeLDAP, OIDC: nil, LDAP: &psession.LDAPSessionData{ - UserDN: happyLDAPUserDN, + UserDN: happyLDAPUserDN, + ExtraRefreshAttributes: map[string][]string{"some-refresh-attribute": {"some-refresh-attribute-value"}}, }, ActiveDirectory: nil, } diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index 1fd75cf2..33894c33 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -98,10 +98,11 @@ type UpstreamLDAPIdentityProviderI interface { } type StoredRefreshAttributes struct { - Username string - Subject string - DN string - AuthTime time.Time + Username string + Subject string + DN string + AuthTime time.Time + AdditionalAttributes map[string][]string } type DynamicUpstreamIDPProvider interface { diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index d578959b..d9de7fab 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -180,6 +180,13 @@ func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentit return errorsx.WithStack(errMissingUpstreamSessionInternalError) } + var additionalAttributes map[string][]string + if s.ProviderType == psession.ProviderTypeLDAP { + additionalAttributes = s.LDAP.ExtraRefreshAttributes + } else { + additionalAttributes = s.ActiveDirectory.ExtraRefreshAttributes + } + // get ldap/ad provider out of cache p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache) if err != nil { @@ -190,10 +197,11 @@ func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentit } // run PerformRefresh err = p.PerformRefresh(ctx, provider.StoredRefreshAttributes{ - Username: username, - Subject: subject, - DN: dn, - AuthTime: session.IDTokenClaims().AuthTime, + Username: username, + Subject: subject, + DN: dn, + AuthTime: session.IDTokenClaims().AuthTime, + AdditionalAttributes: additionalAttributes, }) if err != nil { return errorsx.WithStack(errUpstreamRefreshError.WithHint( diff --git a/internal/psession/pinniped_session.go b/internal/psession/pinniped_session.go index 8009f91d..3a33e1f6 100644 --- a/internal/psession/pinniped_session.go +++ b/internal/psession/pinniped_session.go @@ -66,12 +66,14 @@ type OIDCSessionData struct { // LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider. type LDAPSessionData struct { - UserDN string `json:"userDN"` + UserDN string `json:"userDN"` + ExtraRefreshAttributes map[string][]string `json:"extraRefreshAttributes,omitempty"` } // ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider. type ActiveDirectorySessionData struct { - UserDN string `json:"userDN"` + UserDN string `json:"userDN"` + ExtraRefreshAttributes map[string][]string `json:"extraRefreshAttributes,omitempty"` } // NewPinnipedSession returns a new empty session. diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 781d9f5e..ea320168 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -593,6 +593,15 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c } sort.Strings(mappedGroupNames) + mappedRefreshAttributes := make(map[string][]string) + for k := range p.c.RefreshAttributeChecks { + mappedVal, err := p.getSearchResultAttributeValue(k, userEntry, username) + if err != nil { + return nil, err + } + mappedRefreshAttributes[k] = []string{mappedVal} + } + // Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! err = bindFunc(conn, userEntry.DN) if err != nil { @@ -615,6 +624,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c Name: mappedUsername, UID: mappedUID, Groups: mappedGroupNames, + Extra: mappedRefreshAttributes, }, DN: userEntry.DN, } @@ -676,7 +686,7 @@ func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest { TimeLimit: 90, TypesOnly: false, Filter: "(objectClass=*)", // we already have the dn, so the filter doesn't matter - Attributes: p.refreshAttributes(), + Attributes: p.userSearchRequestedAttributes(), Controls: nil, // this could be used to enable paging, but we're already limiting the result max size } } @@ -689,6 +699,9 @@ func (p *Provider) userSearchRequestedAttributes() []string { if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName { attributes = append(attributes, p.c.UserSearch.UIDAttribute) } + for k := range p.c.RefreshAttributeChecks { + attributes = append(attributes, k) + } return attributes } @@ -703,14 +716,6 @@ func (p *Provider) groupSearchRequestedAttributes() []string { } } -func (p *Provider) refreshAttributes() []string { - attributes := p.userSearchRequestedAttributes() - for k := range p.c.RefreshAttributeChecks { - attributes = append(attributes, k) - } - return attributes -} - func (p *Provider) userSearchFilter(username string) string { safeUsername := p.escapeUsernameForSearchFilter(username) if len(p.c.UserSearch.Filter) == 0 { @@ -869,43 +874,22 @@ func getDomainFromDistinguishedName(distinguishedName string) (string, error) { return strings.Join(domainComponents[1:], "."), nil } -func PwdUnchangedSinceLogin(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error { - authTime := attributes.AuthTime - pwdLastSetWin32Format := entry.GetAttributeValues(PwdLastSetAttribute) - if len(pwdLastSetWin32Format) != 1 { - return fmt.Errorf("expected to find 1 value for %s attribute, but found %d", PwdLastSetAttribute, len(pwdLastSetWin32Format)) +func AttributeUnchangedSinceLogin(attribute string) func(*ldap.Entry, provider.StoredRefreshAttributes) error { + return func(entry *ldap.Entry, storedAttributes provider.StoredRefreshAttributes) error { + prevAttributeValues := storedAttributes.AdditionalAttributes[attribute] + newValues := entry.GetAttributeValues(attribute) + + if len(newValues) != 1 { + return fmt.Errorf(`expected to find 1 value for "%s" attribute, but found %d`, attribute, len(newValues)) + } + if len(prevAttributeValues) != 1 { + return fmt.Errorf(`expected to find 1 stored value for "%s" attribute, but found %d`, attribute, len(prevAttributeValues)) + } + if prevAttributeValues[0] != newValues[0] { + return fmt.Errorf(`value for attribute "%s" has changed since initial value at login. Previous value: "%s", new value: "%s"`, attribute, prevAttributeValues[0], newValues[0]) + } + return nil } - // convert to a time.Time - pwdLastSetParsed, err := win32timestampToTime(pwdLastSetWin32Format[0]) - if err != nil { - return err - } - - // if pwdLastSet > authTime, that means that the password has been changed since the initial login. - // return an error so the user is prompted to log in again. - if pwdLastSetParsed.After(authTime) { - return fmt.Errorf("password has changed since login. login time: %s, password set time: %s", authTime, pwdLastSetParsed) - } - return nil -} - -func win32timestampToTime(win32timestamp string) (time.Time, error) { - // take a win32 timestamp (represented as the number of 100 ns intervals since - // January 1, 1601) and make a time.Time - - const unixTimeBaseAsWin = 116_444_736_000_000_000 // The unix base time (January 1, 1970 UTC) as 100 ns since Win32 epoch (1601-01-01) - const hundredNsToSecFactor = 10_000_000 - - win32Time, err := strconv.ParseInt(win32timestamp, 10, 64) - if err != nil { - return time.Time{}, fmt.Errorf("couldn't parse as timestamp") - } - - unixsec := (win32Time - unixTimeBaseAsWin) / hundredNsToSecFactor - unixns := (win32Time % hundredNsToSecFactor) * 100 - - convertedTime := time.Unix(unixsec, unixns).UTC() - return convertedTime, nil } func ValidUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error { diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 9b573b9c..dd26501a 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -160,6 +160,7 @@ func TestEndUserAuthentication(t *testing.T) { Name: testUserSearchResultUsernameAttributeValue, UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)), Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, + Extra: map[string][]string{}, } if editFunc != nil { editFunc(u) @@ -507,6 +508,7 @@ func TestEndUserAuthentication(t *testing.T) { Name: testUserSearchResultUsernameAttributeValue, UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)), Groups: []string{"a", "b", "c"}, + Extra: map[string][]string{}, }, DN: testUserSearchResultDNValue, }, @@ -610,6 +612,66 @@ func TestEndUserAuthentication(t *testing.T) { r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"} }), }, + { + name: "requesting additional refresh related attributes", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{ + "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error { + return nil + }, + } + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Attributes = append(r.Attributes, "some-attribute-to-check-during-refresh") + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + ldap.NewEntryAttribute("some-attribute-to-check-during-refresh", []string{"some-attribute-value"}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Extra = map[string][]string{"some-attribute-to-check-during-refresh": {"some-attribute-value"}} + }), + }, + { + name: "requesting additional refresh related attributes, but they aren't returned", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{ + "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error { + return nil + }, + } + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Attributes = append(r.Attributes, "some-attribute-to-check-during-refresh") + })).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: "found 0 values for attribute \"some-attribute-to-check-during-refresh\" while searching for user \"some-upstream-username\", but expected 1 result", + }, { name: "override group parsing when domain can't be determined from dn", username: testUpstreamUsername, @@ -1265,7 +1327,9 @@ func TestUpstreamRefresh(t *testing.T) { UIDAttribute: testUserSearchUIDAttribute, UsernameAttribute: testUserSearchUsernameAttribute, }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{pwdLastSetAttribute: PwdUnchangedSinceLogin}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), + }, } tests := []struct { @@ -1520,7 +1584,7 @@ func TestUpstreamRefresh(t *testing.T) { wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result", }, { - name: "search result has a recent pwdLastSet value", + name: "search result has a changed pwdLastSet value", providerConfig: providerConfig, setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -1539,7 +1603,7 @@ func TestUpstreamRefresh(t *testing.T) { }, { Name: pwdLastSetAttribute, - Values: []string{"132803468800000000"}, + Values: []string{"132801740800000001"}, }, }, }, @@ -1547,7 +1611,7 @@ func TestUpstreamRefresh(t *testing.T) { }, nil).Times(1) conn.EXPECT().Close().Times(1) }, - wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: password has changed since login. login time: 2021-11-01 23:43:19 +0000 UTC, password set time: 2021-11-02 17:14:40 +0000 UTC", + wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login. Previous value: \"132801740800000000\", new value: \"132801740800000001\"", }, } @@ -1577,10 +1641,11 @@ func TestUpstreamRefresh(t *testing.T) { subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" authTime := time.Date(2021, time.November, 1, 23, 43, 19, 0, time.UTC) err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{ - Username: testUserSearchResultUsernameAttributeValue, - Subject: subject, - DN: testUserSearchResultDNValue, - AuthTime: authTime, + Username: testUserSearchResultUsernameAttributeValue, + Subject: subject, + DN: testUserSearchResultDNValue, + AuthTime: authTime, + AdditionalAttributes: map[string][]string{pwdLastSetAttribute: {"132801740800000000"}}, }) if tt.wantErr != "" { require.Error(t, err) @@ -1960,148 +2025,67 @@ func TestGetDomainFromDistinguishedName(t *testing.T) { } } -func TestPwdUnchangedSinceLogin(t *testing.T) { - authTime := "2021-11-01T23:43:19.826433579Z" // this is the format that fosite automatically stores - authTimeParsed, err := time.Parse(time.RFC3339Nano, authTime) - require.NoError(t, err) - pwdResetTimeAfterAuthTime := "132803468800000000" // Nov 2 - pwdResetTimeBeforeAuthTime := "132801740800000000" // Oct 31 +func TestAttributeUnchangedSinceLogin(t *testing.T) { + initialVal := "some-attribute-value" + changedVal := "some-different-attribute-value" + attributeName := "some-attribute-name" tests := []struct { name string - authTime *time.Time entry *ldap.Entry wantResult bool wantErr string }{ { - name: "happy path where password has not been reset since login", - authTime: &authTimeParsed, + name: "happy path where value has not changed since login", entry: &ldap.Entry{ DN: "some-dn", Attributes: []*ldap.EntryAttribute{ { - Name: "pwdLastSet", - Values: []string{pwdResetTimeBeforeAuthTime}, + Name: attributeName, + Values: []string{initialVal}, }, }, }, }, { - name: "password has been reset since login", - authTime: &authTimeParsed, + name: "password has been reset since login", entry: &ldap.Entry{ DN: "some-dn", Attributes: []*ldap.EntryAttribute{ { - Name: "pwdLastSet", - Values: []string{pwdResetTimeAfterAuthTime}, + Name: attributeName, + Values: []string{changedVal}, }, }, }, - wantErr: "password has changed since login. login time: 2021-11-01 23:43:19.826433579 +0000 UTC, password set time: 2021-11-02 17:14:40 +0000 UTC", + wantErr: "value for attribute \"some-attribute-name\" has changed since initial value at login. Previous value: \"some-attribute-value\", new value: \"some-different-attribute-value\"", }, { - name: "ldap timestamp is in the wrong format", - authTime: &authTimeParsed, - entry: &ldap.Entry{ - DN: "some-dn", - Attributes: []*ldap.EntryAttribute{ - { - Name: "pwdLastSet", - Values: []string{"invalid"}, - }, - }, - }, - wantErr: "couldn't parse as timestamp", - }, - { - name: "no value for pwdLastSet attribute", - authTime: &authTimeParsed, + name: "no value for attribute attribute", entry: &ldap.Entry{ DN: "some-dn", Attributes: []*ldap.EntryAttribute{}, }, - wantErr: "expected to find 1 value for pwdLastSet attribute, but found 0", + wantErr: "expected to find 1 value for \"some-attribute-name\" attribute, but found 0", }, { - name: "too many values for pwdLastSet attribute", - authTime: &authTimeParsed, + name: "too many values for attribute", entry: &ldap.Entry{ DN: "some-dn", Attributes: []*ldap.EntryAttribute{ { - Name: "pwdLastSet", + Name: attributeName, Values: []string{"val1", "val2"}, }, }, }, - wantErr: "expected to find 1 value for pwdLastSet attribute, but found 2", + wantErr: "expected to find 1 value for \"some-attribute-name\" attribute, but found 2", }, } for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - err := PwdUnchangedSinceLogin(tt.entry, provider.StoredRefreshAttributes{AuthTime: *tt.authTime}) - if tt.wantErr != "" { - require.Error(t, err) - require.Equal(t, tt.wantErr, err.Error()) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestWin32TimestampToTime(t *testing.T) { - happyPasswordChangeTime := time.Date(2021, time.January, 2, 0, 12, 21, 0, time.UTC).UTC() - tests := []struct { - name string - timestampString string - wantTime time.Time - wantErr string - }{ - { - name: "happy case with a valid timestamp", - timestampString: "132540199410000000", - wantTime: happyPasswordChangeTime, - }, - { - name: "handles error with a string thats not a timestamp", - timestampString: "not timestamp", - wantErr: "couldn't parse as timestamp", - }, - { - name: "handles error with too big of a timestamp", - timestampString: "132540199410000000000", - wantErr: "couldn't parse as timestamp", - }, - { - name: "timestamp of zero", - timestampString: "0", - wantTime: time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC).UTC(), - }, - { - name: "fractional seconds", - timestampString: "132540199410000001", - wantTime: time.Date(2021, time.January, 2, 0, 12, 21, 100, time.UTC).UTC(), - }, - { - name: "max allowable value", - timestampString: "9223372036854775807", // 2^63-1 - wantTime: time.Date(30828, time.September, 14, 2, 48, 5, 477580700, time.UTC).UTC(), - }, - { - name: "just past max allowable value", - timestampString: "9223372036854775808", // 2^63 - wantErr: "couldn't parse as timestamp", - }, - } - - for _, test := range tests { - tt := test - t.Run(tt.name, func(t *testing.T) { - actualTime, err := win32timestampToTime(tt.timestampString) - require.Equal(t, tt.wantTime, actualTime) + err := AttributeUnchangedSinceLogin(attributeName)(tt.entry, provider.StoredRefreshAttributes{AdditionalAttributes: map[string][]string{attributeName: {initialVal}}}) if tt.wantErr != "" { require.Error(t, err) require.Equal(t, tt.wantErr, err.Error())