Skip to content

Commit

Permalink
feat: add MFA support (disabled by default) (#736)
Browse files Browse the repository at this point in the history
* db: add initial schema

* fix: update migrations

* fix: add additional constraints

* fix: add default

* chore: remove whitespace

* chore: remove email as a type

* fix: prevent updated_at from being nullable

* chore: update comments

* feat:enable and disable mfa

* refactor: remove model files

* fix: pull in master

* refactor: rename backup codes to recovery codes

* fix: update number of user fields

* Update signup_test.go

* test: disable and disable

* feat: add recovery codes api

* feat: add initial modles for factor

* feat: initial enroll endpoint

* feat: initial commit for challenge API

* refactor: add logic for filtering between factorid and simple name

* refactor: split based on factor simple name or id

* refactor: make endpoints idempotent

* chore: undo whitespace change

* chore: remove whitespace

* chore: naming and initial tests

* test: add model test for good measure

* feat: add find factor methods

* fix: change method definitions

* initial commit for verify factor

* test: add tests, remove factor_ prefix

* tests: add http test

* feat:initial verify endpoint

* tests: add more tests

* refactor: remove whitespace changes and minor labels

* fix: update db schema

* refactor: add states for factor_status

* fix: update statuses

* fix: add semicolon

* refactor: change bools to timestamps

* refactor: drop _mfa suffix

* refactor: change recovery code ID type to uuid

* fix: set length of token back to 8

* refactor: revert code consumption time to used_at

* test: add test for FindValidRecoveryCodesByUser

* refactor: convert var names to lowercase

* fix: add error check at end of GenerateRecoveryCodes

* chore: run gofmt -s -w

* refactor: update tests with new naming scheme

* fix: update tests with new names

* chore: renaming

* tests: refactor and add initial enroll factor tests

* fix: add associations from factor to user

* refactor: cleanup

* refactor: cleanup magic strings

* refactor: remove newlines

* chore: introduce new factor type

* chore: backpatch naming

* test: update verify factor test

* refactor: add struct tags

* refactor: add struct tas and change route names

* chore: reintroduce uncommented lines

* refactor: clean up tests

* chore: add misc config vars and errors

* tests: Add additional case for expried challenge + invalid code

* fix: patch test

* chore: add newlines

* chore: remove stray comment

* refactor: delete challenge if expired

* feat: remove unused fields, change recovery_codes from GET to POST

* refactor: remove type field from endpoint

* chore: speed up tests (#564)

On a not-so-good laptop, a full test run went from ~90seconds to less
than 10. Three independent changes were made:

1 - In TruncateAll, use `delete from` instead of `truncate`. While
  truncate is faster for large tables, for small tables, it has
considerably more overhead (my understanding is that delete just flags
the tuple as dead, whereas truncate involves a vacuum-like operation).

2 - In tests, use bcrypt.MinCost instead of bcrypt.DefaultCost

3 - Use a 10 millisecond timeout, instead of 1 second timeout, for
TestHookTimeout

* feat: add admin delete methods

* feat: add unenroll endpoint

* Revert "feat: add admin delete methods"

This reverts commit c65c136.

* fix: change behavior of unenroll

* refactor: strip out /enable and /disable

* Revert "feat: add admin delete methods"

This reverts commit c65c136.

* refactr: move routes to /user

* refactor: modify routes

* chore: remove stray comment

* chore: increase number of requests made so expected error is observed

* Revert "chore: increase number of requests made so expected error is observed"

This reverts commit 5374da8.

* chore: refactor MFA unenroll route

* chore: add updated routes

* feat: admin endpoints

* fix: refactor and update audit log action types

* tests: add additional checks for deletion endpoints

* refactor: remove stray constants

* refactor: remove stray lines and comments

* refactor: remove notion of MFAEnabled

* refactor: remove /recovery_codes endpoint

* feat: add update factor admin endpoint

* feat: add IsMFAEnabled

* refactor: reinstate MFAEnabled

* refactor: make issuer mandatory

* chore: remove created_at field from /challenge

* chore: merge in master

* Add MFA Sessions Fields (#643)

* feat: add session fields

* fix: handle terr

* fix: change column name

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: move constants into models (#646)

* test: check for unverified case

* refactor: move constants into models layer

* chore: test semantic release

* chore: update comment

Co-authored-by: joel@joellee.org <joel@joellee.org>

* feat: Convert QR to SVG (#624)

* initial commit

* chore:add qrcodesize param

* refactor: strip our QRCodeSize, undo session changes

Co-authored-by: joel@joellee.org <joel@joellee.org>

* fix: change method of reading from config

* refactor: remove unused var

* fix: patch gosec errors

* feat: cherry-pick step up changes onto separate branch (#652)

* refactor: cherry pick onto separate branch

* fix: update recovery code files

* feat: cherry-pick partial changes from stepup login branch

* Update models/recovery_code.go

Co-authored-by: joel@joellee.org <joel@joellee.org>

* feat: initial gating of routes requiring 1FA

* chore: add AMR entry

* refactor: update claims

* test: add initial mfa login tests

* refactor: Add sign in method to issueRefreshToken

* fix: distinguish between code logins and recovery code logins

* fix: add concept of first MFA login

* chore: add AMRClaims model content

* fix: patch various errors related to method signature

* fix: types for audit action logging

* fix: patch sql syntax errors

* chore: patch tests for stepup login

* fix: update number of user fields

* refactor: modify access token generation

* fix: step up login

* fix: get tests to pass

* fix: patch gosec errors

* chore: pull in master into mfa (#660)

* refactor: `TruncateAll` for better readability (#650)

* Remove unused GenerateEmailOtp function (#655)

* Remove unused function

* chore: remove helper function

* Update crypto.go

* refactor: configuration with validation (#648)

* feat: use proper ip address (#649)

* refactor: add `GrantParams` for issuing refresh tokens (#659)

* fix: remove instance.go

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>
Co-authored-by: joel@joellee.org <joel@joellee.org>

* tests: add notion of first log in

* feat: add session logic

* refactor: remove stepup login

* feat: remove stepup login related details

* refactor: remove unused fields

* chore: remove forgotten file

* chore: merge in master

* fix: remove recovery code logic

* refactor: change types to text, remove dead code

* fix: change verify test expected status code

* refactor: convert challenge_id and factor_id to be uuid's

* refactor: remove GetFactor

* refactor: downcase sql files, shift constants back to respective files

* refactor: remove require1FA

* refactor: modify aal levels to be enum

* refactor: reinstate notion of factor type

* refactor:change types of enroll/unenroll from string to bool

* test: add missing require checks

* fix: add tests for removing factor related sessions on unenroll

* fix: update db sessions in issueRefreshToken as well

* fix: convert issuer to default to siteURL

* refactor: change enroll factor message and remove redundant requireAuthentication check

* refactor: remove success field in verifyFactor Response

* fix: remove success code reutrned and allow unverified factors to be unenrolled

* refactor: add IP address type

* refactor: add IP address type

* fix: return token in mfa verify & update session aal (#682)

* fix: add default value for challenge IP

* fix: patch staticcheck errors

* fix: add partial index to allow multiple empty strings

* refactor: remove FindChallengesByFactorID and add IP address to challenge

* fix: add IP address checks

* fix: minor mfa changes (#692)

* refactor enroll

* fix: omit user, user_id & empty friendly_name from response

* Reinstate auth_tests (#696)

* fix: remove belongs to association on factor

* fix: extract issueRefreshToken logic for MFA

* test:reinstate tests

* fix: revert changes which introduce updateMFASessionAndClaims

* Revert "fix: remove belongs to association on factor"

This reverts commit 10057f7.

Co-authored-by: joel@joellee.org <joel@joellee.org>

* MFA: Remove dead code (#697)

* fix: remove belongs to association on factor

* fix: extract issueRefreshToken logic for MFA

* test:reinstate tests

* fix: revert changes which introduce updateMFASessionAndClaims

* Revert "fix: remove belongs to association on factor"

This reverts commit 10057f7.

* fix: resolve staticcheck errors, remove unused code

Co-authored-by: joel@joellee.org <joel@joellee.org>

* Add Rate limiters for MFA (#698)

* fix: add rate limiters

* chore: add conf

* chore: run gofmt

* fix: add max enrolled factors

* refactor: remove requireAuthentication

* fix: merge challenge and verify rate limits

Co-authored-by: joel@joellee.org <joel@joellee.org>

* Rename err to terr (#723)

* fix: remove belongs to association on factor

* fix: extract issueRefreshToken logic for MFA

* refactor: lowercase totp and rename totp_secret to secret to be more generic

* refactor: remove disabled state

* refactor: rename err -> terr

* Revert "refactor: remove disabled state"

This reverts commit bce773d.

* Revert "refactor: lowercase totp and rename totp_secret to secret to be more generic"

This reverts commit 2b6920f.

* Revert "fix: extract issueRefreshToken logic for MFA"

This reverts commit 2dbd47c.

* Revert "fix: remove belongs to association on factor"

This reverts commit 10057f7.

Co-authored-by: joel@joellee.org <joel@joellee.org>

* fix: merge master into v1 (#726)

* patch: merge master into v1

* chore: run gofmt

* refactor: rename TOTPSecret->Secret

* fix: adjust application code to align with db schema

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: replace mfa/constants.go for enum (#727)

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: add sessions refactors (#728)

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: add mfa_test.go refactors (#729)

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: MFA Refactors (#730)

* inital fixes

* Update api/mfa.go

* fix: rename FactorStates

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: update factors.go and getBodyBytes  (#732)

* refactor: change json.NewDecoder -> getBodyBytes

* refactor: update admin to use getBodyBytes

* fix: update comments

* fix: patch error message

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: refactor models/factor and block admin from modifying factor status (#733)

* refactor: change json.NewDecoder -> getBodyBytes

* refactor: update admin to use getBodyBytes

* fix: update comments

* fix: patch error message

* refactor: remove FindFactorByFriendlyName

* chore: update error messages

* fix: refactor unenroll test

* fix: patch staticcheck

* fix: patch stray staticcheck error

* refactor: add test case for unverified factor

* fix: correct error naming and adjust error messages

Co-authored-by: joel@joellee.org <joel@joellee.org>

* fix: update admin tests to add negatives (#734)

Co-authored-by: joel@joellee.org <joel@joellee.org>

* fix: mfa verify should reuse existing session (#691)

* fix: remove belongs to association on factor

* fix: extract issueRefreshToken logic for MFA

* chore: run gofmt

* fix: downgrade other sessions instead of deleting them

* refactor: convert all aal hardcoded strings to use enum

* fix: update AAL Calcuation to not duplicate claims

* fix: reinstate auth_test

* fix: move downgrade to be a method on factor struct

* refactor: merge factor and AAL update methods

* refactor: change mfa_constants to enum

* refactor: change signInMethod to Enum

* refactor: change hardcoded strings to test constants

* refactor: rename secondary session variables

* test: add check that unenrolling clears factorID from assoc session

* refactor: remove unused structs

* refactor: nit changes (#694)

* refactor: validate totp only if challenge isn't expired

* refactor: better error handling

* refactor: read from DB session instead of JWT token

* refactor: remove outdated test

* test: add tests for calculate AAL and AMR

* fix: add test to ensure claims are not duplicated

* fix: add ordering condition

* refactor: rename signInMethod to authenticationMethod

* fix: change verify bad code return from 401->400

* chore: run gofmt

* fix: re-read association from session

* refactor: change methods using enum types to AuthenticationMethod

* fix: change more functions to take in enum

* test: add integration tests

* test: add test to check AAL maintainance

* fix: add test for refresh token rotation

* chore: fix staticcheck

* refactor: move sessionID calculation out from transaction

* refactor: remove authentication method map

* refactor: remove additional CalculateAALAndAMR

* fix: remove requirement for tokens to be not revoked

* Apply suggestions from code review

Co-authored-by: Kang Ming <kang.ming1996@gmail.com>

* fix: gofmt, refactor return type for enrollfactor

* refactor: add comments to clarify tests, remove unused codew

* fix: add index to refresh_token

* refactor: minor renaming and add check on qrcode contents

* fix: replace expires in to default expiry

* fix: update unverified factor test

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Kang Ming <kang.ming1996@gmail.com>

* chore: remove dead code

* refactor: add explicit struct tags(mfa) (#740)

* refactor: explicitly name structs

* refactor: add explicit tags to user and phone

* refactor: add admin related tags

Co-authored-by: joel@joellee.org <joel@joellee.org>

* fix: trigger token swap on MFA verify to ensure token is latest one (#742)

* fix: trigger token swap to ensure token is latest one

* test: add additional test to check 2FA followed by 1FA sign in

* Update api/token.go

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

* Revert "Update api/token.go"

This reverts commit f6aa576.

* refactor: reduce number of params needed

* refactor: move FindSessionByUserID to tests

* Revert "refactor: move FindSessionByUserID to tests"

This reverts commit 3b56457.

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

* chore: add feature flag (#737)

* fix: add initial flags on tests

* fix: add MFA_ENABLED flag to tests

* fix: checks in test admin

* fix: add enabled flag

* fix: properly make use of config

* fix: remove stray env var addition

* feat: initial prefixing

* fix: prefix mfa related tests

* Update models/refresh_token.go

Co-authored-by: Kang Ming <kang.ming1996@gmail.com>

* fix: update comparison checks at

* chore: MFA_ENABLED->GOTRUE_MFA_ENABLED

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Kang Ming <kang.ming1996@gmail.com>

* refactor: mfa without user in route, other tiny fixes (#743)

* refactor: add unenroll factor response

* refactor: remove /user/<prefix> route for mfa

* feat: add mfa indexes (#745)

* feat: add mfa indexes

* Update models/amr.go

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

* Update migrations/20221011041400_add_mfa_indexes.up.sql

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

* fix: return mfa enabled in settings (#747)

* refactor: better error messages for mfa (#751)

* refactor: rename errors

* fix: add aal2 check

* refactor: simplify amr claims

* fix: add additional test for unenrolling verified factors

* Apply suggestions from code review

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

* refactor: remove requirement for code in order to unenroll (#753)

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: pluralize factors endpoint, use lowercase `totp` (#752)

* test: add sanity checks to ensure secret does not leak (#755)

* refactor: remove requirement for code in order to unenroll

* test: add sanity checks for secret leakage

* fix: update status codes for unenroll factor

Co-authored-by: joel@joellee.org <joel@joellee.org>

* refactor: sort amr claims as most-recent first (#756)

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>
Co-authored-by: Kang Ming <kang.ming1996@gmail.com>
  • Loading branch information
5 people committed Oct 18, 2022
1 parent 85cff37 commit 940f582
Show file tree
Hide file tree
Showing 38 changed files with 2,043 additions and 58 deletions.
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

0 comments on commit 940f582

Please sign in to comment.