Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mfa verification postgres hook #1314

Merged
merged 46 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
67ecb95
feat: initial commit
Nov 20, 2023
6078578
fix: add scaffolding
Nov 20, 2023
51e1987
feat: generate payload
Nov 20, 2023
1c15e18
feat: set up calling mechanism
Nov 20, 2023
c62ff8a
feat: add more surrounding logic
Nov 20, 2023
fa1122a
fix: reinstate relevant constants
Nov 21, 2023
da86cde
feat: add minor validation and cleanup
Nov 21, 2023
185b1d7
feat: add hook configuration to settings
Nov 21, 2023
6676552
feat: update naming conventions
Nov 22, 2023
9493543
feat:refactor to do w/o abstraction
Nov 24, 2023
4b08570
fix: remove now redundant methods
Nov 24, 2023
78e184c
fix: add some logging
Nov 24, 2023
f3ae14b
fix: remove unused code
Nov 24, 2023
8708df0
Merge branch 'master' into j0/mfa_verification_counter_hook
J0 Nov 24, 2023
dc9d6da
fix: reset unused files
Nov 24, 2023
1cd6eff
feat: add stubs, revert unneeded changes
Nov 24, 2023
3a93440
Merge branch 'j0/mfa_verification_counter_hook' of github.com:supabas…
Nov 24, 2023
8a3ec9c
refactor: rename hook ep
Nov 27, 2023
e21a806
refactor: rename HookErrorResponse->AuthHookErrorResponse
Nov 27, 2023
b7a8c23
test: add initial tests
Nov 27, 2023
5a4ce40
feat: properly fetch response
Nov 27, 2023
aa9d920
feat: update tests
Nov 27, 2023
176ad48
feat: add a few tests
Nov 27, 2023
32b1a8d
fix: update test structure
Nov 27, 2023
b9d6ba7
feat: add local timeout and more tests
Nov 27, 2023
b50b7ed
refactor: cut back on redundant code
Nov 27, 2023
651151e
fix: patch tests
Nov 27, 2023
f9fa25e
fix: partial conversion to use Auth Hook structs
Nov 27, 2023
47504b0
feat: add initial Error Message
Nov 27, 2023
fa1cbde
refactor: rename some vars
Nov 27, 2023
c4470d7
chore: small comments
Nov 27, 2023
2842b23
chore: add a default message
Nov 28, 2023
2260f96
refactor: remove excess code
Nov 28, 2023
ddae946
chore: convert fetchhookname into configuration load
Nov 28, 2023
25f95f4
fix: light refactor of tests
Nov 28, 2023
7693417
Merge branch 'master' of github.com:supabase/gotrue into j0/mfa_verif…
Nov 29, 2023
af2c255
fix: add status code check
Nov 29, 2023
7b874b8
refactor: use errors
Nov 29, 2023
447de5f
refactor: shove config back to configuration
Nov 29, 2023
18ccfc6
Update internal/conf/configuration.go
J0 Nov 30, 2023
be4fd32
Update internal/hooks/auth_hooks.go
J0 Nov 30, 2023
a15631b
refactor: use error interface instead
Nov 30, 2023
8641c92
Merge branch 'j0/mfa_verification_counter_hook' of github.com:supabas…
Nov 30, 2023
2d512cb
fix: make schema constant
Nov 30, 2023
3c99bde
test: reinstate test suite
Nov 30, 2023
3699c01
fix: remove unused structs
Nov 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions internal/api/auth_hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package api

import (
"encoding/json"
"errors"
"net/url"
"time"

"fmt"
"github.com/gofrs/uuid"
"github.com/supabase/gotrue/internal/conf"
"github.com/supabase/gotrue/internal/storage"
"strings"
)

type HookType string

const (
PostgresHook HookType = "postgres"
HTTPHook HookType = "http"
)

type AuthHook struct {
*conf.ExtensibilityPointConfiguration
payload []byte
hookType HookType
event string
db *storage.Connection
}

// Hook Events
const (
MFAVerificationEvent = "auth.mfa_verfication"
)

const (
defaultTimeout = time.Second * 2
)

type HookErrorResponse struct {
ErrorMessage string `json:"error_message"`
ErrorCode string `json:"error_code"`
RetryAfter bool `json:"retry_after"`
}
J0 marked this conversation as resolved.
Show resolved Hide resolved

type MFAVerificationHookResponse struct {
Decision string `json:"decision"`
}

func parseErrorResponse(response []byte) (*HookErrorResponse, error) {
var errResp HookErrorResponse
err := json.Unmarshal(response, &errResp)
if err != nil {
return nil, err
}
if errResp.ErrorMessage != "" {
return &errResp, nil
}
return nil, err
}

func parseMFAVerificationResponse(response []byte) (*MFAVerificationHookResponse, error) {
var MFAVerificationResponse MFAVerificationHookResponse
err := json.Unmarshal(response, &MFAVerificationResponse)
if err != nil {
return nil, err
}

return &MFAVerificationResponse, err
}

// Functions for encoding and decoding payload
func CreateMFAVerificationHookInput(user_id uuid.UUID, factor_id uuid.UUID, valid bool) ([]byte, error) {
payload := struct {
UserID uuid.UUID `json:"user_id"`
FactorID uuid.UUID `json:"factor_id"`
Valid bool `json:"valid"`
}{
UserID: user_id,
FactorID: factor_id,
Valid: valid,
}
J0 marked this conversation as resolved.
Show resolved Hide resolved
data, err := json.Marshal(&payload)
if err != nil {
return nil, err
}
return data, nil
J0 marked this conversation as resolved.
Show resolved Hide resolved
}

func (ah *AuthHook) Trigger() ([]byte, error) {
// Parse URI object
url, err := url.Parse(ah.ExtensibilityPointConfiguration.URI)
if err != nil {
return nil, err
}
// trigger appropriate type of hook
switch url.Scheme {
case string(PostgresHook):
return ah.triggerPostgresHook()
case string(HTTPHook):
return ah.triggerHTTPHook()
default:
return nil, errors.New("unsupported hook type")
}

return nil, nil
J0 marked this conversation as resolved.
Show resolved Hide resolved
}

func (ah *AuthHook) fetchHookName() (string, error) {
u, err := url.Parse(ah.ExtensibilityPointConfiguration.URI)
if err != nil {
return "", err
}
pathParts := strings.Split(u.Path, "/")
if len(pathParts) < 3 {
return "", fmt.Errorf("URI path does not contain enough parts")
}
J0 marked this conversation as resolved.
Show resolved Hide resolved
schema := pathParts[1]
table := pathParts[2]
// TODO: maybe enforce checks on this name?

return schema + "." + table, nil
}

func (ah *AuthHook) triggerPostgresHook() ([]byte, error) {
// Determine Result payload and request payload
var result []byte
hookName, err := ah.fetchHookName()
if err != nil {
return nil, err
}
if err := ah.db.Transaction(func(tx *storage.Connection) error {
resp := tx.RawQuery(fmt.Sprintf("SELECT %s('%s')", hookName, ah.payload))
J0 marked this conversation as resolved.
Show resolved Hide resolved
terr := resp.First(result)
if terr != nil {
return terr
}
return nil
}); err != nil {
return nil, err
}

return result, nil

}

func (a *AuthHook) triggerHTTPHook() ([]byte, error) {
return nil, errors.New("not implemented error")
J0 marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 1 addition & 3 deletions internal/api/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ type HookEvent string

const (
headerHookSignature = "x-webhook-signature"
defaultHookRetries = 3
gotrueIssuer = "gotrue"
ValidateEvent = "validate"
SignupEvent = "signup"
EmailChangeEvent = "email_change"
LoginEvent = "login"
defaultHookRetries = 3
)

var defaultTimeout = time.Second * 5

type webhookClaims struct {
jwt.StandardClaims
SHA256 string `json:"sha256"`
Expand Down
41 changes: 40 additions & 1 deletion internal/api/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"

"errors"
"net/url"

"github.com/aaronarduino/goqrsvg"
Expand Down Expand Up @@ -245,7 +246,45 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error {
return badRequestError("%v has expired, verify against another challenge or create a new challenge.", challenge.ID)
}

if valid := totp.Validate(params.Code, factor.Secret); !valid {
valid := totp.Validate(params.Code, factor.Secret)
if config.Hook.MFA.IsEnabled() {
// To allow for future cases where we don't know that the payload is going to be passed in

payload, err := CreateMFAVerificationHookInput(user.ID, factor.ID, valid)
if err != nil {
return err
}

h := AuthHook{
event: MFAVerificationEvent,
payload: payload,
hookType: PostgresHook,
// TODO: find a better way to relay this
db: a.db,
}

resp, err := h.Trigger()
if err != nil {
return err
}
parsedErrorResponse, err := parseErrorResponse(resp)
if err != nil {
return err
}
if parsedErrorResponse != nil {
return errors.New(parsedErrorResponse.ErrorMessage)
}
// TODO: Decide what to do here
response, err := parseMFAVerificationResponse(resp)
if err != nil {
return err
}
J0 marked this conversation as resolved.
Show resolved Hide resolved
// TODO: don't hard code this and also change to Enum + handle success case
if response.Decision == "reject" {
return errors.New("has made 5 unsuccssful verification attempts")
}
J0 marked this conversation as resolved.
Show resolved Hide resolved
}
if !valid {
return badRequestError("Invalid TOTP code entered")
}

Expand Down
5 changes: 5 additions & 0 deletions internal/api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type Settings struct {
SmsProvider string `json:"sms_provider"`
MFAEnabled bool `json:"mfa_enabled"`
SAMLEnabled bool `json:"saml_enabled"`

// TODO: Remove this later. For debugging
MFAHook string `json:"mfa_hook"`
}

func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -67,6 +70,8 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Phone: config.External.Phone.Enabled,
Zoom: config.External.Zoom.Enabled,
},
// TODO: Remove this too. For debugging
MFAHook: config.Hook.MFA.URI,

DisableSignup: config.DisableSignup,
MailerAutoconfirm: config.Mailer.Autoconfirm,
Expand Down
25 changes: 24 additions & 1 deletion internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ type GlobalConfiguration struct {
Sms SmsProviderConfiguration `json:"sms"`
DisableSignup bool `json:"disable_signup" split_words:"true"`
Webhook WebhookConfig `json:"webhook" split_words:"true"`
Hook HookConfiguration `json:"hook" split_words:"true"`
Security SecurityConfiguration `json:"security"`
Sessions SessionsConfiguration `json:"sessions"`
MFA MFAConfiguration `json:"MFA"`
Expand Down Expand Up @@ -379,6 +380,29 @@ type WebhookConfig struct {
Events []string `json:"events"`
}

// Moving away from the existing HookConfig so we can get a fresh start.
type HookConfiguration struct {
// TODO (Joel): Fix the naming later
MFA ExtensibilityPointConfiguration `json:"mfa"`
}

type ExtensibilityPointConfiguration struct {
URI string `json:"uri"`
}

func (e *ExtensibilityPointConfiguration) ValidateExtensibilityPoint() error {
if e.URI != "" {
_, err := url.Parse(e.URI)
if err != nil {
return errors.New("hook entry should be a valid URI")
}
}
return nil
J0 marked this conversation as resolved.
Show resolved Hide resolved
}
func (e *ExtensibilityPointConfiguration) IsEnabled() bool {
return e.URI != ""
}

func (w *WebhookConfig) HasEvent(event string) bool {
for _, name := range w.Events {
if event == name {
Expand Down Expand Up @@ -424,7 +448,6 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) {
}
config.Sms.SMSTemplate = template
}

return config, nil
}

Expand Down
4 changes: 3 additions & 1 deletion internal/conf/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ func TestMain(m *testing.M) {

func TestGlobal(t *testing.T) {
os.Setenv("GOTRUE_SITE_URL", "http://localhost:8080")
os.Setenv("GOTRUE_DB_DRIVER", "mysql")
os.Setenv("GOTRUE_DB_DRIVER", "postgres")
J0 marked this conversation as resolved.
Show resolved Hide resolved
os.Setenv("GOTRUE_DB_DATABASE_URL", "fake")
os.Setenv("GOTRUE_OPERATOR_TOKEN", "token")
os.Setenv("GOTRUE_API_REQUEST_ID_HEADER", "X-Request-ID")
os.Setenv("GOTRUE_JWT_SECRET", "secret")
os.Setenv("API_EXTERNAL_URL", "http://localhost:9999")
os.Setenv("GOTRUE_HOOK_MFA_URI", "postgres://postgres/auth/count_failed_attempts")
J0 marked this conversation as resolved.
Show resolved Hide resolved
gc, err := LoadGlobal("")
require.NoError(t, err)
require.NotNil(t, gc)
assert.Equal(t, "X-Request-ID", gc.API.RequestIDHeader)
assert.Equal(t, "postgres://postgres/auth/count_failed_attempts", gc.Hook.MFA.URI)
}
Loading