chatai/auth_v2.169.0/internal/api/anonymous_test.go

330 lines
10 KiB
Go

package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
mail "github.com/supabase/auth/internal/mailer"
"github.com/supabase/auth/internal/models"
)
type AnonymousTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
}
func TestAnonymous(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &AnonymousTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *AnonymousTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
// Create anonymous user
params := &SignupParams{
Aud: ts.Config.JWT.Aud,
Provider: "anonymous",
}
u, err := params.ToUserModel(false)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new anonymous test user")
}
func (ts *AnonymousTestSuite) TestAnonymousLogins() {
ts.Config.External.AnonymousUsers.Enabled = true
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"data": map[string]interface{}{
"field": "foo",
},
}))
req := httptest.NewRequest(http.MethodPost, "/signup", &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)
data := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
assert.NotEmpty(ts.T(), data.User.ID)
assert.Equal(ts.T(), ts.Config.JWT.Aud, data.User.Aud)
assert.Empty(ts.T(), data.User.GetEmail())
assert.Empty(ts.T(), data.User.GetPhone())
assert.True(ts.T(), data.User.IsAnonymous)
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"field": "foo"}), data.User.UserMetaData)
}
func (ts *AnonymousTestSuite) TestConvertAnonymousUserToPermanent() {
ts.Config.External.AnonymousUsers.Enabled = true
ts.Config.Sms.TestOTP = map[string]string{"1234567890": "000000", "1234560000": "000000"}
// test OTPs still require setting up an sms provider
ts.Config.Sms.Provider = "twilio"
ts.Config.Sms.Twilio.AccountSid = "fake-sid"
ts.Config.Sms.Twilio.AuthToken = "fake-token"
ts.Config.Sms.Twilio.MessageServiceSid = "fake-message-service-sid"
cases := []struct {
desc string
body map[string]interface{}
verificationType string
}{
{
desc: "convert anonymous user to permanent user with email",
body: map[string]interface{}{
"email": "test@example.com",
},
verificationType: "email_change",
},
{
desc: "convert anonymous user to permanent user with phone",
body: map[string]interface{}{
"phone": "1234567890",
},
verificationType: "phone_change",
},
{
desc: "convert anonymous user to permanent user with email & password",
body: map[string]interface{}{
"email": "test2@example.com",
"password": "test-password",
},
verificationType: "email_change",
},
{
desc: "convert anonymous user to permanent user with phone & password",
body: map[string]interface{}{
"phone": "1234560000",
"password": "test-password",
},
verificationType: "phone_change",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req := httptest.NewRequest(http.MethodPost, "/signup", &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)
signupResponse := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&signupResponse))
// Add email to anonymous user
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
req = httptest.NewRequest(http.MethodPut, "/user", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signupResponse.Token))
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// Check if anonymous user is still anonymous
user, err := models.FindUserByID(ts.API.db, signupResponse.User.ID)
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), user)
require.True(ts.T(), user.IsAnonymous)
// Check if user has a password set
if c.body["password"] != nil {
require.True(ts.T(), user.HasPassword())
}
switch c.verificationType {
case mail.EmailChangeVerification:
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"token_hash": user.EmailChangeTokenNew,
"type": c.verificationType,
}))
case phoneChangeVerification:
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"phone": user.PhoneChange,
"token": "000000",
"type": c.verificationType,
}))
}
req = httptest.NewRequest(http.MethodPost, "/verify", &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)
data := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
// User is a permanent user and not anonymous anymore
assert.Equal(ts.T(), signupResponse.User.ID, data.User.ID)
assert.Equal(ts.T(), ts.Config.JWT.Aud, data.User.Aud)
assert.False(ts.T(), data.User.IsAnonymous)
// User should have an identity
assert.Len(ts.T(), data.User.Identities, 1)
switch c.verificationType {
case mail.EmailChangeVerification:
assert.Equal(ts.T(), c.body["email"], data.User.GetEmail())
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "email", "providers": []interface{}{"email"}}), data.User.AppMetaData)
assert.NotEmpty(ts.T(), data.User.EmailConfirmedAt)
case phoneChangeVerification:
assert.Equal(ts.T(), c.body["phone"], data.User.GetPhone())
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "phone", "providers": []interface{}{"phone"}}), data.User.AppMetaData)
assert.NotEmpty(ts.T(), data.User.PhoneConfirmedAt)
}
})
}
}
func (ts *AnonymousTestSuite) TestRateLimitAnonymousSignups() {
var buffer bytes.Buffer
ts.Config.External.AnonymousUsers.Enabled = true
// It rate limits after 30 requests
for i := 0; i < int(ts.Config.RateLimitAnonymousUsers); i++ {
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("My-Custom-Header", "1.2.3.4")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusOK, w.Code)
}
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("My-Custom-Header", "1.2.3.4")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusTooManyRequests, w.Code)
// It ignores X-Forwarded-For by default
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req.Header.Set("X-Forwarded-For", "1.1.1.1")
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusTooManyRequests, w.Code)
// It doesn't rate limit a new value for the limited header
req.Header.Set("My-Custom-Header", "5.6.7.8")
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusBadRequest, w.Code)
}
func (ts *AnonymousTestSuite) TestAdminUpdateAnonymousUser() {
claims := &AccessTokenClaims{
Role: "supabase_admin",
}
adminJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err)
u1, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err)
u1.IsAnonymous = true
require.NoError(ts.T(), ts.API.db.Create(u1))
u2, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err)
u2.IsAnonymous = true
require.NoError(ts.T(), ts.API.db.Create(u2))
cases := []struct {
desc string
userId uuid.UUID
body map[string]interface{}
expected map[string]interface{}
expectedIdentities int
}{
{
desc: "update anonymous user with email and email confirm true",
userId: u1.ID,
body: map[string]interface{}{
"email": "foo@example.com",
"email_confirm": true,
},
expected: map[string]interface{}{
"email": "foo@example.com",
"is_anonymous": false,
},
expectedIdentities: 1,
},
{
desc: "update anonymous user with email and email confirm false",
userId: u2.ID,
body: map[string]interface{}{
"email": "bar@example.com",
"email_confirm": false,
},
expected: map[string]interface{}{
"email": "bar@example.com",
"is_anonymous": true,
},
expectedIdentities: 1,
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s", c.userId), &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", adminJwt))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
var data models.User
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.NotNil(ts.T(), data)
require.Len(ts.T(), data.Identities, c.expectedIdentities)
actual := map[string]interface{}{
"email": data.GetEmail(),
"is_anonymous": data.IsAnonymous,
}
require.Equal(ts.T(), c.expected, actual)
})
}
}