Skip to content

Commit

Permalink
Merge branch 'master' into j0/remove_unneeded_helper_method
Browse files Browse the repository at this point in the history
  • Loading branch information
J0 authored Mar 4, 2024
2 parents f470937 + 656474e commit 7864093
Show file tree
Hide file tree
Showing 45 changed files with 768 additions and 470 deletions.
2 changes: 2 additions & 0 deletions hack/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_FLOW_STATE_EXPIRY_DURATION="300s"
GOTRUE_RATE_LIMIT_VERIFY="100000"
GOTRUE_RATE_LIMIT_TOKEN_REFRESH="30"
GOTRUE_RATE_LIMIT_ANONYMOUS_USERS="5"
GOTRUE_RATE_LIMIT_HEADER="My-Custom-Header"
GOTRUE_TRACING_ENABLED=true
GOTRUE_TRACING_EXPORTER=default
GOTRUE_TRACING_HOST=127.0.0.1
Expand Down
25 changes: 7 additions & 18 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,12 @@ func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Contex
}

func (a *API) getAdminParams(r *http.Request) (*AdminUserParams, error) {
params := AdminUserParams{}

body, err := getBodyBytes(r)
if err != nil {
return nil, badRequestError("Could not read body").WithInternalError(err)
params := &AdminUserParams{}
if err := retrieveRequestParams(r, params); err != nil {
return nil, err
}

if err := json.Unmarshal(body, &params); err != nil {
return nil, badRequestError("Could not decode admin user params: %v", err)
}

return &params, nil
return params, nil
}

// adminUsers responds with a list of all users in a given audience
Expand Down Expand Up @@ -565,16 +559,11 @@ func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) erro
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)
if err := retrieveRequestParams(r, params); err != nil {
return err
}

err = a.db.Transaction(func(tx *storage.Connection) error {
err := a.db.Transaction(func(tx *storage.Connection) error {
if params.FriendlyName != "" {
if terr := factor.UpdateFriendlyName(tx, params.FriendlyName); terr != nil {
return terr
Expand Down
2 changes: 1 addition & 1 deletion internal/api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ func (ts *AdminTestSuite) TestAdminUserDelete() {
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
u, err := signupParams.ToUserModel(false /* <- isSSOUser */)
require.NoError(ts.T(), err)
u, err = ts.API.signupNewUser(context.Background(), ts.API.db, u)
u, err = ts.API.signupNewUser(ts.API.db, u)
require.NoError(ts.T(), err)

// Setup request
Expand Down
58 changes: 58 additions & 0 deletions internal/api/anonymous.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package api

import (
"net/http"

"github.com/supabase/auth/internal/metering"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)

func (a *API) SignupAnonymously(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.config
db := a.db.WithContext(ctx)
aud := a.requestAud(ctx, r)

if config.DisableSignup {
return forbiddenError("Signups not allowed for this instance")
}

params := &SignupParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
params.Aud = aud
params.Provider = "anonymous"

newUser, err := params.ToUserModel(false /* <- isSSOUser */)
if err != nil {
return err
}

var grantParams models.GrantParams
grantParams.FillGrantParams(r)

var token *AccessTokenResponse
err = db.Transaction(func(tx *storage.Connection) error {
var terr error
newUser, terr = a.signupNewUser(tx, newUser)
if terr != nil {
return terr
}
token, terr = a.issueRefreshToken(ctx, tx, newUser, models.Anonymous, grantParams)
if terr != nil {
return terr
}
if terr := a.setCookieTokens(config, token, false, w); terr != nil {
return terr
}
return nil
})
if err != nil {
return internalServerError("Database error creating anonymous user").WithInternalError(err)
}

metering.RecordLogin("anonymous", newUser.ID)
return sendJSON(w, http.StatusOK, token)
}
177 changes: 177 additions & 0 deletions internal/api/anonymous_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package api

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"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
// 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(map[string]interface{}{
"email": "test@example.com",
}))

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)

// Verify email change
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"token_hash": user.EmailChangeTokenNew,
"type": "email_change",
}))

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.Equal(ts.T(), "test@example.com", data.User.GetEmail())
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "email", "providers": []interface{}{"email"}}), data.User.AppMetaData)
assert.False(ts.T(), data.User.IsAnonymous)
assert.NotEmpty(ts.T(), data.User.EmailConfirmedAt)

// User should have an email identity
assert.Len(ts.T(), data.User.Identities, 1)
}

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)
}
28 changes: 25 additions & 3 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func NewAPI(globalConfig *conf.GlobalConfiguration, db *storage.Connection) *API
return NewAPIWithVersion(context.Background(), globalConfig, db, defaultVersion)
}

func (a *API) deprecationNotices(ctx context.Context) {
func (a *API) deprecationNotices() {
config := a.config

log := logrus.WithField("component", "api")
Expand Down Expand Up @@ -92,7 +92,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
}
}

api.deprecationNotices(ctx)
api.deprecationNotices()

xffmw, _ := xff.Default()
logger := observability.NewStructuredLogger(logrus.StandardLogger(), globalConfig)
Expand Down Expand Up @@ -143,7 +143,28 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati

sharedLimiter := api.limitEmailOrPhoneSentHandler()
r.With(sharedLimiter).With(api.requireAdminCredentials).Post("/invite", api.Invite)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/signup", api.Signup)
r.With(sharedLimiter).With(api.verifyCaptcha).Route("/signup", func(r *router) {
// rate limit per hour
limiter := tollbooth.NewLimiter(api.config.RateLimitAnonymousUsers/(60*60), &limiter.ExpirableOptions{
DefaultExpirationTTL: time.Hour,
}).SetBurst(int(api.config.RateLimitAnonymousUsers)).SetMethods([]string{"POST"})
r.Post("/", func(w http.ResponseWriter, r *http.Request) error {
params := &SignupParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
if params.Email == "" && params.Phone == "" {
if !api.config.External.AnonymousUsers.Enabled {
return unprocessableEntityError("Anonymous sign-ins are disabled")
}
if _, err := api.limitHandler(limiter)(w, r); err != nil {
return err
}
return api.SignupAnonymously(w, r)
}
return api.Signup(w, r)
})
})
r.With(sharedLimiter).With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/resend", api.Resend)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
Expand Down Expand Up @@ -185,6 +206,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
})

r.With(api.requireAuthentication).Route("/factors", func(r *router) {
r.Use(api.requireNotAnonymous)
r.Post("/", api.EnrollFactor)
r.Route("/{factor_id}", func(r *router) {
r.Use(api.loadFactor)
Expand Down
11 changes: 10 additions & 1 deletion internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ func (a *API) requireAuthentication(w http.ResponseWriter, r *http.Request) (con
return ctx, err
}

func (a *API) requireAdmin(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, error) {
func (a *API) requireNotAnonymous(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
claims := getClaims(ctx)
if claims.IsAnonymous {
return nil, forbiddenError("Anonymous user not allowed to perform these actions")
}
return ctx, nil
}

func (a *API) requireAdmin(ctx context.Context, r *http.Request) (context.Context, error) {
// Find the administrative user
claims := getClaims(ctx)
if claims == nil {
Expand Down
Loading

0 comments on commit 7864093

Please sign in to comment.