diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f218a72d893..15a75846c23 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/querylibrary" @@ -211,6 +212,7 @@ type HTTPServer struct { tagService tag.Service oauthTokenService oauthtoken.OAuthTokenService statsService stats.Service + authnService authn.Service } type ServerOptions struct { @@ -253,7 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, - statsService stats.Service, + statsService stats.Service, authnService authn.Service, k8saccess k8saccess.K8SAccess, // required so that the router is registered ) (*HTTPServer, error) { web.Env = cfg.Env @@ -360,6 +362,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi QueryLibraryService: queryLibraryService, oauthTokenService: oauthTokenService, statsService: statsService, + authnService: authnService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/login.go b/pkg/api/login.go index 1ed59213fbe..74d76373081 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -17,6 +17,8 @@ import ( "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/featuremgmt" loginService "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" @@ -167,6 +169,34 @@ func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { } func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { + if hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientForm, &authn.Request{HTTPRequest: c.Req, Resp: c.Resp}) + if err != nil { + tokenErr := &auth.CreateTokenErr{} + if errors.As(err, &tokenErr) { + return response.Error(tokenErr.StatusCode, tokenErr.ExternalErr, tokenErr.InternalErr) + } + return response.Err(err) + } + + cookies.WriteSessionCookie(c, hs.Cfg, identity.SessionToken.UnhashedToken, hs.Cfg.LoginMaxLifetime) + result := map[string]interface{}{ + "message": "Logged in", + } + + if redirectTo := c.GetCookie("redirect_to"); len(redirectTo) > 0 { + if err := hs.ValidateRedirectTo(redirectTo); err == nil { + result["redirectUrl"] = redirectTo + } else { + c.Logger.Info("Ignored invalid redirect_to cookie value.", "url", redirectTo) + } + cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg) + } + + metrics.MApiLoginPost.Inc() + return response.JSON(http.StatusOK, result) + } + cmd := dtos.LoginCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad login data", err) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index ea30addb451..9e09ba3deaa 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,9 +12,6 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -24,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" loginservice "github.com/grafana/grafana/pkg/services/login" @@ -33,6 +31,8 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func fakeSetIndexViewData(t *testing.T) { @@ -324,6 +324,7 @@ func TestLoginPostRedirect(t *testing.T) { HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, AuthTokenService: authtest.NewFakeUserAuthTokenService(), + Features: featuremgmt.WithFeatures(), } hs.Cfg.CookieSecure = true @@ -603,6 +604,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, AuthTokenService: authtest.NewFakeUserAuthTokenService(), + Features: featuremgmt.WithFeatures(), HooksService: hookService, } diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index a6394b77308..5537e7e8998 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -24,6 +24,7 @@ const ( ClientJWT = "auth.client.jwt" ClientRender = "auth.client.render" ClientSession = "auth.client.session" + ClientForm = "auth.client.form" ) const ( diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 9a874c33c41..ae93c60aa65 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -65,18 +65,23 @@ func ProvideService( } var passwordClients []authn.PasswordClient - if !s.cfg.DisableLogin { passwordClients = append(passwordClients, clients.ProvideGrafana(userService)) } - if s.cfg.LDAPEnabled { passwordClients = append(passwordClients, clients.ProvideLDAP(cfg)) } - // only configure basic auth client if it is enabled, and we have at least one password client enabled - if s.cfg.BasicAuthEnabled && len(passwordClients) > 0 { - s.clients[authn.ClientBasic] = clients.ProvideBasic(loginAttempts, passwordClients...) + // if we have password clients configure check if basic auth or form auth is enabled + if len(passwordClients) > 0 { + passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...) + if s.cfg.BasicAuthEnabled { + s.clients[authn.ClientBasic] = clients.ProvideBasic(passwordClient) + } + // FIXME (kalleep): Remove the global variable and stick it into cfg + if !setting.DisableLoginForm { + s.clients[authn.ClientForm] = clients.ProvideForm(passwordClient) + } } if s.cfg.JWTAuthEnabled { @@ -128,6 +133,8 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ return nil, true, err } + // FIXME (kalleep): Handle disabled identities + for _, hook := range s.postAuthHooks { if err := hook(ctx, identity, r); err != nil { s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity) diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index e3fcf608908..4c8c0c529cf 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -2,30 +2,25 @@ package clients import ( "context" - "errors" "strings" "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/loginattempt" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" - "github.com/grafana/grafana/pkg/web" ) var ( errDecodingBasicAuthHeader = errutil.NewBase(errutil.StatusBadRequest, "basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header")) - errBasicAuthCredentials = errutil.NewBase(errutil.StatusUnauthorized, "basic-auth.invalid-credentials", errutil.WithPublicMessage("Invalid username or password")) ) var _ authn.Client = new(Basic) -func ProvideBasic(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Basic { - return &Basic{clients, loginAttempts} +func ProvideBasic(client authn.PasswordClient) *Basic { + return &Basic{client} } type Basic struct { - clients []authn.PasswordClient - loginAttempts loginattempt.Service + client authn.PasswordClient } func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -34,44 +29,10 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, errDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err) } - r.SetMeta(authn.MetaKeyUsername, username) - - ok, err := c.loginAttempts.Validate(ctx, username) - if err != nil { - return nil, err - } - if !ok { - return nil, errBasicAuthCredentials.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") - } - - if len(password) == 0 { - return nil, errBasicAuthCredentials.Errorf("no password provided") - } - - for _, pwClient := range c.clients { - identity, err := pwClient.AuthenticatePassword(ctx, r, username, password) - if err != nil { - if errors.Is(err, errIdentityNotFound) { - // continue to next password client if identity could not be found - continue - } - if errors.Is(err, errInvalidPassword) { - // only add login attempt if identity was found but the provided password was invalid - _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) - } - return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity: %w", err) - } - - return identity, nil - } - - return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity using basic auth") + return c.client.AuthenticatePassword(ctx, r, username, password) } func (c *Basic) Test(ctx context.Context, r *authn.Request) bool { - if len(c.clients) == 0 { - return false - } return looksLikeBasicAuthRequest(r) } diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index 1f61ade0dc7..81b974ee20d 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/stretchr/testify/assert" ) @@ -15,8 +14,7 @@ func TestBasic_Authenticate(t *testing.T) { type TestCase struct { desc string req *authn.Request - blockLogin bool - clients []authn.PasswordClient + client authn.PasswordClient expectedErr error expectedIdentity *authn.Identity } @@ -25,40 +23,19 @@ func TestBasic_Authenticate(t *testing.T) { { desc: "should success when password client return identity", req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, + client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}, expectedIdentity: &authn.Identity{ID: "user:1"}, }, { - desc: "should success when found in second client", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, - expectedIdentity: &authn.Identity{ID: "user:2"}, - }, - { - desc: "should fail for empty password", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}}, - expectedErr: errBasicAuthCredentials, - }, - { - desc: "should if login is blocked by to many attempts", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}}, - blockLogin: true, - expectedErr: errBasicAuthCredentials, - }, - { - desc: "should fail when not found in any clients", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, - expectedErr: errBasicAuthCredentials, + desc: "should fail when basic auth header could not be decoded", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {}}}}, + expectedErr: errDecodingBasicAuthHeader, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideBasic( - loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, - tt.clients..., - ) + c := ProvideBasic(tt.client) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { @@ -74,10 +51,9 @@ func TestBasic_Authenticate(t *testing.T) { func TestBasic_Test(t *testing.T) { type TestCase struct { - desc string - req *authn.Request - noClients bool - expected bool + desc string + req *authn.Request + expected bool } tests := []TestCase{ @@ -92,18 +68,6 @@ func TestBasic_Test(t *testing.T) { }, expected: true, }, - { - desc: "should fail when no password client is configured", - req: &authn.Request{ - HTTPRequest: &http.Request{ - Header: map[string][]string{ - authorizationHeaderName: {encodeBasicAuth("user", "password")}, - }, - }, - }, - noClients: true, - expected: false, - }, { desc: "should fail when no http request is passed", req: &authn.Request{}, @@ -124,10 +88,7 @@ func TestBasic_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideBasic(loginattempttest.FakeLoginAttemptService{}, authntest.FakePasswordClient{}) - if tt.noClients { - c.clients = nil - } + c := ProvideBasic(authntest.FakePasswordClient{}) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } diff --git a/pkg/services/authn/clients/constants.go b/pkg/services/authn/clients/constants.go index d0c2f477f31..0d8734c61fa 100644 --- a/pkg/services/authn/clients/constants.go +++ b/pkg/services/authn/clients/constants.go @@ -10,5 +10,4 @@ const ( var ( errIdentityNotFound = errutil.NewBase(errutil.StatusNotFound, "identity.not-found") - errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "identity.invalid-password", errutil.WithPublicMessage("Invalid password or username")) ) diff --git a/pkg/services/authn/clients/form.go b/pkg/services/authn/clients/form.go new file mode 100644 index 00000000000..f8e4de8bdb0 --- /dev/null +++ b/pkg/services/authn/clients/form.go @@ -0,0 +1,42 @@ +package clients + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" +) + +var ( + errBadForm = errutil.NewBase(errutil.StatusBadRequest, "form-auth.invalid", errutil.WithPublicMessage("bad login data")) +) + +var _ authn.Client = new(Form) + +func ProvideForm(client authn.PasswordClient) *Form { + return &Form{client} +} + +type Form struct { + client authn.PasswordClient +} + +type loginForm struct { + Username string `json:"user" binding:"Required"` + Password string `json:"password" binding:"Required"` +} + +func (f *Form) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + form := loginForm{} + if err := web.Bind(r.HTTPRequest, &form); err != nil { + return nil, errBadForm.Errorf("failed to parse request: %w", err) + } + return f.client.AuthenticatePassword(ctx, r, form.Username, form.Password) +} + +func (f *Form) Test(ctx context.Context, r *authn.Request) bool { + // FIXME: How should we detect this?? + // Maybe create client test interface and not all clients has to implement this?? + return true +} diff --git a/pkg/services/authn/clients/form_test.go b/pkg/services/authn/clients/form_test.go new file mode 100644 index 00000000000..6c3c0b30f9e --- /dev/null +++ b/pkg/services/authn/clients/form_test.go @@ -0,0 +1,48 @@ +package clients + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" +) + +func TestForm_Authenticate(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + expectedErr error + } + + tests := []testCase{ + { + desc: "should success on valid request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"user": "test", "password": "test"}`)), + }}, + }, + { + desc: "should return error for bad request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{}`)), + }}, + expectedErr: errBadForm, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvideForm(&authntest.FakePasswordClient{}) + _, err := c.Authenticate(context.Background(), tt.req) + assert.ErrorIs(t, err, tt.expectedErr) + }) + } +} diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 364be4bb65b..0a4a9bfbf8a 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -28,6 +28,7 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern }) if errors.Is(err, multildap.ErrCouldNotFindUser) { + // FIXME: disable user in grafana if not found return nil, errIdentityNotFound.Errorf("no user found: %w", err) } @@ -35,7 +36,6 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern r.SetMeta(authn.MetaKeyAuthModule, "ldap") if errors.Is(err, multildap.ErrInvalidCredentials) { - // FIXME: disable user in grafana if not found return nil, errInvalidPassword.Errorf("invalid password: %w", err) } diff --git a/pkg/services/authn/clients/password.go b/pkg/services/authn/clients/password.go new file mode 100644 index 00000000000..50e76b4c35f --- /dev/null +++ b/pkg/services/authn/clients/password.go @@ -0,0 +1,67 @@ +package clients + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/loginattempt" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" +) + +var ( + errEmptyPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.empty", errutil.WithPublicMessage("Invalid username or password")) + errPasswordAuthFailed = errutil.NewBase(errutil.StatusBadRequest, "password-auth.failed", errutil.WithPublicMessage("Invalid username or password")) + errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.invalid", errutil.WithPublicMessage("Invalid password or username")) + errLoginAttemptBlocked = errutil.NewBase(errutil.StatusUnauthorized, "login-attempt.blocked", errutil.WithPublicMessage("Invalid username or password")) +) + +var _ authn.PasswordClient = new(Password) + +func ProvidePassword(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Password { + return &Password{loginAttempts, clients} +} + +type Password struct { + loginAttempts loginattempt.Service + clients []authn.PasswordClient +} + +func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + r.SetMeta(authn.MetaKeyUsername, username) + + ok, err := c.loginAttempts.Validate(ctx, username) + if err != nil { + return nil, err + } + if !ok { + return nil, errLoginAttemptBlocked.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") + } + + if len(password) == 0 { + return nil, errEmptyPassword.Errorf("no password provided") + } + + var clientErr error + for _, pwClient := range c.clients { + var identity *authn.Identity + identity, clientErr = pwClient.AuthenticatePassword(ctx, r, username, password) + // for invalid password or if the identity is not found by a client continue to next one + if errors.Is(clientErr, errInvalidPassword) || errors.Is(clientErr, errIdentityNotFound) { + continue + } + + if clientErr != nil { + return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErr) + } + + return identity, nil + } + + if errors.Is(clientErr, errInvalidPassword) { + _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) + } + + return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErr) +} diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go new file mode 100644 index 00000000000..e064265e90a --- /dev/null +++ b/pkg/services/authn/clients/password_test.go @@ -0,0 +1,82 @@ +package clients + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" +) + +func TestPassword_AuthenticatePassword(t *testing.T) { + type TestCase struct { + desc string + username string + password string + req *authn.Request + blockLogin bool + clients []authn.PasswordClient + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []TestCase{ + { + desc: "should success when password client return identity", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, + expectedIdentity: &authn.Identity{ID: "user:1"}, + }, + { + desc: "should success when found in second client", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, + expectedIdentity: &authn.Identity{ID: "user:2"}, + }, + { + desc: "should fail for empty password", + username: "test", + password: "", + req: &authn.Request{}, + expectedErr: errEmptyPassword, + }, + { + desc: "should if login is blocked by to many attempts", + username: "test", + password: "test", + req: &authn.Request{}, + blockLogin: true, + expectedErr: errLoginAttemptBlocked, + }, + { + desc: "should fail when not found in any clients", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, + expectedErr: errPasswordAuthFailed, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tt.clients...) + + identity, err := c.AuthenticatePassword(context.Background(), tt.req, tt.username, tt.password) + if tt.expectedErr != nil { + assert.ErrorIs(t, err, tt.expectedErr) + assert.Nil(t, identity) + } else { + assert.NoError(t, err) + assert.EqualValues(t, *tt.expectedIdentity, *identity) + } + }) + } +}