chatdesk-ui/auth_v2.169.0/internal/api/user_test.go

559 lines
19 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/models"
)
type UserTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
}
func TestUser(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &UserTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *UserTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
// Create user
u, err := models.NewUser("123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
}
func (ts *UserTestSuite) generateToken(user *models.User, sessionId *uuid.UUID) string {
req := httptest.NewRequest(http.MethodPost, "/token?grant_type=password", nil)
token, _, err := ts.API.generateAccessToken(req, ts.API.db, user, sessionId, models.PasswordGrant)
require.NoError(ts.T(), err, "Error generating access token")
return token
}
func (ts *UserTestSuite) generateAccessTokenAndSession(user *models.User) string {
session, err := models.NewSession(user.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(session))
req := httptest.NewRequest(http.MethodPost, "/token?grant_type=password", nil)
token, _, err := ts.API.generateAccessToken(req, ts.API.db, user, &session.ID, models.PasswordGrant)
require.NoError(ts.T(), err, "Error generating access token")
return token
}
func (ts *UserTestSuite) TestUserGet() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err, "Error finding user")
token := ts.generateAccessTokenAndSession(u)
require.NoError(ts.T(), err, "Error generating access token")
req := httptest.NewRequest(http.MethodGet, "http://localhost/user", nil)
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)
}
func (ts *UserTestSuite) TestUserUpdateEmail() {
cases := []struct {
desc string
userData map[string]interface{}
isSecureEmailChangeEnabled bool
isMailerAutoconfirmEnabled bool
expectedCode int
}{
{
desc: "User doesn't have an existing email",
userData: map[string]interface{}{
"email": "",
"phone": "",
},
isSecureEmailChangeEnabled: false,
isMailerAutoconfirmEnabled: false,
expectedCode: http.StatusOK,
},
{
desc: "User doesn't have an existing email and double email confirmation required",
userData: map[string]interface{}{
"email": "",
"phone": "234567890",
},
isSecureEmailChangeEnabled: true,
isMailerAutoconfirmEnabled: false,
expectedCode: http.StatusOK,
},
{
desc: "User has an existing email",
userData: map[string]interface{}{
"email": "foo@example.com",
"phone": "",
},
isSecureEmailChangeEnabled: false,
isMailerAutoconfirmEnabled: false,
expectedCode: http.StatusOK,
},
{
desc: "User has an existing email and double email confirmation required",
userData: map[string]interface{}{
"email": "bar@example.com",
"phone": "",
},
isSecureEmailChangeEnabled: true,
isMailerAutoconfirmEnabled: false,
expectedCode: http.StatusOK,
},
{
desc: "Update email with mailer autoconfirm enabled",
userData: map[string]interface{}{
"email": "bar@example.com",
"phone": "",
},
isSecureEmailChangeEnabled: true,
isMailerAutoconfirmEnabled: true,
expectedCode: http.StatusOK,
},
{
desc: "Update email with mailer autoconfirm enabled and anonymous user",
userData: map[string]interface{}{
"email": "bar@example.com",
"phone": "",
"is_anonymous": true,
},
isSecureEmailChangeEnabled: true,
isMailerAutoconfirmEnabled: true,
expectedCode: http.StatusOK,
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
u, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), u.SetEmail(ts.API.db, c.userData["email"].(string)), "Error setting user email")
require.NoError(ts.T(), u.SetPhone(ts.API.db, c.userData["phone"].(string)), "Error setting user phone")
if isAnonymous, ok := c.userData["is_anonymous"]; ok {
u.IsAnonymous = isAnonymous.(bool)
}
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user")
token := ts.generateAccessTokenAndSession(u)
require.NoError(ts.T(), err, "Error generating access token")
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": "new@example.com",
}))
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.Config.Mailer.SecureEmailChangeEnabled = c.isSecureEmailChangeEnabled
ts.Config.Mailer.Autoconfirm = c.isMailerAutoconfirmEnabled
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.expectedCode, w.Code)
var data models.User
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
if c.isMailerAutoconfirmEnabled && u.IsAnonymous {
require.Empty(ts.T(), data.EmailChange)
require.Equal(ts.T(), "new@example.com", data.GetEmail())
require.Len(ts.T(), data.Identities, 1)
} else {
require.Equal(ts.T(), "new@example.com", data.EmailChange)
require.Len(ts.T(), data.Identities, 0)
}
// remove user after each case
require.NoError(ts.T(), ts.API.db.Destroy(u))
})
}
}
func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
existingUser, err := models.NewUser("22222222", "", "", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(existingUser))
cases := []struct {
desc string
userData map[string]string
expectedCode int
}{
{
desc: "New phone number is the same as current phone number",
userData: map[string]string{
"phone": "123456789",
},
expectedCode: http.StatusOK,
},
{
desc: "New phone number exists already",
userData: map[string]string{
"phone": "22222222",
},
expectedCode: http.StatusUnprocessableEntity,
},
{
desc: "New phone number is different from current phone number",
userData: map[string]string{
"phone": "234567890",
},
expectedCode: http.StatusOK,
},
}
ts.Config.Sms.Autoconfirm = true
for _, c := range cases {
ts.Run(c.desc, func() {
token := ts.generateAccessTokenAndSession(u)
require.NoError(ts.T(), err, "Error generating access token")
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"phone": c.userData["phone"],
}))
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(), c.expectedCode, w.Code)
if c.expectedCode == http.StatusOK {
// check that the user response returned contains the updated phone field
data := &models.User{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Equal(ts.T(), data.GetPhone(), c.userData["phone"])
}
})
}
}
func (ts *UserTestSuite) TestUserUpdatePassword() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
r, err := models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{})
require.NoError(ts.T(), err)
r2, err := models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{})
require.NoError(ts.T(), err)
// create a session and modify it's created_at time to simulate a session that is not recently logged in
notRecentlyLoggedIn, err := models.FindSessionByID(ts.API.db, *r2.SessionId, true)
require.NoError(ts.T(), err)
// cannot use Update here because Update doesn't removes the created_at field
require.NoError(ts.T(), ts.API.db.RawQuery(
"update "+notRecentlyLoggedIn.TableName()+" set created_at = ? where id = ?",
time.Now().Add(-24*time.Hour),
notRecentlyLoggedIn.ID).Exec(),
)
type expected struct {
code int
isAuthenticated bool
}
var cases = []struct {
desc string
newPassword string
nonce string
requireReauthentication bool
sessionId *uuid.UUID
expected expected
}{
{
desc: "Need reauthentication because outside of recently logged in window",
newPassword: "newpassword123",
nonce: "",
requireReauthentication: true,
sessionId: &notRecentlyLoggedIn.ID,
expected: expected{code: http.StatusBadRequest, isAuthenticated: false},
},
{
desc: "No nonce provided",
newPassword: "newpassword123",
nonce: "",
sessionId: &notRecentlyLoggedIn.ID,
requireReauthentication: true,
expected: expected{code: http.StatusBadRequest, isAuthenticated: false},
},
{
desc: "Invalid nonce",
newPassword: "newpassword1234",
nonce: "123456",
sessionId: &notRecentlyLoggedIn.ID,
requireReauthentication: true,
expected: expected{code: http.StatusUnprocessableEntity, isAuthenticated: false},
},
{
desc: "No need reauthentication because recently logged in",
newPassword: "newpassword123",
nonce: "",
requireReauthentication: true,
sessionId: r.SessionId,
expected: expected{code: http.StatusOK, isAuthenticated: true},
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
ts.Config.Security.UpdatePasswordRequireReauthentication = c.requireReauthentication
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"password": c.newPassword, "nonce": c.nonce}))
req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer)
req.Header.Set("Content-Type", "application/json")
token := ts.generateToken(u, c.sessionId)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
// Setup response recorder
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.expected.code, w.Code)
// Request body
u, err = models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
isAuthenticated, _, err := u.Authenticate(context.Background(), ts.API.db, c.newPassword, ts.API.config.Security.DBEncryption.DecryptionKeys, ts.API.config.Security.DBEncryption.Encrypt, ts.API.config.Security.DBEncryption.EncryptionKeyID)
require.NoError(ts.T(), err)
require.Equal(ts.T(), c.expected.isAuthenticated, isAuthenticated)
})
}
}
func (ts *UserTestSuite) TestUserUpdatePasswordNoReauthenticationRequired() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
type expected struct {
code int
isAuthenticated bool
}
var cases = []struct {
desc string
newPassword string
nonce string
requireReauthentication bool
expected expected
}{
{
desc: "Invalid password length",
newPassword: "",
nonce: "",
requireReauthentication: false,
expected: expected{code: http.StatusUnprocessableEntity, isAuthenticated: false},
},
{
desc: "Valid password length",
newPassword: "newpassword",
nonce: "",
requireReauthentication: false,
expected: expected{code: http.StatusOK, isAuthenticated: true},
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
ts.Config.Security.UpdatePasswordRequireReauthentication = c.requireReauthentication
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"password": c.newPassword, "nonce": c.nonce}))
req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer)
req.Header.Set("Content-Type", "application/json")
token := ts.generateAccessTokenAndSession(u)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
// Setup response recorder
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.expected.code, w.Code)
// Request body
u, err = models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
isAuthenticated, _, err := u.Authenticate(context.Background(), ts.API.db, c.newPassword, ts.API.config.Security.DBEncryption.DecryptionKeys, ts.API.config.Security.DBEncryption.Encrypt, ts.API.config.Security.DBEncryption.EncryptionKeyID)
require.NoError(ts.T(), err)
require.Equal(ts.T(), c.expected.isAuthenticated, isAuthenticated)
})
}
}
func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() {
ts.Config.Security.UpdatePasswordRequireReauthentication = true
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
// Confirm the test user
now := time.Now()
u.EmailConfirmedAt = &now
require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")
token := ts.generateAccessTokenAndSession(u)
// request for reauthentication nonce
req := httptest.NewRequest(http.MethodGet, "http://localhost/reauthenticate", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), w.Code, http.StatusOK)
u, err = models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), u.ReauthenticationToken)
require.NotEmpty(ts.T(), u.ReauthenticationSentAt)
// update reauthentication token to a known token
u.ReauthenticationToken = crypto.GenerateTokenHash(u.GetEmail(), "123456")
require.NoError(ts.T(), ts.API.db.Update(u))
// update password with reauthentication token
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"password": "newpass",
"nonce": "123456",
}))
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(), w.Code, http.StatusOK)
// Request body
u, err = models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
isAuthenticated, _, err := u.Authenticate(context.Background(), ts.API.db, "newpass", ts.Config.Security.DBEncryption.DecryptionKeys, ts.Config.Security.DBEncryption.Encrypt, ts.Config.Security.DBEncryption.EncryptionKeyID)
require.NoError(ts.T(), err)
require.True(ts.T(), isAuthenticated)
require.Empty(ts.T(), u.ReauthenticationToken)
require.Nil(ts.T(), u.ReauthenticationSentAt)
}
func (ts *UserTestSuite) TestUserUpdatePasswordLogoutOtherSessions() {
ts.Config.Security.UpdatePasswordRequireReauthentication = false
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
// Confirm the test user
now := time.Now()
u.EmailConfirmedAt = &now
require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")
// Login the test user to get first session
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": u.GetEmail(),
"password": "password",
}))
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))
// Login test user to get second session
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": u.GetEmail(),
"password": "password",
}))
req = httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=password", &buffer)
req.Header.Set("Content-Type", "application/json")
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
session2 := AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&session2))
// Update user's password using first session
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"password": "newpass",
}))
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.StatusOK, w.Code)
// Attempt to refresh session1 should pass
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"refresh_token": session1.RefreshToken,
}))
req = httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &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)
// Attempt to refresh session2 should fail
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"refresh_token": session2.RefreshToken,
}))
req = httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer)
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.NotEqual(ts.T(), http.StatusOK, w.Code)
}