Active Directory checks whether password has changed recently during

upstream refresh

Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
Margo Crawford
2021-12-09 16:16:35 -08:00
parent 8db0203839
commit da9b4620b3
10 changed files with 612 additions and 96 deletions
@@ -317,6 +317,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{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
}
if spec.GroupSearch.Attributes.GroupName == "" {
@@ -221,6 +221,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
}
// Make a copy with targeted changes.
@@ -537,6 +538,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -593,6 +595,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: "sAMAccountName",
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -652,6 +655,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -711,6 +715,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
@@ -769,6 +774,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -898,6 +904,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1022,6 +1029,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1073,6 +1081,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1273,6 +1282,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},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1325,6 +1335,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1381,6 +1392,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1431,6 +1443,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1627,6 +1640,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
GroupNameAttribute: testGroupNameAttrName,
},
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin},
},
},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
@@ -1753,6 +1767,16 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer())
}
expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks
actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks
copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks))
for k, v := range expectedRefreshAttributeChecks {
require.NotNil(t, actualRefreshAttributeChecks[k])
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualRefreshAttributeChecks[k]).Pointer())
}
require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig)
}
@@ -7,6 +7,7 @@ import (
"context"
"net/url"
"sync"
"time"
"golang.org/x/oauth2"
"k8s.io/apimachinery/pkg/types"
@@ -93,7 +94,14 @@ type UpstreamLDAPIdentityProviderI interface {
authenticators.UserAuthenticator
// PerformRefresh performs a refresh against the upstream LDAP identity provider
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) error
}
type StoredRefreshAttributes struct {
Username string
Subject string
DN string
AuthTime time.Time
}
type DynamicUpstreamIDPProvider interface {
+20 -22
View File
@@ -75,11 +75,6 @@ func NewHandler(
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
session := accessRequest.GetSession().(*psession.PinnipedSession)
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
if err != nil {
return err
}
downstreamSubject := session.Fosite.Claims.Subject
customSessionData := session.Custom
if customSessionData == nil {
@@ -95,9 +90,9 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
case psession.ProviderTypeOIDC:
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
case psession.ProviderTypeLDAP:
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
return upstreamLDAPRefresh(ctx, providerCache, session)
case psession.ProviderTypeActiveDirectory:
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
return upstreamLDAPRefresh(ctx, providerCache, session)
default:
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
@@ -169,7 +164,15 @@ func findOIDCProviderByNameAndValidateUID(
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
}
func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister, username string, subject string) error {
func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession) error {
username, err := getDownstreamUsernameFromPinnipedSession(session)
if err != nil {
return err
}
subject := session.Fosite.Claims.Subject
s := session.Custom
// if you have neither a valid ldap session config nor a valid active directory session config
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
@@ -182,8 +185,16 @@ func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, pro
if err != nil {
return err
}
if session.IDTokenClaims().AuthTime.IsZero() {
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
// run PerformRefresh
err = p.PerformRefresh(ctx, dn, username, subject)
err = p.PerformRefresh(ctx, provider.StoredRefreshAttributes{
Username: username,
Subject: subject,
DN: dn,
AuthTime: session.IDTokenClaims().AuthTime,
})
if err != nil {
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
@@ -235,16 +246,3 @@ func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession)
}
return downstreamUsername, nil
}
func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession) (string, error) {
extra := session.Fosite.Claims.Extra
if extra == nil {
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
downstreamUsernameInterface := extra["username"]
if downstreamUsernameInterface == nil {
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
downstreamUsername := downstreamUsernameInterface.(string)
return downstreamUsername, nil
}
+41 -2
View File
@@ -2181,6 +2181,45 @@ func TestRefreshGrant(t *testing.T) {
},
},
},
{
name: "auth time is the zero value", // time.Times can never be nil, but it is possible that it would be the zero value which would mean something's wrong
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
require.NoError(t, err)
session := firstRequester.GetSession().(*psession.PinnipedSession)
fositeSessionClaims := session.Fosite.IDTokenClaims()
fositeSessionClaims.AuthTime = time.Time{}
session.Fosite.Claims = fositeSessionClaims
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
require.NoError(t, err)
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
require.NoError(t, err)
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "when the ldap provider in the session storage is found but has the wrong resource UID during the refresh request",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
@@ -2201,7 +2240,7 @@ func TestRefreshGrant(t *testing.T) {
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Provider 'some-ldap-idp' of type 'ldap' from upstream session data has changed its resource UID since authentication."
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
}
`),
},
@@ -2227,7 +2266,7 @@ func TestRefreshGrant(t *testing.T) {
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Provider 'some-ad-idp' of type 'activedirectory' from upstream session data has changed its resource UID since authentication."
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
}
`),
},
@@ -111,16 +111,16 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
return u.URL
}
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
if u.performRefreshArgs == nil {
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
}
u.performRefreshCallCount++
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
Ctx: ctx,
DN: userDN,
ExpectedUsername: expectedUsername,
ExpectedSubject: expectedSubject,
DN: storedRefreshAttributes.DN,
ExpectedUsername: storedRefreshAttributes.Username,
ExpectedSubject: storedRefreshAttributes.Subject,
})
if u.PerformRefreshErr != nil {
return u.PerformRefreshErr
+66 -7
View File
@@ -15,6 +15,7 @@ import (
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -40,6 +41,7 @@ const (
defaultLDAPPort = uint16(389)
defaultLDAPSPort = uint16(636)
sAMAccountNameAttribute = "sAMAccountName"
pwdLastSetAttribute = "pwdLastSet"
)
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
@@ -119,6 +121,9 @@ type ProviderConfig struct {
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
// name when it comes out of LDAP.
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
// RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected.
RefreshAttributeChecks map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error
}
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
@@ -170,9 +175,11 @@ func (p *Provider) GetConfig() ProviderConfig {
return p.c
}
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
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
userDN := storedRefreshAttributes.DN
searchResult, err := p.performRefresh(ctx, userDN)
if err != nil {
p.traceRefreshFailure(t, err)
@@ -196,9 +203,9 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
if err != nil {
return err
}
if newUsername != expectedUsername {
if newUsername != storedRefreshAttributes.Username {
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
userDN, expectedUsername, newUsername,
userDN, storedRefreshAttributes.Username, newUsername,
)
}
@@ -207,10 +214,15 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
return err
}
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
if newSubject != expectedSubject {
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, expectedSubject, newSubject)
if newSubject != storedRefreshAttributes.Subject {
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, storedRefreshAttributes.Subject, newSubject)
}
for attribute, validateFunc := range p.c.RefreshAttributeChecks {
err = validateFunc(userEntry, storedRefreshAttributes)
if err != nil {
return fmt.Errorf(`validation for attribute "%s" failed during upstream refresh: %w`, attribute, err)
}
}
// we checked that the user still exists and their information is the same, so just return.
return nil
}
@@ -652,7 +664,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.userSearchRequestedAttributes(),
Attributes: p.refreshAttributes(),
Controls: nil, // this could be used to enable paging, but we're already limiting the result max size
}
}
@@ -679,6 +691,14 @@ 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 {
@@ -836,3 +856,42 @@ 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))
}
// 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 = 116444736000000000 // The unix base time (January 1, 1970 UTC) as 100 ns since Win32 epoch (1601-01-01)
const hundredNsToSecFactor = 10000000
win32Time, err := strconv.ParseUint(win32timestamp, 10, 64)
if err != nil {
return nil, fmt.Errorf("couldn't parse as timestamp")
}
unixsec := int64(win32Time-unixTimeBaseAsWin) / hundredNsToSecFactor
unixns := int64(win32Time % hundredNsToSecFactor)
convertedTime := time.Unix(unixsec, unixns).UTC()
return &convertedTime, nil
}
+178 -2
View File
@@ -16,6 +16,8 @@ import (
"testing"
"time"
provider2 "go.pinniped.dev/internal/oidc/provider"
"github.com/go-ldap/ldap/v3"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
@@ -1217,6 +1219,7 @@ func TestEndUserAuthentication(t *testing.T) {
}
func TestUpstreamRefresh(t *testing.T) {
pwdLastSetAttribute := "pwdLastSet"
expectedUserSearch := &ldap.SearchRequest{
BaseDN: testUserSearchResultDNValue,
Scope: ldap.ScopeBaseObject,
@@ -1225,7 +1228,7 @@ func TestUpstreamRefresh(t *testing.T) {
TimeLimit: 90,
TypesOnly: false,
Filter: "(objectClass=*)",
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute},
Controls: nil, // don't need paging because we set the SizeLimit so small
}
@@ -1242,6 +1245,10 @@ func TestUpstreamRefresh(t *testing.T) {
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
},
},
},
},
@@ -1259,6 +1266,7 @@ func TestUpstreamRefresh(t *testing.T) {
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider2.StoredRefreshAttributes) error{pwdLastSetAttribute: PwdUnchangedSinceLogin},
}
tests := []struct {
@@ -1512,6 +1520,36 @@ 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",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132803468800000000"},
},
},
},
},
}, 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",
},
}
for _, tt := range tests {
@@ -1538,7 +1576,13 @@ func TestUpstreamRefresh(t *testing.T) {
provider := New(*providerConfig)
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
err := provider.PerformRefresh(context.Background(), testUserSearchResultDNValue, testUserSearchResultUsernameAttributeValue, subject)
authTime := time.Date(2021, time.November, 1, 23, 43, 19, 0, time.UTC)
err := provider.PerformRefresh(context.Background(), provider2.StoredRefreshAttributes{
Username: testUserSearchResultUsernameAttributeValue,
Subject: subject,
DN: testUserSearchResultDNValue,
AuthTime: authTime,
})
if tt.wantErr != "" {
require.Error(t, err)
require.Equal(t, tt.wantErr, err.Error())
@@ -1916,3 +1960,135 @@ 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
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,
entry: &ldap.Entry{
DN: "some-dn",
Attributes: []*ldap.EntryAttribute{
{
Name: "pwdLastSet",
Values: []string{pwdResetTimeBeforeAuthTime},
},
},
},
},
{
name: "password has been reset since login",
authTime: &authTimeParsed,
entry: &ldap.Entry{
DN: "some-dn",
Attributes: []*ldap.EntryAttribute{
{
Name: "pwdLastSet",
Values: []string{pwdResetTimeAfterAuthTime},
},
},
},
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",
},
{
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,
entry: &ldap.Entry{
DN: "some-dn",
Attributes: []*ldap.EntryAttribute{},
},
wantErr: "expected to find 1 value for pwdLastSet attribute, but found 0",
},
{
name: "too many values for pwdLastSet attribute",
authTime: &authTimeParsed,
entry: &ldap.Entry{
DN: "some-dn",
Attributes: []*ldap.EntryAttribute{
{
Name: "pwdLastSet",
Values: []string{"val1", "val2"},
},
},
},
wantErr: "expected to find 1 value for pwdLastSet attribute, but found 2",
},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
err := PwdUnchangedSinceLogin(tt.entry, provider2.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",
},
}
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)
if tt.wantErr != "" {
require.Error(t, err)
require.Equal(t, tt.wantErr, err.Error())
} else {
require.NoError(t, err)
}
})
}
}