Skip to content

Commit

Permalink
fix: update aal requirements to update user (#1766)
Browse files Browse the repository at this point in the history
## What kind of change does this PR introduce?

If a user has verified factors (mfa enabled) we should require an AAL2
session in order to proceed with any operation

We restrict phone, email, and password from updates as we consider those
as sensitive fields

Context:
https://supabase.slack.com/archives/C02AK9166FR/p1725466764804889

---------

Co-authored-by: Stojan Dimitrovski <[email protected]>
  • Loading branch information
J0 and hf committed Sep 17, 2024
1 parent 77d5897 commit 25d9874
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 0 deletions.
62 changes: 62 additions & 0 deletions internal/api/mfa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,68 @@ func (ts *MFATestSuite) TestDuplicateTOTPEnrollsReturnExpectedMessage() {
require.Contains(ts.T(), errorResponse.Message, expectedErrorMessage)
}

func (ts *MFATestSuite) AAL2RequiredToUpdatePasswordAfterEnrollment() {
resp := performTestSignupAndVerify(ts, ts.TestEmail, ts.TestPassword, true /* <- requireStatusOK */)
accessTokenResp := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(resp.Body).Decode(&accessTokenResp))

var w *httptest.ResponseRecorder
var buffer bytes.Buffer
token := accessTokenResp.Token
// Update Password to new password
newPassword := "newpass"
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"password": newPassword,
}))

req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer)
req.Header.Set("Content-Type", "application/json")

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

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

// Logout
reqURL := "http://localhost/logout"
req = httptest.NewRequest(http.MethodPost, reqURL, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
w = httptest.NewRecorder()

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

// Get AAL1 token
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": ts.TestEmail,
"password": newPassword,
}))

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)
require.Equal(ts.T(), http.StatusOK, w.Code)
session1 := AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&session1))

// Update Password again, this should fail
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"password": ts.TestPassword,
}))

req = httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer)
req.Header.Set("Content-Type", "application/json")

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", session1.Token))

w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusUnauthorized, w.Code)

}

func (ts *MFATestSuite) TestMultipleEnrollsCleanupExpiredFactors() {
// All factors are deleted when a subsequent enroll is made
ts.API.config.MFA.FactorExpiryDuration = 0 * time.Second
Expand Down
6 changes: 6 additions & 0 deletions internal/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error {
}
}

if user.HasMFAEnabled() && !session.IsAAL2() {
if (params.Password != nil && *params.Password != "") || (params.Email != "" && user.GetEmail() != params.Email) || (params.Phone != "" && user.GetPhone() != params.Phone) {
return httpError(http.StatusUnauthorized, ErrorCodeInsufficientAAL, "AAL2 session is required to update email or password when MFA is enabled.")
}
}

if user.IsAnonymous {
if params.Password != nil && *params.Password != "" {
if params.Email == "" && params.Phone == "" {
Expand Down
10 changes: 10 additions & 0 deletions internal/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,16 @@ func (u *User) IsBanned() bool {
return time.Now().Before(*u.BannedUntil)
}

func (u *User) HasMFAEnabled() bool {
for _, factor := range u.Factors {
if factor.IsVerified() {
return true
}
}

return false
}

func (u *User) UpdateBannedUntil(tx *storage.Connection) error {
return tx.UpdateOnly(u, "banned_until")
}
Expand Down

0 comments on commit 25d9874

Please sign in to comment.