Skip to content

Commit c675749

Browse files
cstocktonChris Stockton
andauthored
feat: add metadata field to all hooks (#2365)
Adds constructors for all hook input types: * MFAVerificationAttemptInput * PasswordVerificationAttemptInput * CustomAccessTokenInput * SendSMSInput, * SendEmailInput To consistently populate metadata fields: * `name` - Hook Name * `uuid` - Request UUID * `time` - Request Time * `ip_address` Request IP Address This improves observability and security auditing by guaranteeing that all hook invocations include request metadata. It also enables new use cases by passing the request IP address. For example more advanced methods for rate limiting login or MFA attempts may now be implemented. Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
1 parent 961a7e6 commit c675749

8 files changed

Lines changed: 207 additions & 64 deletions

File tree

internal/api/e2e_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ func runVerifyBeforeUserCreatedHook(
6666
hookReq := &v0hooks.BeforeUserCreatedInput{}
6767
err := call.Unmarshal(hookReq)
6868
require.NoError(t, err)
69+
70+
require.NotNil(t, hookReq.Metadata)
71+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
6972
require.Equal(t, v0hooks.BeforeUserCreated, hookReq.Metadata.Name)
73+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
74+
require.False(t, hookReq.Metadata.Time.IsZero())
7075

7176
u := hookReq.User
7277
require.Equal(t, expUser.ID, u.ID)
@@ -103,7 +108,12 @@ func runVerifyAfterUserCreatedHook(
103108
hookReq := &v0hooks.AfterUserCreatedInput{}
104109
err := call.Unmarshal(hookReq)
105110
require.NoError(t, err)
111+
112+
require.NotNil(t, hookReq.Metadata)
113+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
106114
require.Equal(t, v0hooks.AfterUserCreated, hookReq.Metadata.Name)
115+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
116+
require.False(t, hookReq.Metadata.Time.IsZero())
107117

108118
u := hookReq.User
109119
require.Equal(t, expUser.ID, u.ID)
@@ -176,6 +186,12 @@ func signupAndConfirmEmail(
176186
err = call.Unmarshal(hookReq)
177187
require.NoError(t, err)
178188

189+
require.NotNil(t, hookReq.Metadata)
190+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
191+
require.Equal(t, v0hooks.SendEmail, hookReq.Metadata.Name)
192+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
193+
require.False(t, hookReq.Metadata.Time.IsZero())
194+
179195
// verify that the latest user from find user matches OTP
180196
otpHash := crypto.GenerateTokenHash(
181197
expUser.GetEmail(), hookReq.EmailData.Token)
@@ -285,6 +301,12 @@ func TestE2EHooks(t *testing.T) {
285301
err = call.Unmarshal(hookReq)
286302
require.NoError(t, err)
287303

304+
require.NotNil(t, hookReq.Metadata)
305+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
306+
require.Equal(t, v0hooks.SendSMS, hookReq.Metadata.Name)
307+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
308+
require.False(t, hookReq.Metadata.Time.IsZero())
309+
288310
latestUser, err := models.FindUserByID(inst.Conn, signupUser.ID)
289311
require.NoError(t, err)
290312
require.NotNil(t, latestUser)
@@ -383,6 +405,12 @@ func TestE2EHooks(t *testing.T) {
383405
err = call.Unmarshal(hookReq)
384406
require.NoError(t, err)
385407

408+
require.NotNil(t, hookReq.Metadata)
409+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
410+
require.Equal(t, v0hooks.SendSMS, hookReq.Metadata.Name)
411+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
412+
require.False(t, hookReq.Metadata.Time.IsZero())
413+
386414
require.Equal(t, currentUser.ID, hookReq.User.ID)
387415
require.Equal(t, currentUser.Aud, hookReq.User.Aud)
388416
require.Equal(t, currentUser.Phone, hookReq.User.Phone)
@@ -924,6 +952,13 @@ func TestE2EHooks(t *testing.T) {
924952
hookReq := &v0hooks.CustomAccessTokenInput{}
925953
err := call.Unmarshal(hookReq)
926954
require.NoError(t, err)
955+
956+
require.NotNil(t, hookReq.Metadata)
957+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
958+
require.Equal(t, v0hooks.CustomizeAccessToken, hookReq.Metadata.Name)
959+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
960+
require.False(t, hookReq.Metadata.Time.IsZero())
961+
927962
require.Equal(t, currentUser.ID, hookReq.UserID)
928963
require.Equal(t, currentUser.ID.String(), hookReq.Claims.Subject)
929964
}
@@ -1127,6 +1162,12 @@ func TestE2EHooks(t *testing.T) {
11271162
err = call.Unmarshal(hookReq)
11281163
require.NoError(t, err)
11291164

1165+
require.NotNil(t, hookReq.Metadata)
1166+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
1167+
require.Equal(t, v0hooks.SendEmail, hookReq.Metadata.Name)
1168+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
1169+
require.False(t, hookReq.Metadata.Time.IsZero())
1170+
11301171
// hook user matches the signup user
11311172
require.Equal(t, signupUser.ID, hookReq.User.ID)
11321173
require.Equal(t, signupUser.Aud, hookReq.User.Aud)
@@ -1240,6 +1281,12 @@ func TestE2EHooks(t *testing.T) {
12401281
err = call.Unmarshal(hookReq)
12411282
require.NoError(t, err)
12421283

1284+
require.NotNil(t, hookReq.Metadata)
1285+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
1286+
require.Equal(t, v0hooks.SendEmail, hookReq.Metadata.Name)
1287+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
1288+
require.False(t, hookReq.Metadata.Time.IsZero())
1289+
12431290
// verify there is an ott generated
12441291
ott, err := models.FindOneTimeToken(
12451292
inst.Conn,
@@ -1343,6 +1390,12 @@ func TestE2EHooks(t *testing.T) {
13431390
err = call.Unmarshal(hookReq)
13441391
require.NoError(t, err)
13451392

1393+
require.NotNil(t, hookReq.Metadata)
1394+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
1395+
require.Equal(t, v0hooks.SendEmail, hookReq.Metadata.Name)
1396+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
1397+
require.False(t, hookReq.Metadata.Time.IsZero())
1398+
13461399
// hook user matches the signup user
13471400
require.Equal(t, signupUser.ID, hookReq.User.ID)
13481401
require.Equal(t, signupUser.Aud, hookReq.User.Aud)
@@ -1453,6 +1506,12 @@ func TestE2EHooks(t *testing.T) {
14531506
err = call.Unmarshal(hookReq)
14541507
require.NoError(t, err)
14551508

1509+
require.NotNil(t, hookReq.Metadata)
1510+
require.NotEmpty(t, hookReq.Metadata.IPAddress)
1511+
require.Equal(t, v0hooks.SendEmail, hookReq.Metadata.Name)
1512+
require.NotEqual(t, uuid.Nil, hookReq.Metadata.UUID)
1513+
require.False(t, hookReq.Metadata.Time.IsZero())
1514+
14561515
// verify there is an ott generated
14571516
ott, err := models.FindOneTimeToken(
14581517
inst.Conn,

internal/api/hooks_test.go

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,6 @@ func (ts *HooksTestSuite) TestRunHTTPHook() {
7777
// setup mock requests for hooks
7878
defer gock.OffAll()
7979

80-
input := v0hooks.SendSMSInput{
81-
User: ts.TestUser,
82-
SMS: v0hooks.SMS{
83-
OTP: "123456",
84-
},
85-
}
8680
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
8781
ts.Config.Hook.SendSMS.URI = testURL
8882

@@ -126,8 +120,16 @@ func (ts *HooksTestSuite) TestRunHTTPHook() {
126120
ts.Run(tc.description, func() {
127121
req, _ := http.NewRequest("POST", ts.Config.Hook.SendSMS.URI, nil)
128122

123+
input := v0hooks.NewSendSMSInput(
124+
req,
125+
ts.TestUser,
126+
v0hooks.SMS{
127+
OTP: "123456",
128+
},
129+
)
130+
129131
var output v0hooks.SendSMSOutput
130-
err := ts.API.hooksMgr.InvokeHook(ts.API.db, req, &input, &output)
132+
err := ts.API.hooksMgr.InvokeHook(ts.API.db, req, input, &output)
131133

132134
if !tc.expectError {
133135
require.NoError(ts.T(), err)
@@ -143,12 +145,6 @@ func (ts *HooksTestSuite) TestRunHTTPHook() {
143145
func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
144146
defer gock.OffAll()
145147

146-
input := v0hooks.SendSMSInput{
147-
User: ts.TestUser,
148-
SMS: v0hooks.SMS{
149-
OTP: "123456",
150-
},
151-
}
152148
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
153149
ts.Config.Hook.SendSMS.URI = testURL
154150

@@ -169,8 +165,16 @@ func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
169165
req, err := http.NewRequest("POST", "http://localhost:9998/otp", nil)
170166
require.NoError(ts.T(), err)
171167

168+
input := v0hooks.NewSendSMSInput(
169+
req,
170+
ts.TestUser,
171+
v0hooks.SMS{
172+
OTP: "123456",
173+
},
174+
)
175+
172176
var output v0hooks.SendSMSOutput
173-
err = ts.API.hooksMgr.InvokeHook(ts.API.db, req, &input, &output)
177+
err = ts.API.hooksMgr.InvokeHook(ts.API.db, req, input, &output)
174178
require.NoError(ts.T(), err)
175179

176180
// Ensure that all expected HTTP interactions (mocks) have been called
@@ -180,12 +184,6 @@ func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
180184
func (ts *HooksTestSuite) TestShouldReturnErrorForNonJSONContentType() {
181185
defer gock.OffAll()
182186

183-
input := v0hooks.SendSMSInput{
184-
User: ts.TestUser,
185-
SMS: v0hooks.SMS{
186-
OTP: "123456",
187-
},
188-
}
189187
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
190188
ts.Config.Hook.SendSMS.URI = testURL
191189

@@ -198,8 +196,16 @@ func (ts *HooksTestSuite) TestShouldReturnErrorForNonJSONContentType() {
198196
req, err := http.NewRequest("POST", "http://localhost:9999/otp", nil)
199197
require.NoError(ts.T(), err)
200198

199+
input := v0hooks.NewSendSMSInput(
200+
req,
201+
ts.TestUser,
202+
v0hooks.SMS{
203+
OTP: "123456",
204+
},
205+
)
206+
201207
var output v0hooks.SendSMSOutput
202-
err = ts.API.hooksMgr.InvokeHook(ts.API.db, req, &input, &output)
208+
err = ts.API.hooksMgr.InvokeHook(ts.API.db, req, input, &output)
203209
require.Error(ts.T(), err, "Expected an error due to wrong content type")
204210
require.Contains(ts.T(), err.Error(), "Invalid JSON response.")
205211
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called")

internal/api/mail.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -860,12 +860,13 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User,
860860
emailData.FactorType = params.factorType
861861
}
862862

863-
input := v0hooks.SendEmailInput{
864-
User: u,
865-
EmailData: emailData,
866-
}
863+
input := v0hooks.NewSendEmailInput(
864+
r,
865+
u,
866+
emailData,
867+
)
867868
output := v0hooks.SendEmailOutput{}
868-
return a.hooksMgr.InvokeHook(tx, r, &input, &output)
869+
return a.hooksMgr.InvokeHook(tx, r, input, &output)
869870
}
870871

871872
// Increment email send operations here, since this metric is meant to count number of mail

internal/api/mfa.go

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -408,16 +408,17 @@ func (a *API) challengePhoneFactor(w http.ResponseWriter, r *http.Request) error
408408
phone := factor.Phone.String()
409409

410410
if config.Hook.SendSMS.Enabled {
411-
input := v0hooks.SendSMSInput{
412-
User: user,
413-
SMS: v0hooks.SMS{
411+
input := v0hooks.NewSendSMSInput(
412+
r,
413+
user,
414+
v0hooks.SMS{
414415
OTP: otp,
415416
SMSType: "mfa",
416417
Phone: phone,
417418
},
418-
}
419+
)
419420
output := v0hooks.SendSMSOutput{}
420-
err := a.hooksMgr.InvokeHook(db, r, &input, &output)
421+
err := a.hooksMgr.InvokeHook(db, r, input, &output)
421422
if err != nil {
422423
return apierrors.NewInternalServerError("error invoking hook")
423424
}
@@ -648,15 +649,16 @@ func (a *API) verifyTOTPFactor(w http.ResponseWriter, r *http.Request, params *V
648649
})
649650

650651
if config.Hook.MFAVerificationAttempt.Enabled {
651-
input := v0hooks.MFAVerificationAttemptInput{
652-
UserID: user.ID,
653-
FactorID: factor.ID,
654-
FactorType: factor.FactorType,
655-
Valid: valid,
656-
}
652+
input := v0hooks.NewMFAVerificationAttemptInput(
653+
r,
654+
user.ID,
655+
factor.ID,
656+
factor.FactorType,
657+
valid,
658+
)
657659

658660
output := v0hooks.MFAVerificationAttemptOutput{}
659-
err := a.hooksMgr.InvokeHook(nil, r, &input, &output)
661+
err := a.hooksMgr.InvokeHook(nil, r, input, &output)
660662
if err != nil {
661663
return err
662664
}
@@ -799,15 +801,16 @@ func (a *API) verifyPhoneFactor(w http.ResponseWriter, r *http.Request, params *
799801
valid = subtle.ConstantTimeCompare([]byte(otpCode), []byte(params.Code)) == 1
800802
}
801803
if config.Hook.MFAVerificationAttempt.Enabled {
802-
input := v0hooks.MFAVerificationAttemptInput{
803-
UserID: user.ID,
804-
FactorID: factor.ID,
805-
FactorType: factor.FactorType,
806-
Valid: valid,
807-
}
804+
input := v0hooks.NewMFAVerificationAttemptInput(
805+
r,
806+
user.ID,
807+
factor.ID,
808+
factor.FactorType,
809+
valid,
810+
)
808811

809812
output := v0hooks.MFAVerificationAttemptOutput{}
810-
err := a.hooksMgr.InvokeHook(nil, r, &input, &output)
813+
err := a.hooksMgr.InvokeHook(nil, r, input, &output)
811814
if err != nil {
812815
return err
813816
}

internal/api/phone.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ func (a *API) sendPhoneConfirmation(r *http.Request, tx *storage.Connection, use
9595
otp = crypto.GenerateOtp(config.Sms.OtpLength)
9696

9797
if config.Hook.SendSMS.Enabled {
98-
input := v0hooks.SendSMSInput{
99-
User: user,
100-
SMS: v0hooks.SMS{
98+
input := v0hooks.NewSendSMSInput(
99+
r,
100+
user,
101+
v0hooks.SMS{
101102
OTP: otp,
102103
Phone: phone,
103104
},
104-
}
105+
)
105106
output := v0hooks.SendSMSOutput{}
106-
err := a.hooksMgr.InvokeHook(tx, r, &input, &output)
107+
err := a.hooksMgr.InvokeHook(tx, r, input, &output)
107108
if err != nil {
108109
return "", err
109110
}

internal/api/token.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,13 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
152152
}
153153

154154
if config.Hook.PasswordVerificationAttempt.Enabled {
155-
input := v0hooks.PasswordVerificationAttemptInput{
156-
UserID: user.ID,
157-
Valid: isValidPassword,
158-
}
155+
input := v0hooks.NewPasswordVerificationAttemptInput(
156+
r,
157+
user.ID,
158+
isValidPassword,
159+
)
159160
output := v0hooks.PasswordVerificationAttemptOutput{}
160-
if err := a.hooksMgr.InvokeHook(nil, r, &input, &output); err != nil {
161+
if err := a.hooksMgr.InvokeHook(nil, r, input, &output); err != nil {
161162
return err
162163
}
163164

0 commit comments

Comments
 (0)