330 lines
10 KiB
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)
|
|
})
|
|
}
|
|
}
|