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 support (disabled by default) #736

Merged
merged 233 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
233 commits
Select commit Hold shift + click to select a range
2e8505a
db: add initial schema
Jun 14, 2022
1410733
fix: update migrations
Jun 14, 2022
9050cf8
fix: add additional constraints
Jun 14, 2022
aaa3d99
fix: add default
Jun 14, 2022
c4f8164
chore: remove whitespace
Jun 14, 2022
0ca6741
chore: remove email as a type
Jun 14, 2022
f83ab8e
fix: prevent updated_at from being nullable
Jun 14, 2022
04eac13
chore: update comments
Jun 14, 2022
6699405
feat:enable and disable mfa
Jun 15, 2022
76c0c6b
refactor: remove model files
Jun 15, 2022
8fd5ba8
Merge branch 'master' of https://github.com/supabase/gotrue into j0_a…
Jun 15, 2022
146cb6f
fix: pull in master
Jun 15, 2022
e796995
Merge branch 'master' of https://github.com/supabase/gotrue into j0_a…
Jun 15, 2022
6885b79
Merge branch 'j0_add_db_changes' of https://github.com/supabase/gotru…
Jun 15, 2022
b761423
refactor: rename backup codes to recovery codes
Jun 15, 2022
3afbc97
fix: update number of user fields
Jun 15, 2022
040e6d0
Update signup_test.go
J0 Jun 15, 2022
9cdc5d5
test: disable and disable
Jun 15, 2022
dc31a46
Merge branch 'j0_add_enable_add_disable' of https://github.com/supaba…
Jun 15, 2022
ce8f5b3
feat: add recovery codes api
Jun 15, 2022
6349e8f
feat: add initial modles for factor
Jun 16, 2022
4f5895e
feat: initial enroll endpoint
Jun 16, 2022
0fa9752
feat: initial commit for challenge API
Jun 16, 2022
680941a
refactor: add logic for filtering between factorid and simple name
Jun 16, 2022
53c089d
refactor: split based on factor simple name or id
Jun 16, 2022
9c9516b
refactor: make endpoints idempotent
Jun 16, 2022
f4e7a2f
chore: undo whitespace change
Jun 16, 2022
cb6a175
chore: remove whitespace
Jun 16, 2022
4c7ae3f
chore: naming and initial tests
Jun 16, 2022
c7bdbc4
test: add model test for good measure
Jun 16, 2022
fef5980
feat: add find factor methods
Jun 17, 2022
c191073
fix: change method definitions
Jun 17, 2022
3027ef9
initial commit for verify factor
Jun 20, 2022
55dee45
test: add tests, remove factor_ prefix
Jun 20, 2022
3e86dbf
Merge branch 'j0_add_enable_add_disable' into j0_generate_recovery_codes
J0 Jun 20, 2022
b1fa20a
tests: add http test
Jun 21, 2022
4ea0986
feat:initial verify endpoint
Jun 21, 2022
023f584
tests: add more tests
Jun 22, 2022
d1be5ff
chore: merge in downstream changes
Jun 22, 2022
285ac43
refactor: remove whitespace changes and minor labels
Jun 22, 2022
0c99b1c
Merge branch 'j0_generate_recovery_codes' of https://github.com/supab…
Jun 22, 2022
d26c628
fix: update db schema
Jun 29, 2022
e98ff1e
refactor: add states for factor_status
Jun 30, 2022
77c1fa8
fix: update statuses
Jun 30, 2022
1bebb39
fix: add semicolon
Jun 30, 2022
684e4fc
refactor: change bools to timestamps
Jun 30, 2022
61e46a8
Merge pull request #496 from supabase/j0_add_db_changes
J0 Jun 30, 2022
76e42aa
refactor: drop _mfa suffix
Jun 30, 2022
21e32da
Merge pull request #500 from supabase/j0_add_enable_add_disable
J0 Jun 30, 2022
14ba5f1
Merge branch 'mfa' into j0_generate_recovery_codes
J0 Jun 30, 2022
d8a0537
Merge branch 'mfa' into j0_enroll_device
J0 Jun 30, 2022
124ff88
Merge branch 'mfa' into j0_challenge_factor
J0 Jun 30, 2022
aa81ee8
refactor: change recovery code ID type to uuid
Jun 30, 2022
8948b71
fix: set length of token back to 8
Jun 30, 2022
b025bdd
refactor: revert code consumption time to used_at
Jun 30, 2022
23090cd
test: add test for FindValidRecoveryCodesByUser
Jul 1, 2022
f3ac106
refactor: convert var names to lowercase
Jul 1, 2022
5e27983
fix: add error check at end of GenerateRecoveryCodes
Jul 1, 2022
9d0618f
chore: run gofmt -s -w
Jul 1, 2022
a457637
Merge pull request #501 from supabase/j0_generate_recovery_codes
J0 Jul 1, 2022
7642d65
feat: pull in changes from main branch
Jul 1, 2022
5d546b6
chore: resolve errors.go merge conflicts
Jul 1, 2022
de3184a
refactor: update tests with new naming scheme
Jul 1, 2022
36f8ac5
chore: run gofmt
Jul 2, 2022
c653135
fix: update tests with new names
Jul 2, 2022
4f2460a
chore: renaming
Jul 2, 2022
1ae17b7
tests: refactor and add initial enroll factor tests
Jul 3, 2022
006d478
fix: add associations from factor to user
Jul 3, 2022
6edf167
refactor: cleanup
Jul 4, 2022
ea4d346
refactor: cleanup magic strings
Jul 4, 2022
5ec3dfb
Merge branch 'j0_enroll_device' into j0_challenge_factor
J0 Jul 4, 2022
ccdc78f
refactor: remove newlines
Jul 4, 2022
2bd0dfc
chore: introduce new factor type
Jul 5, 2022
6b8b9d0
fix: merge mfa into verify_factor
Jul 5, 2022
60c800b
chore: backpatch naming
Jul 5, 2022
141a71f
test: update verify factor test
Jul 5, 2022
327c9f1
refactor: add struct tags
Jul 11, 2022
9deae93
Merge pull request #502 from supabase/j0_enroll_device
J0 Jul 11, 2022
32e093f
Merge branch 'mfa' into j0_challenge_factor
J0 Jul 11, 2022
f176f92
refactor: add struct tas and change route names
Jul 12, 2022
d4a5281
refactor: add struct tags and change route names
Jul 12, 2022
1599ea8
Merge pull request #504 from supabase/j0_challenge_factor
J0 Jul 12, 2022
55a3bdf
fix: resolve merge conflicts
Jul 12, 2022
e507103
chore: reintroduce uncommented lines
Jul 19, 2022
46e6586
refactor: clean up tests
Jul 20, 2022
44c3baa
chore: add misc config vars and errors
Jul 20, 2022
4504906
tests: Add additional case for expried challenge + invalid code
Jul 20, 2022
7c06b18
fix: patch test
Jul 21, 2022
718b501
chore: add newlines
Jul 21, 2022
1c71c1d
chore: remove stray comment
Jul 21, 2022
d70fce4
refactor: delete challenge if expired
Jul 26, 2022
560cbc7
Merge pull request #508 from supabase/j0_verify_factor
J0 Jul 26, 2022
beddffd
feat: remove unused fields, change recovery_codes from GET to POST
Jul 26, 2022
a3ed021
refactor: remove type field from endpoint
Jul 26, 2022
e5088b2
Merge pull request #565 from supabase/j0_mfa_fixes
J0 Jul 26, 2022
237f69a
chore: speed up tests (#564)
karlseguin Jul 26, 2022
2ad9d68
Merge branch 'mfa' of https://github.com/supabase/gotrue into mfa
Jul 26, 2022
c65c136
feat: add admin delete methods
Jul 26, 2022
524c990
feat: add unenroll endpoint
Jul 26, 2022
0b63c9c
Revert "feat: add admin delete methods"
Jul 26, 2022
0c36586
fix: change behavior of unenroll
Jul 26, 2022
f68aedb
refactor: strip out /enable and /disable
Jul 27, 2022
0e90d48
Revert "feat: add admin delete methods"
Jul 27, 2022
3274e85
Merge pull request #568 from supabase/j0_refactor_endpoint_routes
J0 Jul 27, 2022
b50d0eb
refactr: move routes to /user
Jul 28, 2022
0c2c9fc
refactor: modify routes
Jul 28, 2022
b0e5dda
chore: remove stray comment
Jul 29, 2022
5374da8
chore: increase number of requests made so expected error is observed
Jul 29, 2022
bb24b9a
Revert "chore: increase number of requests made so expected error is …
Jul 29, 2022
22effe7
Merge pull request #570 from supabase/j0_patch_mfa_routes
J0 Jul 29, 2022
0c7fed4
chore:resolve merge conflicts
Aug 1, 2022
1a275ef
chore: refactor MFA unenroll route
Aug 1, 2022
9f92e2e
chore: add updated routes
Aug 1, 2022
f029a49
chore: resolve merge conflicts
Aug 1, 2022
032c12f
Merge pull request #566 from supabase/j0_mfa_unenroll
J0 Aug 1, 2022
e62e000
feat: admin endpoints
Aug 1, 2022
84fb5ba
fix: refactor and update audit log action types
Aug 1, 2022
37c305d
Merge branch 'mfa' of https://github.com/supabase/gotrue into j0_admi…
Aug 1, 2022
3e08ffb
tests: add additional checks for deletion endpoints
Aug 1, 2022
31137e4
refactor: remove stray constants
Aug 2, 2022
ce941f9
refactor: remove stray lines and comments
Aug 2, 2022
77aac52
refactor: remove notion of MFAEnabled
Aug 2, 2022
835fa5a
Merge pull request #579 from supabase/j0_remove_mfa_enabled_on_user
J0 Aug 2, 2022
a1e5afb
Merge pull request #576 from supabase/j0_admin_delete_endpoints
J0 Aug 2, 2022
a4022b1
refactor: remove /recovery_codes endpoint
Aug 2, 2022
607501a
Merge branch 'mfa' into j0_remove_recovery_codes_endpoint
J0 Aug 2, 2022
9cf6fac
Merge pull request #580 from supabase/j0_remove_recovery_codes_endpoint
J0 Aug 2, 2022
65b2424
feat: add update factor admin endpoint
Aug 3, 2022
ac845ba
feat: add IsMFAEnabled
Aug 3, 2022
a0eeee9
refactor: reinstate MFAEnabled
Aug 3, 2022
a4af59f
refactor: make issuer mandatory
Aug 6, 2022
ca67dcf
Merge pull request #595 from supabase/j0_minor_mfa_fixes
J0 Aug 8, 2022
cc1dd36
Merge pull request #586 from supabase/j0_add_update_factor_admin
J0 Aug 8, 2022
5eb4030
Merge branch 'mfa' into j0/reintroduce_mfa_enabled_checks
J0 Aug 9, 2022
1da78f2
chore: remove created_at field from /challenge
Aug 10, 2022
e405501
Merge branch 'mfa' into j0_remove_created_at
J0 Aug 10, 2022
ae60ac0
Merge pull request #597 from supabase/j0_remove_created_at
J0 Aug 10, 2022
258d669
Merge pull request #588 from supabase/j0/reintroduce_mfa_enabled_checks
J0 Aug 10, 2022
6670cc7
chore: merge in master
Aug 28, 2022
96bf6fb
chore: merge in master
Aug 28, 2022
30cc63c
Add MFA Sessions Fields (#643)
J0 Aug 29, 2022
b36b337
refactor: move constants into models (#646)
J0 Aug 29, 2022
9c009e4
feat: Convert QR to SVG (#624)
J0 Aug 29, 2022
ccfc4f5
Merge branch 'master' of github.com:supabase/gotrue into mfa
Aug 29, 2022
5738884
fix: change method of reading from config
Aug 29, 2022
3df3093
refactor: remove unused var
Aug 29, 2022
65d4255
fix: patch gosec errors
Aug 29, 2022
d0a5966
feat: cherry-pick step up changes onto separate branch (#652)
J0 Aug 29, 2022
091d56c
feat: initial gating of routes requiring 1FA
Aug 30, 2022
b8f486c
chore: add AMR entry
Aug 30, 2022
93d1549
refactor: update claims
Aug 30, 2022
7de12a0
Merge branch 'mfa' of github.com:supabase/gotrue into j0/mfa_one_fa_r…
Aug 30, 2022
732fd5c
test: add initial mfa login tests
Aug 30, 2022
912e685
refactor: Add sign in method to issueRefreshToken
Aug 30, 2022
6f5353a
fix: distinguish between code logins and recovery code logins
Aug 30, 2022
f750ec3
fix: add concept of first MFA login
Aug 31, 2022
9fdc8da
chore: add AMRClaims model content
Aug 31, 2022
8593fca
fix: patch various errors related to method signature
Aug 31, 2022
32224ff
fix: types for audit action logging
Aug 31, 2022
ad811ad
fix: patch sql syntax errors
Aug 31, 2022
ac0c3c8
chore: patch tests for stepup login
Aug 31, 2022
8572a3b
fix: update number of user fields
Sep 1, 2022
a30189a
feat: merge in GrantAuthenticatedUser params
Sep 1, 2022
1a4fa1f
refactor: modify access token generation
Sep 1, 2022
02f4bbb
fix: step up login
Sep 1, 2022
04f838d
fix: get tests to pass
Sep 1, 2022
7bff666
fix: patch gosec errors
Sep 1, 2022
8399e02
chore: pull in master into mfa (#660)
J0 Sep 1, 2022
0f5d1ea
fix: patch merge conflicts
Sep 2, 2022
14738e3
tests: add notion of first log in
Sep 2, 2022
5402221
feat: add session logic
Sep 2, 2022
aba96e4
refactor: remove stepup login
Sep 5, 2022
ea783cc
feat: remove stepup login related details
Sep 6, 2022
d00c496
refactor: remove unused fields
Sep 6, 2022
967f234
chore: merge in master
Sep 6, 2022
66ccf66
chore: remove forgotten file
hf Sep 7, 2022
ff53d0f
chore: merge in master
Sep 7, 2022
313a3bf
fix: remove recovery code logic
Sep 7, 2022
d8c6e03
Merge branch 'mfa_v1' of github.com:supabase/gotrue into mfa_v1
Sep 7, 2022
1f42bf8
refactor: change types to text, remove dead code
Sep 8, 2022
e3c1712
fix: change verify test expected status code
Sep 8, 2022
de537dd
refactor: convert challenge_id and factor_id to be uuid's
Sep 8, 2022
60ccd3f
refactor: remove GetFactor
Sep 9, 2022
86a530d
refactor: downcase sql files, shift constants back to respective files
Sep 9, 2022
98ab91b
refactor: remove require1FA
Sep 9, 2022
3eede1b
refactor: modify aal levels to be enum
Sep 9, 2022
f5a7a8b
refactor: reinstate notion of factor type
Sep 11, 2022
10b4d31
refactor:change types of enroll/unenroll from string to bool
Sep 11, 2022
67a77e8
test: add missing require checks
Sep 12, 2022
be4b0bc
fix: merge in master, globally replace hardcoded TOTP var
Sep 12, 2022
a66de66
fix: add tests for removing factor related sessions on unenroll
Sep 12, 2022
a2a8a67
fix: update db sessions in issueRefreshToken as well
Sep 13, 2022
b25f611
fix: convert issuer to default to siteURL
Sep 13, 2022
88d89c0
refactor: change enroll factor message and remove redundant requireAu…
Sep 13, 2022
d5ee5e2
refactor: remove success field in verifyFactor Response
Sep 13, 2022
744362f
fix: remove success code reutrned and allow unverified factors to be …
Sep 13, 2022
e1a16fc
refactor: add IP address type
Sep 14, 2022
e94c98f
refactor: add IP address type
Sep 14, 2022
840e5ad
fix: return token in mfa verify & update session aal (#682)
kangmingtay Sep 15, 2022
329f5d4
fix: add default value for challenge IP
Sep 16, 2022
3f65ad7
fix: resolve merge conflicts
Sep 16, 2022
03f4caa
fix: patch staticcheck errors
Sep 16, 2022
b6971b4
fix: add partial index to allow multiple empty strings
Sep 16, 2022
3e742c4
refactor: remove FindChallengesByFactorID and add IP address to chall…
Sep 16, 2022
fb58fff
fix: add IP address checks
Sep 16, 2022
5da121c
fix: minor mfa changes (#692)
kangmingtay Sep 19, 2022
147f686
Reinstate auth_tests (#696)
J0 Sep 23, 2022
5b6ed06
MFA: Remove dead code (#697)
J0 Sep 23, 2022
5688b50
Add Rate limiters for MFA (#698)
J0 Sep 26, 2022
9929b47
Rename err to terr (#723)
J0 Oct 3, 2022
b27b85c
fix: merge master into v1 (#726)
J0 Oct 4, 2022
ef4891f
refactor: replace mfa/constants.go for enum (#727)
J0 Oct 4, 2022
b3963f8
refactor: add sessions refactors (#728)
J0 Oct 4, 2022
b51826a
refactor: add mfa_test.go refactors (#729)
J0 Oct 4, 2022
0a6f508
refactor: MFA Refactors (#730)
J0 Oct 4, 2022
706afd7
refactor: update factors.go and getBodyBytes (#732)
J0 Oct 4, 2022
638dde1
refactor: refactor models/factor and block admin from modifying facto…
J0 Oct 5, 2022
438e503
fix: update admin tests to add negatives (#734)
J0 Oct 5, 2022
f90a1ed
fix: mfa verify should reuse existing session (#691)
J0 Oct 5, 2022
b4c5e06
fix: resolve merge conflicts
Oct 5, 2022
fd3cae5
chore: remove dead code
Oct 5, 2022
438bffb
refactor: add explicit struct tags(mfa) (#740)
J0 Oct 10, 2022
a64ce0d
fix: trigger token swap on MFA verify to ensure token is latest one (…
J0 Oct 10, 2022
73c49f6
chore: add feature flag (#737)
J0 Oct 10, 2022
5521df1
refactor: mfa without user in route, other tiny fixes (#743)
hf Oct 11, 2022
243364b
feat: add mfa indexes (#745)
J0 Oct 11, 2022
8f2e09d
fix: return mfa enabled in settings (#747)
kangmingtay Oct 12, 2022
5a8c3b4
refactor: better error messages for mfa (#751)
J0 Oct 17, 2022
a6a1874
refactor: remove requirement for code in order to unenroll (#753)
J0 Oct 17, 2022
204bb87
refactor: pluralize factors endpoint, use lowercase `totp` (#752)
hf Oct 18, 2022
2ad2737
Merge branch 'master' into mfa
J0 Oct 18, 2022
d061f9f
test: add sanity checks to ensure secret does not leak (#755)
J0 Oct 18, 2022
06ac877
refactor: sort amr claims as most-recent first (#756)
hf Oct 18, 2022
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
110 changes: 110 additions & 0 deletions api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ type AdminUserParams struct {
BanDuration string `json:"ban_duration"`
}

type adminUserUpdateFactorParams struct {
FriendlyName string `json:"friendly_name"`
FactorType string `json:"factor_type"`
}

type AdminListUsersResponse struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
Expand Down Expand Up @@ -56,6 +61,24 @@ func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context,
return withUser(ctx, u), nil
}

func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Context, error) {
factorID, err := uuid.FromString(chi.URLParam(r, "factor_id"))
if err != nil {
return nil, badRequestError("factor_id must be an UUID")
}

observability.LogEntrySetField(r, "factor_id", factorID)

f, err := models.FindFactorByFactorID(a.db, factorID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, notFoundError("Factor not found")
}
return nil, internalServerError("Database error loading factor").WithInternalError(err)
}
return withFactor(r.Context(), f), nil
}

func (a *API) getAdminParams(r *http.Request) (*AdminUserParams, error) {
params := AdminUserParams{}

Expand Down Expand Up @@ -378,3 +401,90 @@ func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error {

return sendJSON(w, http.StatusOK, map[string]interface{}{})
}

func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
factor := getFactor(ctx)

MFAEnabled, err := models.IsMFAEnabled(a.db, user)
if err != nil {
return err
} else if !MFAEnabled {
return forbiddenError("You do not have a verified factor enrolled")
}

err = a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{
"user_id": user.ID,
"factor_id": factor.ID,
}); terr != nil {
return terr
}
if terr := tx.Destroy(factor); terr != nil {
return internalServerError("Database error deleting factor").WithInternalError(terr)
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, factor)
}

func (a *API) adminUserGetFactors(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
factors, terr := models.FindFactorsByUser(a.db, user)
if terr != nil {
return terr
}
return sendJSON(w, http.StatusOK, factors)
}

// adminUserUpdate updates a single factor object
func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
factor := getFactor(ctx)
user := getUser(ctx)
adminUser := getAdminUser(ctx)
params := &adminUserUpdateFactorParams{}
body, err := getBodyBytes(r)
if err != nil {
return badRequestError("Could not read body").WithInternalError(err)
}

if err := json.Unmarshal(body, params); err != nil {
return badRequestError("Could not read factor update params: %v", err)
}

err = a.db.Transaction(func(tx *storage.Connection) error {
if params.FriendlyName != "" {
if terr := factor.UpdateFriendlyName(tx, params.FriendlyName); terr != nil {
return terr
}
}
if params.FactorType != "" {
if params.FactorType != models.TOTP {
return badRequestError("Factor Type not valid")
}
if terr := factor.UpdateFactorType(tx, params.FactorType); terr != nil {
return terr
}
}

if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UpdateFactorAction, "", map[string]interface{}{
"user_id": user.ID,
"factor_id": factor.ID,
"factor_type": factor.FactorType,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return err
}

return sendJSON(w, http.StatusOK, factor)
}
110 changes: 110 additions & 0 deletions api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,113 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() {
})
}
}

// TestAdminUserDeleteFactor tests API /admin/users/<user_id>/factor/<factor_id>/
func (ts *AdminTestSuite) TestAdminUserDeleteFactor() {
if !ts.API.config.MFA.Enabled {
return
}
u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")

f, err := models.NewFactor(u, "testSimpleName", models.TOTP, models.FactorStateVerified, "secretkey")

require.NoError(ts.T(), err, "Error creating test factor model")
require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")

// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), nil)

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))

ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)

_, err = models.FindFactorByFactorID(ts.API.db, f.ID)
require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error())

}

// TestAdminUserGetFactor tests API /admin/user/<user_id>/factors/
func (ts *AdminTestSuite) TestAdminUserGetFactors() {
if !ts.API.config.MFA.Enabled {
return
}
u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")

f, err := models.NewFactor(u, "testSimpleName", models.TOTP, models.FactorStateUnverified, "secretkey")
require.NoError(ts.T(), err, "Error creating test factor model")
require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")

// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s/factor/", u.ID), nil)

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))

ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
getFactorsResp := []*models.Factor{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&getFactorsResp))
require.Equal(ts.T(), getFactorsResp[0].Secret, nil)
}

func (ts *AdminTestSuite) TestAdminUserUpdateFactor() {
if !ts.API.config.MFA.Enabled {
return
}
u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")

f, err := models.NewFactor(u, "testSimpleName", models.TOTP, models.FactorStateUnverified, "secretkey")
require.NoError(ts.T(), err, "Error creating test factor model")
require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")

var cases = []struct {
Desc string
FactorData map[string]interface{}
ExpectedCode int
}{
{
Desc: "Update Factor friendly name",
FactorData: map[string]interface{}{
"friendly_name": "john",
},
ExpectedCode: http.StatusOK,
},
{
Desc: "Update factor: valid factor type",
FactorData: map[string]interface{}{
"friendly_name": "john",
"factor_type": models.TOTP,
},
ExpectedCode: http.StatusOK,
},
{
Desc: "Update factor: invalid factor",
FactorData: map[string]interface{}{
"factor_type": "invalid_factor",
},
ExpectedCode: http.StatusBadRequest,
},
}

// Initialize factor data
for _, c := range cases {
ts.Run(c.Desc, func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.FactorData))
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.ExpectedCode, w.Code)
})
}

}
32 changes: 31 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
r.With(sharedLimiter).Put("/", api.UserUpdate)
})

if api.config.MFA.Enabled {
r.With(api.requireAuthentication).Route("/factors", func(r *router) {
r.Post("/", api.EnrollFactor)
r.Route("/{factor_id}", func(r *router) {
r.Use(api.loadFactor)

r.With(api.limitHandler(
tollbooth.NewLimiter(api.config.MFA.RateLimitChallengeAndVerify/60, &limiter.ExpirableOptions{
DefaultExpirationTTL: time.Minute,
}).SetBurst(30))).Post("/verify", api.VerifyFactor)
r.With(api.limitHandler(
tollbooth.NewLimiter(api.config.MFA.RateLimitChallengeAndVerify/60, &limiter.ExpirableOptions{
DefaultExpirationTTL: time.Minute,
}).SetBurst(30))).Post("/challenge", api.ChallengeFactor)
r.Delete("/", api.UnenrollFactor)

})
})
}

r.Route("/admin", func(r *router) {
r.Use(api.requireAdminCredentials)

Expand All @@ -143,6 +163,16 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati

r.Route("/{user_id}", func(r *router) {
r.Use(api.loadUser)
if api.config.MFA.Enabled {
r.Route("/factors", func(r *router) {
r.Get("/", api.adminUserGetFactors)
r.Route("/{factor_id}", func(r *router) {
r.Use(api.loadFactor)
r.Delete("/", api.adminUserDeleteFactor)
r.Put("/", api.adminUserUpdateFactor)
})
})
}

r.Get("/", api.adminUserGet)
r.Put("/", api.adminUserUpdate)
Expand All @@ -156,7 +186,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati

corsHandler := cors.New(cors.Options{
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", audHeaderName, useCookieHeader},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Client-IP", "X-Client-Info", audHeaderName, useCookieHeader},
AllowCredentials: true,
})

Expand Down
8 changes: 7 additions & 1 deletion api/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string {

u.Role = "supabase_admin"

token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
var token string
if ts.Config.MFA.Enabled {
token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
} else {
token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)

}
require.NoError(ts.T(), err, "Error generating access token")

p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
Expand Down
8 changes: 7 additions & 1 deletion api/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)

s, err := models.CreateSession(ts.API.db, u)
var s *models.Session
if ts.Config.MFA.Enabled {
s, err = models.MFA_CreateSession(ts.API.db, u, &uuid.Nil)
} else {
s, err = models.CreateSession(ts.API.db, u)

}
require.NoError(ts.T(), err)

cases := []struct {
Expand Down
15 changes: 15 additions & 0 deletions api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
signatureKey = contextKey("signature")
externalProviderTypeKey = contextKey("external_provider_type")
userKey = contextKey("user")
factorKey = contextKey("factor")
sessionKey = contextKey("session")
externalReferrerKey = contextKey("external_referrer")
functionHooksKey = contextKey("function_hooks")
Expand Down Expand Up @@ -71,6 +72,11 @@ func withUser(ctx context.Context, u *models.User) context.Context {
return context.WithValue(ctx, userKey, u)
}

// with Factor adds the factor id to the context.
func withFactor(ctx context.Context, f *models.Factor) context.Context {
return context.WithValue(ctx, factorKey, f)
}

// getUser reads the user from the context.
func getUser(ctx context.Context) *models.User {
if ctx == nil {
Expand All @@ -83,6 +89,15 @@ func getUser(ctx context.Context) *models.User {
return obj.(*models.User)
}

// getFactor reads the factor id from the context
func getFactor(ctx context.Context) *models.Factor {
obj := ctx.Value(factorKey)
if obj == nil {
return nil
}
return obj.(*models.Factor)
}

// withSession adds the session to the context.
func withSession(ctx context.Context, s *models.Session) context.Context {
return context.WithValue(ctx, sessionKey, s)
Expand Down
6 changes: 5 additions & 1 deletion api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,12 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
}
}
}
if config.MFA.Enabled {
token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.OAuth, grantParams)
} else {
token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
}

token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
if terr != nil {
return oauthError("server_error", terr.Error())
}
Expand Down
7 changes: 6 additions & 1 deletion api/invite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string {

u.Role = "supabase_admin"

token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
var token string
if ts.API.config.MFA.Enabled {
token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
} else {
token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
}
require.NoError(ts.T(), err, "Error generating access token")

p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
Expand Down
Loading