From aeecc10535c21ed65025a6d0aa94132737f58540 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 13:06:19 +0800 Subject: [PATCH 01/13] feat: initial commit --- Dockerfile | 2 +- internal/api/mfa.go | 31 ++++++++++++++++++++++++++++++- internal/api/token.go | 30 +++++++++++++++++++++++++++++- internal/conf/configuration.go | 3 ++- internal/hooks/auth_hooks.go | 25 ++++++++++++++++++++++--- 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 88ae5abf25..4fbe67649b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine as build +FROM golang:1.21.4-alpine as build ENV GO111MODULE=on ENV CGO_ENABLED=0 ENV GOOS=linux diff --git a/internal/api/mfa.go b/internal/api/mfa.go index 3f253f0e15..78cfc750a4 100644 --- a/internal/api/mfa.go +++ b/internal/api/mfa.go @@ -231,6 +231,35 @@ func (a *API) invokeHook(ctx context.Context, input any, output hooks.HookOutput } return nil + case hooks.PasswordVerificationAttemptInput: + // TODO: Refactor overlapping logic + payload, err := json.Marshal(&input) + if err != nil { + panic(err) + } + + if err := a.db.Transaction(func(tx *storage.Connection) error { + // We rely on Postgres timeouts to ensure the function doesn't overrun + timeoutQuery := tx.RawQuery(fmt.Sprintf("set local statement_timeout TO '%d';", hooks.DefaultTimeout)) + if terr := timeoutQuery.Exec(); terr != nil { + return terr + } + query := tx.RawQuery(fmt.Sprintf("SELECT %s(?)", a.config.Hook.MFAVerificationAttempt.HookName), payload) + terr := query.First(&response) + if terr != nil { + return terr + } + return nil + }); err != nil { + return err + } + if err = json.Unmarshal(response, &output); err != nil { + return err + } + if output.IsError() { + return &output.(*hooks.PasswordVerificationAttemptOutput).HookError + } + default: panic("invalid extensibility point") } @@ -298,7 +327,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return errors.New(err.Error()) } - if output.Decision == hooks.MFAHookRejection { + if output.Decision == hooks.HookRejection { if err := models.Logout(a.db, user.ID); err != nil { return err } diff --git a/internal/api/token.go b/internal/api/token.go index f4d5584d25..6fca02c1d8 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -13,6 +13,7 @@ import ( "github.com/golang-jwt/jwt" "github.com/supabase/gotrue/internal/conf" + "github.com/supabase/gotrue/internal/hooks" "github.com/supabase/gotrue/internal/metering" "github.com/supabase/gotrue/internal/models" "github.com/supabase/gotrue/internal/storage" @@ -140,8 +141,35 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri return internalServerError("Database error querying schema").WithInternalError(err) } - if user.IsBanned() || !user.Authenticate(ctx, params.Password) { + if user.IsBanned() { return oauthError("invalid_grant", InvalidLoginMessage) + + } + isValidPassword := user.Authenticate(ctx, params.Password) + if !isValidPassword { + return oauthError("invalid_grant", InvalidLoginMessage) + } + if config.Hook.PasswordVerificationAttempt.Enabled { + + input := hooks.PasswordVerificationAttemptInput{ + UserID: user.ID, + Valid: isValidPassword, + } + output := &hooks.PasswordVerificationAttemptOutput{} + err := a.invokeHook(ctx, input, output) + if err != nil { + return errors.New(err.Error()) + } + + if output.Decision == hooks.HookRejection { + if err := models.Logout(a.db, user.ID); err != nil { + return err + } + if output.Message == "" { + output.Message = hooks.DefaultPasswordHookRejectionMessage + } + return forbiddenError(output.Message) + } } if params.Email != "" && !user.IsConfirmed() { diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 6006e69926..93749a8bae 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -441,7 +441,8 @@ type WebhookConfig struct { // Moving away from the existing HookConfig so we can get a fresh start. type HookConfiguration struct { - MFAVerificationAttempt ExtensibilityPointConfiguration `json:"mfa_verification_attempt" split_words:"true"` + MFAVerificationAttempt ExtensibilityPointConfiguration `json:"mfa_verification_attempt" split_words:"true"` + PasswordVerificationAttempt ExtensibilityPointConfiguration `json:"password_verification_attempt" split_words:"true"` } type ExtensibilityPointConfiguration struct { diff --git a/internal/hooks/auth_hooks.go b/internal/hooks/auth_hooks.go index 0c5c87b819..d66b82436e 100644 --- a/internal/hooks/auth_hooks.go +++ b/internal/hooks/auth_hooks.go @@ -18,8 +18,7 @@ const ( // Hook Names const ( - MFAHookRejection = "reject" - MFAHookContinue = "continue" + HookRejection = "reject" ) type HookOutput interface { @@ -39,6 +38,17 @@ type MFAVerificationAttemptOutput struct { HookError AuthHookError `json:"hook_error" split_words:"true"` } +type PasswordVerificationAttemptInput struct { + UserID uuid.UUID `json:"user_id"` + Valid bool `json:"valid"` +} + +type PasswordVerificationAttemptOutput struct { + Decision string `json:"decision"` + Message string `json:"message"` + HookError AuthHookError `json:"hook_error" split_words:"true"` +} + type AuthHookError struct { Code string `json:"code"` Message string `json:"msg"` @@ -50,7 +60,8 @@ func (a *AuthHookError) Error() string { } const ( - DefaultMFAHookRejectionMessage = "Further MFA verification attempts will be rejected." + DefaultMFAHookRejectionMessage = "Further MFA verification attempts will be rejected." + DefaultPasswordHookRejectionMessage = "Further password verification attempts will be rejected." ) func HookError(message string, args ...interface{}) *AuthHookError { @@ -66,3 +77,11 @@ func (mf *MFAVerificationAttemptOutput) IsError() bool { func (mf *MFAVerificationAttemptOutput) Error() string { return mf.HookError.Message } + +func (p *PasswordVerificationAttemptOutput) IsError() bool { + return p.HookError.Message != "" +} + +func (p *PasswordVerificationAttemptOutput) Error() string { + return p.HookError.Message +} From f16144f8563fca7d51b40e48029e90dd4ece6020 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 13:20:31 +0800 Subject: [PATCH 02/13] fix: push check down --- internal/api/token.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/api/token.go b/internal/api/token.go index 6fca02c1d8..cfd18dac71 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -146,9 +146,6 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri } isValidPassword := user.Authenticate(ctx, params.Password) - if !isValidPassword { - return oauthError("invalid_grant", InvalidLoginMessage) - } if config.Hook.PasswordVerificationAttempt.Enabled { input := hooks.PasswordVerificationAttemptInput{ @@ -171,6 +168,9 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri return forbiddenError(output.Message) } } + if !isValidPassword { + return oauthError("invalid_grant", InvalidLoginMessage) + } if params.Email != "" && !user.IsConfirmed() { return oauthError("invalid_grant", "Email not confirmed") From 869d5fedd9559cd1000f629d41b48c9b1d915c8c Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 21:07:19 +0800 Subject: [PATCH 03/13] feat: revert to original 1.21 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4fbe67649b..88ae5abf25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.4-alpine as build +FROM golang:1.21-alpine as build ENV GO111MODULE=on ENV CGO_ENABLED=0 ENV GOOS=linux From d90cabeba1408937499ad022b130a269518e1033 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 21:16:48 +0800 Subject: [PATCH 04/13] feat: add corresponding input handling --- internal/api/mfa.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/internal/api/mfa.go b/internal/api/mfa.go index 16be5e0823..221418863e 100644 --- a/internal/api/mfa.go +++ b/internal/api/mfa.go @@ -264,6 +264,33 @@ func (a *API) invokeHook(ctx context.Context, input, output any) error { } return nil + case *hooks.PasswordVerificationAttemptInput: + hookOutput, ok := output.(*hooks.PasswordVerificationAttemptOutput) + if !ok { + panic("output should be *hooks.MFAVerificationAttemptOutput") + } + + if _, err := a.runHook(ctx, config.Hook.PasswordVerificationAttempt.HookName, input, output); err != nil { + return internalServerError("Error invoking MFA verification hook.").WithInternalError(err) + } + + if hookOutput.IsError() { + httpCode := hookOutput.HookError.HTTPCode + + if httpCode == 0 { + httpCode = http.StatusInternalServerError + } + + httpError := &HTTPError{ + Code: httpCode, + Message: hookOutput.HookError.Message, + } + + return httpError.WithInternalError(&hookOutput.HookError) + } + + return nil + default: panic("unknown hook input type") } From 9dcfdfc4c4a84b451c21a69647e686c22cfa83cc Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 21:30:54 +0800 Subject: [PATCH 05/13] feat: add some test cases --- internal/api/mfa.go | 1 - internal/api/token_test.go | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/api/mfa.go b/internal/api/mfa.go index 221418863e..4474fdab49 100644 --- a/internal/api/mfa.go +++ b/internal/api/mfa.go @@ -236,7 +236,6 @@ func (a *API) runHook(ctx context.Context, name string, input, output any) ([]by func (a *API) invokeHook(ctx context.Context, input, output any) error { config := a.config - switch input.(type) { case *hooks.MFAVerificationAttemptInput: hookOutput, ok := output.(*hooks.MFAVerificationAttemptOutput) diff --git a/internal/api/token_test.go b/internal/api/token_test.go index d2c19b50c3..dbdc84d61a 100644 --- a/internal/api/token_test.go +++ b/internal/api/token_test.go @@ -588,3 +588,65 @@ func (ts *TokenTestSuite) TestMagicLinkPKCESignIn() { require.NotEmpty(ts.T(), verifyResp.Token) } + +func (ts *TokenTestSuite) TestPasswordVerificationHook() { + type verificationHookTestcase struct { + desc string + uri string + hookFunctionSQL string + expectedCode int + cleanupHookFunction string + } + cases := []verificationHookTestcase{ + { + desc: "Default success", + uri: "pg-functions://postgres/auth/password_verification_hook", + hookFunctionSQL: ` + create or replace function password_verification_hook(input jsonb) + returns json as $$ + begin + return json_build_object('decision', 'continue'); + end; $$ language plpgsql;`, + expectedCode: http.StatusOK, + cleanupHookFunction: "password_verification_hook(input jsonb)", + }, { + desc: "Reject- Enabled", + uri: "pg-functions://postgres/auth/password_verification_hook_reject", + hookFunctionSQL: ` + create or replace function password_verification_hook_reject(input jsonb) + returns json as $$ + begin + return json_build_object('decision', 'continue'); + end; $$ language plpgsql;`, + expectedCode: http.StatusForbidden, + cleanupHookFunction: "password_verification_hook_reject(input jsonb)", + }, + } + for _, c := range cases { + ts.T().Run(c.desc, func(t *testing.T) { + ts.Config.Hook.PasswordVerificationAttempt.Enabled = true + ts.Config.Hook.PasswordVerificationAttempt.URI = c.uri + require.NoError(ts.T(), ts.Config.Hook.PasswordVerificationAttempt.ValidateAndPopulateExtensibilityPoint()) + require.NoError(ts.T(), ts.Config.Hook.MFAVerificationAttempt.ValidateAndPopulateExtensibilityPoint()) + + err := ts.API.db.RawQuery(c.hookFunctionSQL).Exec() + require.NoError(t, err) + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "email": "test@example.com", + "password": "password", + })) + + req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=password", &buffer) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), c.expectedCode, w.Code) + cleanupHookSQL := fmt.Sprintf("drop function if exists %s", ts.Config.Hook.PasswordVerificationAttempt.HookName) + require.NoError(ts.T(), ts.API.db.RawQuery(cleanupHookSQL).Exec()) + + }) + } + +} From 7c9889ff632c37ef657c3020b3f5ac5b76a27f35 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 21:40:47 +0800 Subject: [PATCH 06/13] fix: use input address instead of input --- internal/api/token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/token.go b/internal/api/token.go index 6a485ce2b1..920d10c72f 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -153,7 +153,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri Valid: isValidPassword, } output := hooks.PasswordVerificationAttemptOutput{} - err := a.invokeHook(ctx, input, &output) + err := a.invokeHook(ctx, &input, &output) if err != nil { return err } From b7f7d249be2ea22186125bb2a0d3727dba8f2a60 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 22:25:17 +0800 Subject: [PATCH 07/13] feat: reduce test size --- internal/api/mfa.go | 2 +- internal/api/token_test.go | 21 ++++++++++----------- internal/conf/configuration.go | 3 ++- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/api/mfa.go b/internal/api/mfa.go index 4474fdab49..fbf369dfa6 100644 --- a/internal/api/mfa.go +++ b/internal/api/mfa.go @@ -270,7 +270,7 @@ func (a *API) invokeHook(ctx context.Context, input, output any) error { } if _, err := a.runHook(ctx, config.Hook.PasswordVerificationAttempt.HookName, input, output); err != nil { - return internalServerError("Error invoking MFA verification hook.").WithInternalError(err) + return internalServerError("Error invoking password verification hook.").WithInternalError(err) } if hookOutput.IsError() { diff --git a/internal/api/token_test.go b/internal/api/token_test.go index dbdc84d61a..179d39b882 100644 --- a/internal/api/token_test.go +++ b/internal/api/token_test.go @@ -591,11 +591,10 @@ func (ts *TokenTestSuite) TestMagicLinkPKCESignIn() { func (ts *TokenTestSuite) TestPasswordVerificationHook() { type verificationHookTestcase struct { - desc string - uri string - hookFunctionSQL string - expectedCode int - cleanupHookFunction string + desc string + uri string + hookFunctionSQL string + expectedCode int } cases := []verificationHookTestcase{ { @@ -607,8 +606,7 @@ func (ts *TokenTestSuite) TestPasswordVerificationHook() { begin return json_build_object('decision', 'continue'); end; $$ language plpgsql;`, - expectedCode: http.StatusOK, - cleanupHookFunction: "password_verification_hook(input jsonb)", + expectedCode: http.StatusOK, }, { desc: "Reject- Enabled", uri: "pg-functions://postgres/auth/password_verification_hook_reject", @@ -616,10 +614,9 @@ func (ts *TokenTestSuite) TestPasswordVerificationHook() { create or replace function password_verification_hook_reject(input jsonb) returns json as $$ begin - return json_build_object('decision', 'continue'); + return json_build_object('decision', 'reject'); end; $$ language plpgsql;`, - expectedCode: http.StatusForbidden, - cleanupHookFunction: "password_verification_hook_reject(input jsonb)", + expectedCode: http.StatusForbidden, }, } for _, c := range cases { @@ -627,7 +624,6 @@ func (ts *TokenTestSuite) TestPasswordVerificationHook() { ts.Config.Hook.PasswordVerificationAttempt.Enabled = true ts.Config.Hook.PasswordVerificationAttempt.URI = c.uri require.NoError(ts.T(), ts.Config.Hook.PasswordVerificationAttempt.ValidateAndPopulateExtensibilityPoint()) - require.NoError(ts.T(), ts.Config.Hook.MFAVerificationAttempt.ValidateAndPopulateExtensibilityPoint()) err := ts.API.db.RawQuery(c.hookFunctionSQL).Exec() require.NoError(t, err) @@ -642,9 +638,12 @@ func (ts *TokenTestSuite) TestPasswordVerificationHook() { w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), c.expectedCode, w.Code) cleanupHookSQL := fmt.Sprintf("drop function if exists %s", ts.Config.Hook.PasswordVerificationAttempt.HookName) require.NoError(ts.T(), ts.API.db.RawQuery(cleanupHookSQL).Exec()) + // Reset so it doesn't affect other tests + ts.Config.Hook.PasswordVerificationAttempt.Enabled = false }) } diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 93749a8bae..795df630c1 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -447,13 +447,14 @@ type HookConfiguration struct { type ExtensibilityPointConfiguration struct { URI string `json:"uri"` - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled" default:"false"` HookName string `json:"hook_name"` } func (h *HookConfiguration) Validate() error { points := []ExtensibilityPointConfiguration{ h.MFAVerificationAttempt, + h.PasswordVerificationAttempt, } for _, point := range points { if err := point.ValidateAndPopulateExtensibilityPoint(); err != nil { From 1073fb830c76e65e1ad61460db437b855f4ca5c1 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 22:29:08 +0800 Subject: [PATCH 08/13] fix: remove logout and struct tag --- internal/api/token.go | 3 --- internal/conf/configuration.go | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/api/token.go b/internal/api/token.go index 920d10c72f..6664d55c92 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -159,9 +159,6 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri } if output.Decision == hooks.HookRejection { - if err := models.Logout(a.db, user.ID); err != nil { - return err - } if output.Message == "" { output.Message = hooks.DefaultPasswordHookRejectionMessage } diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 795df630c1..162e233d31 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -447,7 +447,7 @@ type HookConfiguration struct { type ExtensibilityPointConfiguration struct { URI string `json:"uri"` - Enabled bool `json:"enabled" default:"false"` + Enabled bool `json:"enabled"` HookName string `json:"hook_name"` } From 6d561cc0797a0f8d18bd35e313a0702004647dd2 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 1 Dec 2023 22:30:27 +0800 Subject: [PATCH 09/13] Update internal/hooks/auth_hooks.go --- internal/hooks/auth_hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/hooks/auth_hooks.go b/internal/hooks/auth_hooks.go index 856ef257aa..3a3406bbb0 100644 --- a/internal/hooks/auth_hooks.go +++ b/internal/hooks/auth_hooks.go @@ -45,7 +45,7 @@ type PasswordVerificationAttemptInput struct { type PasswordVerificationAttemptOutput struct { Decision string `json:"decision"` Message string `json:"message"` - HookError AuthHookError `json:"hook_error" split_words:"true"` + HookError AuthHookError `json:"error" split_words:"true"` } func (mf *MFAVerificationAttemptOutput) IsError() bool { From 37414b85e579d24bd02ca61f59bd217d4750930f Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Dec 2023 22:32:18 +0800 Subject: [PATCH 10/13] fix: remove redundant struct tags --- internal/hooks/auth_hooks.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/hooks/auth_hooks.go b/internal/hooks/auth_hooks.go index 3a3406bbb0..19466200f1 100644 --- a/internal/hooks/auth_hooks.go +++ b/internal/hooks/auth_hooks.go @@ -34,7 +34,7 @@ type MFAVerificationAttemptInput struct { type MFAVerificationAttemptOutput struct { Decision string `json:"decision"` Message string `json:"message"` - HookError AuthHookError `json:"error" split_words:"true"` + HookError AuthHookError `json:"error"` } type PasswordVerificationAttemptInput struct { @@ -45,7 +45,7 @@ type PasswordVerificationAttemptInput struct { type PasswordVerificationAttemptOutput struct { Decision string `json:"decision"` Message string `json:"message"` - HookError AuthHookError `json:"error" split_words:"true"` + HookError AuthHookError `json:"error"` } func (mf *MFAVerificationAttemptOutput) IsError() bool { From 54ea4955126681dde506710276b36edddf739cd1 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sat, 2 Dec 2023 07:18:23 +0800 Subject: [PATCH 11/13] Update internal/api/mfa.go --- internal/api/mfa.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/mfa.go b/internal/api/mfa.go index fbf369dfa6..0ce5a2e533 100644 --- a/internal/api/mfa.go +++ b/internal/api/mfa.go @@ -266,7 +266,7 @@ func (a *API) invokeHook(ctx context.Context, input, output any) error { case *hooks.PasswordVerificationAttemptInput: hookOutput, ok := output.(*hooks.PasswordVerificationAttemptOutput) if !ok { - panic("output should be *hooks.MFAVerificationAttemptOutput") + panic("output should be *hooks.PasswordVerificationAttemptOutput") } if _, err := a.runHook(ctx, config.Hook.PasswordVerificationAttempt.HookName, input, output); err != nil { From 3d78ada71e89913d15c66b1ca76c2fe409657480 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sat, 2 Dec 2023 09:12:37 +0800 Subject: [PATCH 12/13] Update internal/api/token.go Co-authored-by: Kang Ming --- internal/api/token.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/api/token.go b/internal/api/token.go index 6664d55c92..a4a647d54b 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -143,7 +143,6 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri if user.IsBanned() { return oauthError("invalid_grant", InvalidLoginMessage) - } isValidPassword := user.Authenticate(ctx, params.Password) if config.Hook.PasswordVerificationAttempt.Enabled { From 1887f88e37b0294f7177a1219e539044ca75c776 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 4 Dec 2023 13:21:55 +0800 Subject: [PATCH 13/13] feat: add option to logout user --- internal/api/token.go | 5 +++++ internal/hooks/auth_hooks.go | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/api/token.go b/internal/api/token.go index a4a647d54b..8deaf98f0b 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -161,6 +161,11 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri if output.Message == "" { output.Message = hooks.DefaultPasswordHookRejectionMessage } + if output.ShouldLogoutUser { + if err := models.Logout(a.db, user.ID); err != nil { + return err + } + } return forbiddenError(output.Message) } } diff --git a/internal/hooks/auth_hooks.go b/internal/hooks/auth_hooks.go index 19466200f1..a2fc701842 100644 --- a/internal/hooks/auth_hooks.go +++ b/internal/hooks/auth_hooks.go @@ -43,9 +43,10 @@ type PasswordVerificationAttemptInput struct { } type PasswordVerificationAttemptOutput struct { - Decision string `json:"decision"` - Message string `json:"message"` - HookError AuthHookError `json:"error"` + Decision string `json:"decision"` + Message string `json:"message"` + ShouldLogoutUser bool `json:"should_logout_user"` + HookError AuthHookError `json:"error"` } func (mf *MFAVerificationAttemptOutput) IsError() bool {