diff options
Diffstat (limited to 'internal/handlers/handler_firstfactor_test.go')
| -rw-r--r-- | internal/handlers/handler_firstfactor_test.go | 1236 |
1 files changed, 1212 insertions, 24 deletions
diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index 1ab273318..492b27274 100644 --- a/internal/handlers/handler_firstfactor_test.go +++ b/internal/handlers/handler_firstfactor_test.go @@ -1,10 +1,13 @@ package handlers import ( + "database/sql" "fmt" "net/url" "testing" + "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/valyala/fasthttp" @@ -15,6 +18,7 @@ import ( "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/model" + "github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/regulation" ) @@ -36,7 +40,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsNil() { FirstFactorPOST(nil)(s.mock.Ctx) // No body. - AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Failed to parse 1FA request body", "unable to parse body: unexpected end of JSON input") + s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to parse body: unexpected end of JSON input") s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } @@ -47,7 +51,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() { }`) FirstFactorPOST(nil)(s.mock.Ctx) - AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Failed to parse 1FA request body", "unable to validate body: password: non zero value required") + s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to validate body: password: non zero value required") s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } @@ -59,13 +63,13 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() { s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(false, fmt.Errorf("failed")) s.mock.StorageMock. EXPECT(). AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{ - Username: "test", + Username: testValue, Successful: false, Banned: false, Time: s.mock.Clock.Now(), @@ -80,7 +84,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() { }`) FirstFactorPOST(nil)(s.mock.Ctx) - AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Unsuccessful 1FA authentication attempt by user 'test'", "failed") + s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test'", "failed") s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } @@ -93,13 +97,13 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderC s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(false, fmt.Errorf("invalid credentials")) s.mock.StorageMock. EXPECT(). AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{ - Username: "test", + Username: testValue, Successful: false, Banned: false, Time: s.mock.Clock.Now(), @@ -116,6 +120,76 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderC FirstFactorPOST(nil)(s.mock.Ctx) } +func (s *FirstFactorSuite) TestShouldCheckUserNotBanned() { + s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2}, s.mock.StorageMock, &s.mock.Clock) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "keepMeLoggedIn": true + }`) + + gomock.InOrder( + s.mock.UserProviderMock.EXPECT().GetDetails(testValue).Return(&authentication.UserDetails{Username: testValue}, nil), + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, nil), + + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(false, fmt.Errorf("invalid credentials")), + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: false, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + }))) + FirstFactorPOST(nil)(s.mock.Ctx) +} + +func (s *FirstFactorSuite) TestShouldCheckBannedUser() { + s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2, FindTime: time.Hour, BanTime: time.Hour}, s.mock.StorageMock, &s.mock.Clock) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "keepMeLoggedIn": true + }`) + + gomock.InOrder( + s.mock.UserProviderMock.EXPECT().GetDetails(testValue).Return(&authentication.UserDetails{Username: testValue}, nil), + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()). + Return([]model.AuthenticationAttempt{ + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + }, nil), + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: true, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + }))) + + FirstFactorPOST(nil)(s.mock.Ctx) + + s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test' and they are banned until 2013-02-03 00:59:59 +0000 UTC", "") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() { s.mock.UserProviderMock. EXPECT(). @@ -124,13 +198,13 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCrede s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(false, nil) s.mock.StorageMock. EXPECT(). AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{ - Username: "test", + Username: testValue, Successful: false, Banned: false, Time: s.mock.Clock.Now(), @@ -150,7 +224,7 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCrede func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() { s.mock.UserProviderMock. EXPECT(). - GetDetails(gomock.Eq("test")). + GetDetails(gomock.Eq(testValue)). Return(nil, fmt.Errorf("failed")) s.mock.Ctx.Request.SetBodyString(`{ @@ -158,6 +232,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() { "password": "hello", "keepMeLoggedIn": true }`) + FirstFactorPOST(nil)(s.mock.Ctx) AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Error occurred getting details for user with username input 'test' which usually indicates they do not exist", "failed") @@ -172,7 +247,7 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() { s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(true, nil) s.mock.StorageMock. @@ -187,21 +262,21 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() { }`) FirstFactorPOST(nil)(s.mock.Ctx) - AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Unable to mark 1FA authentication attempt by user 'test'", "failed") + s.mock.AssertLastLogMessage(s.T(), "Unable to mark 1FA authentication attempt by user 'test'", "failed") s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() { s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(true, nil) s.mock.UserProviderMock. EXPECT(). - GetDetails(gomock.Eq("test")). + GetDetails(gomock.Eq(testValue)). Return(&authentication.UserDetails{ - Username: "test", + Username: testValue, Emails: []string{"test@example.com"}, Groups: []string{"dev", "admins"}, }, nil) @@ -225,7 +300,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() { userSession, err := s.mock.Ctx.GetSession() s.Assert().NoError(err) - assert.Equal(s.T(), "test", userSession.Username) + assert.Equal(s.T(), testValue, userSession.Username) assert.Equal(s.T(), true, userSession.KeepMeLoggedIn) assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel) assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails) @@ -235,14 +310,14 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() { func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() { s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(true, nil) s.mock.UserProviderMock. EXPECT(). - GetDetails(gomock.Eq("test")). + GetDetails(gomock.Eq(testValue)). Return(&authentication.UserDetails{ - Username: "test", + Username: testValue, Emails: []string{"test@example.com"}, Groups: []string{"dev", "admins"}, }, nil) @@ -267,7 +342,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() { userSession, err := s.mock.Ctx.GetSession() s.Assert().NoError(err) - assert.Equal(s.T(), "test", userSession.Username) + assert.Equal(s.T(), testValue, userSession.Username) assert.Equal(s.T(), false, userSession.KeepMeLoggedIn) assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel) assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails) @@ -319,7 +394,7 @@ func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSess s.mock.UserProviderMock. EXPECT(). - GetDetails(gomock.Eq("test")). + GetDetails(gomock.Eq(testValue)). Return(&authentication.UserDetails{ // This is the name in authentication backend, in some setups the binding is // case insensitive but the user ID in session must match the user in LDAP @@ -376,14 +451,14 @@ func (s *FirstFactorRedirectionSuite) SetupTest() { s.mock.UserProviderMock. EXPECT(). - CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). Return(true, nil) s.mock.UserProviderMock. EXPECT(). - GetDetails(gomock.Eq("test")). + GetDetails(gomock.Eq(testValue)). Return(&authentication.UserDetails{ - Username: "test", + Username: testValue, Emails: []string{"test@example.com"}, Groups: []string{"dev", "admins"}, }, nil) @@ -505,7 +580,1120 @@ func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvi s.mock.Assert200OK(s.T(), nil) } +func (s *FirstFactorRedirectionSuite) TestShouldReplyWhenBadTargetURL() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "targetURL": "#https://23kjnm412jk3" + }`) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyTwoFactorOK() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "targetURL": "https://two-factor.example.com" + }`) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), nil) +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyTwoTwoFactorUnsafe() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "targetURL": "https://unsafe-domain.com" + }`) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), nil) +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyTwoTwoFactorSafe() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "targetURL": "https://test.example.com" + }`) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://test.example.com"}) +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectCantParseUUID() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "aaaaaaaaaaaaaaaaaaaaaaaaaaa-9107-4067-8d31-407ca59eb69c" + }`) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to parse consent session challenge id 'aaaaaaaaaaaaaaaaaaaaaaaaaaa-9107-4067-8d31-407ca59eb69c': invalid UUID length: 55", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectCantGetConsentSession() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(nil, fmt.Errorf("failed to obtain")), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to load consent session by challenge id 'd1ba0ad8-9107-4067-8d31-407ca59eb69c': failed to obtain", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectConsentSessionAlreadyResponded() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{RespondedAt: sql.NullTime{Valid: true, Time: time.Now().Add(-time.Minute)}}, nil), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "consent has already been responded to 'd1ba0ad8-9107-4067-8d31-407ca59eb69c'", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectCantGetClient() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(&schema.Configuration{IdentityProviders: schema.IdentityProviders{OIDC: &schema.IdentityProvidersOpenIDConnect{}}}, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to get client for client with id 'd1ba0ad8-9107-4067-8d31-407ca59eb69c' with consent challenge id '00000000-0000-0000-0000-000000000000': invalid_client", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNoOpaqueID() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, fmt.Errorf("bad identifier")), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to determine consent subject for client with id 'abc' with consent challenge id '00000000-0000-0000-0000-000000000000': bad identifier", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNoOpaqueIDCreateError() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, nil), + s.mock.StorageMock.EXPECT(). + SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(fmt.Errorf("oops")), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to determine consent subject for client with id 'abc' with consent challenge id '00000000-0000-0000-0000-000000000000': oops", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNoOpaqueIDCreateSaveConsentError() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, nil), + s.mock.StorageMock.EXPECT(). + SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + s.mock.StorageMock.EXPECT(). + SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(fmt.Errorf("bad id")), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to update consent subject for client with id 'abc' with consent challenge id '00000000-0000-0000-0000-000000000000': bad id", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectFormRequiresLogin() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + form := url.Values{ + "max_age": []string{"0"}, + } + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: form.Encode()}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, nil), + s.mock.StorageMock.EXPECT(). + SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + s.mock.StorageMock.EXPECT(). + SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "http://example.com/consent/openid/login?workflow=openid_connect&workflow_id=d1ba0ad8-9107-4067-8d31-407ca59eb69c"}) +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectFormRequiresLoginBadForm() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: "1238y12978y189gb128g1287g12807g128702g38172%1"}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, nil), + s.mock.StorageMock.EXPECT(). + SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + s.mock.StorageMock.EXPECT(). + SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.AssertLastLogMessage(s.T(), "unable to get authorization form values from consent session with challenge id '00000000-0000-0000-0000-000000000000': invalid URL escape \"%1\"", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNeeds2FA() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + AuthorizationPolicy: "two_factor", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: ""}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, nil), + s.mock.StorageMock.EXPECT(). + SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + s.mock.StorageMock.EXPECT(). + SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), nil) + s.mock.AssertLastLogMessage(s.T(), "OpenID Connect client 'abc' requires 2FA, cannot be redirected yet", "") +} + +func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNeeds1FA() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"two-factor.example.com"}, + Policy: "two_factor", + }, + }, + }}) + + config := &schema.Configuration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc", + AuthorizationPolicy: "one_factor", + }, + }, + }, + }, + } + + s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "requestMethod": "GET", + "keepMeLoggedIn": false, + "workflow": "openid_connect", + "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c" + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))). + Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: "grant_type=authorization_code"}, nil), + s.mock.StorageMock.EXPECT(). + LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")). + Return(nil, nil), + s.mock.StorageMock.EXPECT(). + SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + s.mock.StorageMock.EXPECT(). + SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()). + Return(nil), + ) + + FirstFactorPOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "http://example.com/api/oidc/authorization?consent_id=d1ba0ad8-9107-4067-8d31-407ca59eb69c&grant_type=authorization_code"}) +} + +type FirstFactorReauthenticateSuite struct { + suite.Suite + + mock *mocks.MockAutheliaCtx +} + +func (s *FirstFactorReauthenticateSuite) SetupTest() { + s.mock = mocks.NewMockAutheliaCtx(s.T()) + + session, err := s.mock.Ctx.GetSession() + + s.Require().NoError(err) + + session.Username = testValue + session.AuthenticationLevel = authentication.OneFactor + + s.Require().NoError(s.mock.Ctx.SaveSession(session)) +} + +func (s *FirstFactorReauthenticateSuite) TearDownTest() { + s.mock.Close() +} + +func (s *FirstFactorReauthenticateSuite) TestShouldFailIfBodyIsNil() { + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + // No body. + s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to parse body: unexpected end of JSON input") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorReauthenticateSuite) TestShouldFailIfBodyIsInBadFormat() { + // Missing password. + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test" + }`) + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to validate body: password: non zero value required") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorReauthenticateSuite) TestShouldFailIfUserProviderCheckPasswordFail() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(false, fmt.Errorf("failed")) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: false, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + })) + + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test'", "failed") + + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorReauthenticateSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderCheckPasswordError() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(false, fmt.Errorf("invalid credentials")) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: false, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + })) + + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) +} + +func (s *FirstFactorReauthenticateSuite) TestShouldCheckUserNotBanned() { + s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2}, s.mock.StorageMock, &s.mock.Clock) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "keepMeLoggedIn": true + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, nil), + + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(false, fmt.Errorf("invalid credentials")), + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: false, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + }))) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) +} + +func (s *FirstFactorReauthenticateSuite) TestShouldCheckBannedUser() { + s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2, FindTime: time.Hour, BanTime: time.Hour}, s.mock.StorageMock, &s.mock.Clock) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "keepMeLoggedIn": true + }`) + + gomock.InOrder( + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()). + Return([]model.AuthenticationAttempt{ + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)}, + }, nil), + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: true, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + }))) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test' and they are banned until 2013-02-03 00:59:59 +0000 UTC", "") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorReauthenticateSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(false, nil) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{ + Username: testValue, + Successful: false, + Banned: false, + Time: s.mock.Clock.Now(), + Type: regulation.AuthType1FA, + RemoteIP: model.NewNullIPFromString("0.0.0.0"), + })) + + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) +} + +func (s *FirstFactorReauthenticateSuite) TestShouldFailIfUserProviderGetDetailsFail() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(true, nil) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). + Return(nil) + + s.mock.UserProviderMock. + EXPECT(). + GetDetails(gomock.Eq(testValue)). + Return(nil, fmt.Errorf("failed")) + + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + s.mock.AssertLastLogMessage(s.T(), "Could not obtain profile details during 1FA authentication for user 'test'", "failed") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorReauthenticateSuite) TestShouldFailIfAuthenticationMarkFail() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(true, nil) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). + Return(fmt.Errorf("failed")) + + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + s.mock.AssertLastLogMessage(s.T(), "Unable to mark 1FA authentication attempt by user 'test'", "failed") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") +} + +func (s *FirstFactorReauthenticateSuite) TestShouldSaveUsernameFromAuthenticationBackendInSession() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(true, nil) + + s.mock.UserProviderMock. + EXPECT(). + GetDetails(gomock.Eq(testValue)). + Return(&authentication.UserDetails{ + // This is the name in authentication backend, in some setups the binding is + // case insensitive but the user ID in session must match the user in LDAP + // for the other modules of Authelia to be coherent. + Username: "Test", + Emails: []string{"test@example.com"}, + Groups: []string{"dev", "admins"}, + }, nil) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). + Return(nil) + + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + // Respond with 200. + assert.Equal(s.T(), fasthttp.StatusOK, s.mock.Ctx.Response.StatusCode()) + assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body()) + + userSession, err := s.mock.Ctx.GetSession() + s.Assert().NoError(err) + + assert.Equal(s.T(), "Test", userSession.Username) + assert.Equal(s.T(), false, userSession.KeepMeLoggedIn) + assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel) + assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails) + assert.Equal(s.T(), []string{"dev", "admins"}, userSession.Groups) +} + +type FirstFactorReauthenticateRedirectionSuite struct { + suite.Suite + + mock *mocks.MockAutheliaCtx +} + +func (s *FirstFactorReauthenticateRedirectionSuite) SetupTest() { + s.mock = mocks.NewMockAutheliaCtx(s.T()) + + session, err := s.mock.Ctx.GetSession() + + s.Require().NoError(err) + + session.Username = testValue + session.AuthenticationLevel = authentication.OneFactor + + s.Require().NoError(s.mock.Ctx.SaveSession(session)) + + s.mock.Ctx.Configuration.Session.Cookies[0].DefaultRedirectionURL = &url.URL{Scheme: "https", Host: "default.local"} + s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass + s.mock.Ctx.Configuration.AccessControl.Rules = []schema.AccessControlRule{ + { + Domains: []string{"default.local"}, + Policy: "one_factor", + }, + } + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration) + + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")). + Return(true, nil) + + s.mock.UserProviderMock. + EXPECT(). + GetDetails(gomock.Eq(testValue)). + Return(&authentication.UserDetails{ + Username: testValue, + Emails: []string{"test@example.com"}, + Groups: []string{"dev", "admins"}, + }, nil) + + s.mock.StorageMock. + EXPECT(). + AppendAuthenticationLog(s.mock.Ctx, gomock.Any()). + Return(nil) +} + +func (s *FirstFactorReauthenticateRedirectionSuite) TearDownTest() { + s.mock.Close() +} + +// When: +// +// 1/ the target url is unknown +// 2/ two_factor is disabled (no policy is set to two_factor) +// 3/ default_redirect_url is provided +// +// Then: +// +// the user should be redirected to the default url. +func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldRedirectToDefaultURLWhenNoTargetURLProvidedAndTwoFactorDisabled() { + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"}) +} + +// When: +// +// 1/ the target url is unsafe +// 2/ two_factor is disabled (no policy is set to two_factor) +// 3/ default_redirect_url is provided +// +// Then: +// +// the user should be redirected to the default url. +func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldRedirectToDefaultURLWhenURLIsUnsafeAndTwoFactorDisabled() { + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello", + "targetURL": "http://notsafe.local" + }`) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"}) +} + +// When: +// +// 1/ two_factor is enabled (default policy) +// +// Then: +// +// the user should receive 200 without redirection URL. +func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldReply200WhenNoTargetURLProvidedAndTwoFactorEnabled() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "two_factor", + }, + }) + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), nil) +} + +// When: +// +// 1/ two_factor is enabled (some rule) +// +// Then: +// +// the user should receive 200 without redirection URL. +func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvidedAndTwoFactorEnabled() { + s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ + AccessControl: schema.AccessControl{ + DefaultPolicy: "one_factor", + Rules: []schema.AccessControlRule{ + { + Domains: []string{"test.example.com"}, + Policy: "one_factor", + }, + { + Domains: []string{"example.com"}, + Policy: "two_factor", + }, + }, + }}) + s.mock.Ctx.Request.SetBodyString(`{ + "password": "hello" + }`) + + FirstFactorReauthenticatePOST(nil)(s.mock.Ctx) + + // Respond with 200. + s.mock.Assert200OK(s.T(), nil) +} + func TestFirstFactorSuite(t *testing.T) { suite.Run(t, new(FirstFactorSuite)) suite.Run(t, new(FirstFactorRedirectionSuite)) } + +func TestFirstFactorReauthenticateSuite(t *testing.T) { + suite.Run(t, new(FirstFactorReauthenticateSuite)) + suite.Run(t, new(FirstFactorReauthenticateRedirectionSuite)) +} + +const ( + testValue = "test" +) |
