Repository: supabase/gotrue
Pull request #1262 (2023-10-02 08:43:06)

feat: initial fix for invite followed by signup.
## What kind of change does this PR introduce?

Currently, sending an `invite` request followed by a `signup` request  w/o  confirmation between invite and signup will expose metadata on UserMetadata and Identities which may be perceived as a leak of sensitive information.

This PR aims to clear out such metadata for cases where the dev has been invited before a signup


## Testing Instructions
How to test locally:

Use this admin bearer `jwt` 

1. Call  http://localhost:9999/invite to `myemail@gmail.com`
2. Wait 60s 
3. Call http://localhost:9999/signup with `myemail@gmail.com` and check that `identities` field is blanked out


Patch:
diff --git a/internal/api/external.go b/internal/api/external.go
index 6d8bed8c..ca817ffb 100644
--- a/internal/api/external.go
+++ b/internal/api/external.go
@@ -528,6 +528,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide
 		return provider.NewFacebookProvider(config.External.Facebook, scopes)
 	case "figma":
 		return provider.NewFigmaProvider(config.External.Figma, scopes)
+	case "fly":
+		return provider.NewFlyProvider(config.External.Fly, scopes)
 	case "github":
 		return provider.NewGithubProvider(config.External.Github, scopes)
 	case "gitlab":
diff --git a/internal/api/phone.go b/internal/api/phone.go
index 3586ca78..4599e8f2 100644
--- a/internal/api/phone.go
+++ b/internal/api/phone.go
@@ -93,7 +93,7 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection,
 			return "", err
 		}
 
-		messageID, err = smsProvider.SendMessage(phone, message, channel, otp)
+		messageID, err = smsProvider.SendMessage(phone, message, channel)
 		if err != nil {
 			return messageID, err
 		}
diff --git a/internal/api/provider/fly.go b/internal/api/provider/fly.go
new file mode 100644
index 00000000..5c0f9aa0
--- /dev/null
+++ b/internal/api/provider/fly.go
@@ -0,0 +1,105 @@
+package provider
+
+import (
+	"context"
+	"errors"
+	"strings"
+
+	"github.com/supabase/gotrue/internal/conf"
+	"golang.org/x/oauth2"
+)
+
+const (
+	defaultFlyAPIBase = "api.fly.io"
+)
+
+type flyProvider struct {
+	*oauth2.Config
+	APIPath string
+}
+
+type flyUser struct {
+	ResourceOwnerID string `json:"resource_owner_id"`
+	UserID          string `json:"user_id"`
+	UserName        string `json:"user_name"`
+	Email           string `json:"email"`
+	Organizations   []struct {
+		ID   string `json:"id"`
+		Role string `json:"role"`
+	} `json:"organizations"`
+	Scope       []string          `json:"scope"`
+	Application map[string]string `json:"application"`
+	ExpiresIn   int               `json:"expires_in"`
+	CreatedAt   int               `json:"created_at"`
+}
+
+// NewFlyProvider creates a Fly oauth provider.
+func NewFlyProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
+	if err := ext.ValidateOAuth(); err != nil {
+		return nil, err
+	}
+
+	authHost := chooseHost(ext.URL, defaultFlyAPIBase)
+
+	// Fly only provides the "read" scope.
+	// https://fly.io/docs/reference/extensions_api/#single-sign-on-flow
+	oauthScopes := []string{
+		"read",
+	}
+
+	if scopes != "" {
+		oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
+	}
+
+	return &flyProvider{
+		Config: &oauth2.Config{
+			ClientID:     ext.ClientID[0],
+			ClientSecret: ext.Secret,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:  authHost + "/oauth/authorize",
+				TokenURL: authHost + "/oauth/token",
+			},
+			RedirectURL: ext.RedirectURI,
+			Scopes:      oauthScopes,
+		},
+		APIPath: authHost,
+	}, nil
+}
+
+func (p flyProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
+	return p.Exchange(context.Background(), code)
+}
+
+func (p flyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
+	var u flyUser
+	if err := makeRequest(ctx, tok, p.Config, p.APIPath+"/oauth/token/info", &u); err != nil {
+		return nil, err
+	}
+
+	if u.Email == "" {
+		return nil, errors.New("unable to find email with Fly provider")
+	}
+
+	return &UserProvidedData{
+		Metadata: &Claims{
+			Issuer:        p.APIPath,
+			Subject:       u.UserID,
+			FullName:      u.UserName,
+			Email:         u.Email,
+			EmailVerified: true,
+			ProviderId:    u.UserID,
+			CustomClaims: map[string]interface{}{
+				"resource_owner_id": u.ResourceOwnerID,
+				"organizations":     u.Organizations,
+				"application":       u.Application,
+				"scope":             u.Scope,
+				"created_at":        u.CreatedAt,
+			},
+		},
+		Emails: []Email{{
+			Email:    u.Email,
+			Verified: true,
+			Primary:  true,
+		}},
+	}, nil
+}
diff --git a/internal/api/settings.go b/internal/api/settings.go
index d9517d2b..c2ab0df3 100644
--- a/internal/api/settings.go
+++ b/internal/api/settings.go
@@ -9,6 +9,7 @@ type ProviderSettings struct {
 	Discord      bool `json:"discord"`
 	Facebook     bool `json:"facebook"`
 	Figma        bool `json:"figma"`
+	Fly          bool `json:"fly"`
 	GitHub       bool `json:"github"`
 	GitLab       bool `json:"gitlab"`
 	Google       bool `json:"google"`
@@ -48,6 +49,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
 			Discord:      config.External.Discord.Enabled,
 			Facebook:     config.External.Facebook.Enabled,
 			Figma:        config.External.Figma.Enabled,
+			Fly:          config.External.Fly.Enabled,
 			GitHub:       config.External.Github.Enabled,
 			GitLab:       config.External.Gitlab.Enabled,
 			Google:       config.External.Google.Enabled,
diff --git a/internal/api/signup.go b/internal/api/signup.go
index 9642fad1..efe30139 100644
--- a/internal/api/signup.go
+++ b/internal/api/signup.go
@@ -287,6 +287,11 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 		metering.RecordLogin("password", user.ID)
 		return sendJSON(w, http.StatusOK, token)
 	}
+	if user.HasBeenInvited() {
+		// Remove sensitive fields
+		user.UserMetaData = map[string]interface{}{}
+		user.Identities = []models.Identity{}
+	}
 	return sendJSON(w, http.StatusOK, user)
 }
 
diff --git a/internal/api/sms_provider/messagebird.go b/internal/api/sms_provider/messagebird.go
index d65ce472..0125b9d1 100644
--- a/internal/api/sms_provider/messagebird.go
+++ b/internal/api/sms_provider/messagebird.go
@@ -56,7 +56,7 @@ func NewMessagebirdProvider(config conf.MessagebirdProviderConfiguration) (SmsPr
 	}, nil
 }
 
-func (t *MessagebirdProvider) SendMessage(phone, message, channel, otp string) (string, error) {
+func (t *MessagebirdProvider) SendMessage(phone string, message string, channel string) (string, error) {
 	switch channel {
 	case SMSProvider:
 		return t.SendSms(phone, message)
diff --git a/internal/api/sms_provider/sms_provider.go b/internal/api/sms_provider/sms_provider.go
index 4f4a0e13..4337ae37 100644
--- a/internal/api/sms_provider/sms_provider.go
+++ b/internal/api/sms_provider/sms_provider.go
@@ -26,7 +26,7 @@ func init() {
 }
 
 type SmsProvider interface {
-	SendMessage(phone, message, channel, otp string) (string, error)
+	SendMessage(phone, message, channel string) (string, error)
 }
 
 func GetSmsProvider(config conf.GlobalConfiguration) (SmsProvider, error) {
diff --git a/internal/api/sms_provider/textlocal.go b/internal/api/sms_provider/textlocal.go
index f3d5e8de..78cff912 100644
--- a/internal/api/sms_provider/textlocal.go
+++ b/internal/api/sms_provider/textlocal.go
@@ -49,7 +49,7 @@ func NewTextlocalProvider(config conf.TextlocalProviderConfiguration) (SmsProvid
 	}, nil
 }
 
-func (t *TextlocalProvider) SendMessage(phone, message, channel, otp string) (string, error) {
+func (t *TextlocalProvider) SendMessage(phone string, message string, channel string) (string, error) {
 	switch channel {
 	case SMSProvider:
 		return t.SendSms(phone, message)
diff --git a/internal/api/sms_provider/twilio.go b/internal/api/sms_provider/twilio.go
index c9019746..ee20b65f 100644
--- a/internal/api/sms_provider/twilio.go
+++ b/internal/api/sms_provider/twilio.go
@@ -63,40 +63,30 @@ func NewTwilioProvider(config conf.TwilioProviderConfiguration) (SmsProvider, er
 	}, nil
 }
 
-func (t *TwilioProvider) SendMessage(phone, message, channel, otp string) (string, error) {
+func (t *TwilioProvider) SendMessage(phone string, message string, channel string) (string, error) {
 	switch channel {
 	case SMSProvider, WhatsappProvider:
-		return t.SendSms(phone, message, channel, otp)
+		return t.SendSms(phone, message, channel)
 	default:
 		return "", fmt.Errorf("channel type %q is not supported for Twilio", channel)
 	}
 }
 
 // Send an SMS containing the OTP with Twilio's API
-func (t *TwilioProvider) SendSms(phone, message, channel, otp string) (string, error) {
+func (t *TwilioProvider) SendSms(phone, message, channel string) (string, error) {
 	sender := t.Config.MessageServiceSid
 	receiver := "+" + phone
-	body := url.Values{
-		"To":      {receiver}, // twilio api requires "+" extension to be included
-		"Channel": {channel},
-		"From":    {sender},
-		"Body":    {message},
-	}
 	if channel == WhatsappProvider {
 		receiver = channel + ":" + receiver
 		if isPhoneNumber.MatchString(formatPhoneNumber(sender)) {
 			sender = channel + ":" + sender
 		}
-		// Used to substitute OTP. See https://www.twilio.com/docs/content/whatsappauthentication for more details
-		contentVariables := fmt.Sprintf(`{"1": "%s"}`, otp)
-		// Programmable Messaging (WhatsApp) takes in different set of inputs
-		body = url.Values{
-			"To":               {receiver}, // twilio api requires "+" extension to be included
-			"Channel":          {channel},
-			"From":             {sender},
-			"ContentSid":       {t.Config.ContentSid},
-			"ContentVariables": {contentVariables},
-		}
+	}
+	body := url.Values{
+		"To":      {receiver}, // twilio api requires "+" extension to be included
+		"Channel": {channel},
+		"From":    {sender},
+		"Body":    {message},
 	}
 	client := &http.Client{Timeout: defaultTimeout}
 	r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode()))
diff --git a/internal/api/sms_provider/twilio_verify.go b/internal/api/sms_provider/twilio_verify.go
index ba3c5ac9..0f9599ee 100644
--- a/internal/api/sms_provider/twilio_verify.go
+++ b/internal/api/sms_provider/twilio_verify.go
@@ -53,7 +53,7 @@ func NewTwilioVerifyProvider(config conf.TwilioVerifyProviderConfiguration) (Sms
 	}, nil
 }
 
-func (t *TwilioVerifyProvider) SendMessage(phone, message, channel, otp string) (string, error) {
+func (t *TwilioVerifyProvider) SendMessage(phone string, message string, channel string) (string, error) {
 	switch channel {
 	case SMSProvider, WhatsappProvider:
 		return t.SendSms(phone, message, channel)
diff --git a/internal/api/sms_provider/vonage.go b/internal/api/sms_provider/vonage.go
index fc82a3ac..a41bc0a1 100644
--- a/internal/api/sms_provider/vonage.go
+++ b/internal/api/sms_provider/vonage.go
@@ -45,7 +45,7 @@ func NewVonageProvider(config conf.VonageProviderConfiguration) (SmsProvider, er
 	}, nil
 }
 
-func (t *VonageProvider) SendMessage(phone, message, channel, otp string) (string, error) {
+func (t *VonageProvider) SendMessage(phone string, message string, channel string) (string, error) {
 	switch channel {
 	case SMSProvider:
 		return t.SendSms(phone, message)
diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go
index e43e7576..32fe877f 100644
--- a/internal/conf/configuration.go
+++ b/internal/conf/configuration.go
@@ -189,6 +189,7 @@ type ProviderConfiguration struct {
 	Discord                 OAuthProviderConfiguration `json:"discord"`
 	Facebook                OAuthProviderConfiguration `json:"facebook"`
 	Figma                   OAuthProviderConfiguration `json:"figma"`
+	Fly                     OAuthProviderConfiguration `json:"fly"`
 	Github                  OAuthProviderConfiguration `json:"github"`
 	Gitlab                  OAuthProviderConfiguration `json:"gitlab"`
 	Google                  OAuthProviderConfiguration `json:"google"`
@@ -270,7 +271,6 @@ type TwilioProviderConfiguration struct {
 	AccountSid        string `json:"account_sid" split_words:"true"`
 	AuthToken         string `json:"auth_token" split_words:"true"`
 	MessageServiceSid string `json:"message_service_sid" split_words:"true"`
-	ContentSid        string `json:"content_sid" split_words:"true"`
 }
 
 type TwilioVerifyProviderConfiguration struct {
diff --git a/internal/models/user.go b/internal/models/user.go
index 1ae44814..fabe7010 100644
--- a/internal/models/user.go
+++ b/internal/models/user.go
@@ -138,6 +138,11 @@ func (u *User) IsConfirmed() bool {
 	return u.EmailConfirmedAt != nil
 }
 
+// HasBeenInvited checks if user has been invited
+func (u *User) HasBeenInvited() bool {
+	return u.InvitedAt != nil
+}
+
 // IsPhoneConfirmed checks if a user's phone has already been
 // registered and confirmed.
 func (u *User) IsPhoneConfirmed() bool {


Test patch:
diff --git a/hack/test.env b/hack/test.env
index 51a487ca..1aad710f 100644
--- a/hack/test.env
+++ b/hack/test.env
@@ -35,6 +35,10 @@ GOTRUE_EXTERNAL_FACEBOOK_ENABLED=true
 GOTRUE_EXTERNAL_FACEBOOK_CLIENT_ID=testclientid
 GOTRUE_EXTERNAL_FACEBOOK_SECRET=testsecret
 GOTRUE_EXTERNAL_FACEBOOK_REDIRECT_URI=https://identity.services.netlify.com/callback
+GOTRUE_EXTERNAL_FLY_ENABLED=true
+GOTRUE_EXTERNAL_FLY_CLIENT_ID=testclientid
+GOTRUE_EXTERNAL_FLY_SECRET=testsecret
+GOTRUE_EXTERNAL_FLY_REDIRECT_URI=https://identity.services.netlify.com/callback
 GOTRUE_EXTERNAL_FIGMA_ENABLED=true
 GOTRUE_EXTERNAL_FIGMA_CLIENT_ID=testclientid
 GOTRUE_EXTERNAL_FIGMA_SECRET=testsecret
diff --git a/internal/api/external_fly_test.go b/internal/api/external_fly_test.go
new file mode 100644
index 00000000..90cf8997
--- /dev/null
+++ b/internal/api/external_fly_test.go
@@ -0,0 +1,264 @@
+package api
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"time"
+
+	jwt "github.com/golang-jwt/jwt"
+	"github.com/stretchr/testify/require"
+	"github.com/supabase/gotrue/internal/models"
+)
+
+func (ts *ExternalTestSuite) TestSignupExternalFly() {
+	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=fly", nil)
+	w := httptest.NewRecorder()
+	ts.API.handler.ServeHTTP(w, req)
+	ts.Require().Equal(http.StatusFound, w.Code)
+	u, err := url.Parse(w.Header().Get("Location"))
+	ts.Require().NoError(err, "redirect url parse failed")
+	q := u.Query()
+	ts.Equal(ts.Config.External.Fly.RedirectURI, q.Get("redirect_uri"))
+	ts.Equal(ts.Config.External.Fly.ClientID, []string{q.Get("client_id")})
+	ts.Equal("code", q.Get("response_type"))
+	ts.Equal("read", q.Get("scope"))
+
+	claims := ExternalProviderClaims{}
+	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
+	_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(ts.Config.JWT.Secret), nil
+	})
+	ts.Require().NoError(err)
+
+	ts.Equal("fly", claims.Provider)
+	ts.Equal(ts.Config.SiteURL, claims.SiteURL)
+}
+
+func FlyTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, email string) *httptest.Server {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.URL.Path {
+		case "/oauth/token":
+			*tokenCount++
+			ts.Equal(code, r.FormValue("code"))
+			ts.Equal("authorization_code", r.FormValue("grant_type"))
+			ts.Equal(ts.Config.External.Fly.RedirectURI, r.FormValue("redirect_uri"))
+
+			w.Header().Add("Content-Type", "application/json")
+			fmt.Fprint(w, `{"access_token":"fly_token","expires_in":100000,"refresh_token":"fly_refresh_token"}`)
+		case "/oauth/token/info":
+			*userCount++
+			w.Header().Add("Content-Type", "application/json")
+			fmt.Fprintf(w, `{"resource_owner_id":"test_resource_owner_id","scope":["read"],"expires_in":1111,"application":{"uid":"test_app_uid"},"created_at":1696003692,"user_id":"test_user_id","user_name":"test_user","email":"%s","organizations":[{"id":"test_org_id","role":"test"}]}`, email)
+		default:
+			w.WriteHeader(500)
+			ts.Fail("unknown fly oauth call %s", r.URL.Path)
+		}
+	}))
+
+	ts.Config.External.Fly.URL = server.URL
+
+	return server
+}
+
+func (ts *ExternalTestSuite) TestSignupExternalFly_AuthorizationCode() {
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "")
+
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
+}
+
+func (ts *ExternalTestSuite) TestSignupExternalFly_PKCE() {
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+
+	// for the plain challenge method, the code verifier == code challenge
+	// code challenge has to be between 43 - 128 chars for the plain challenge method
+	codeVerifier := "testtesttesttesttesttesttesttesttesttesttesttesttesttest"
+
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	cases := []struct {
+		desc                string
+		codeChallengeMethod string
+	}{
+		{
+			desc:                "SHA256",
+			codeChallengeMethod: "s256",
+		},
+	}
+
+	for _, c := range cases {
+		ts.Run(c.desc, func() {
+			var codeChallenge string
+			if c.codeChallengeMethod == "s256" {
+				hashedCodeVerifier := sha256.Sum256([]byte(codeVerifier))
+				codeChallenge = base64.RawURLEncoding.EncodeToString(hashedCodeVerifier[:])
+			} else {
+				codeChallenge = codeVerifier
+			}
+			// Check for valid auth code returned
+			u := performPKCEAuthorization(ts, "fly", code, codeChallenge, c.codeChallengeMethod)
+			m, err := url.ParseQuery(u.RawQuery)
+			authCode := m["code"][0]
+			require.NoError(ts.T(), err)
+			require.NotEmpty(ts.T(), authCode)
+
+			// Check for valid provider access token, mock does not return refresh toekn
+			user, err := models.FindUserByEmailAndAudience(ts.API.db, "fly@example.com", ts.Config.JWT.Aud)
+			require.NoError(ts.T(), err)
+			require.NotEmpty(ts.T(), user)
+			flowState, err := models.FindFlowStateByAuthCode(ts.API.db, authCode)
+			require.NoError(ts.T(), err)
+			require.Equal(ts.T(), "fly_token", flowState.ProviderAccessToken)
+
+			// Exchange Auth Code for token
+			var buffer bytes.Buffer
+			require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+				"code_verifier": codeVerifier,
+				"auth_code":     authCode,
+			}))
+			req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=pkce", &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)
+
+			// Validate that access token and provider tokens are present
+			data := AccessTokenResponse{}
+			require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
+			require.NotEmpty(ts.T(), data.Token)
+			require.NotEmpty(ts.T(), data.RefreshToken)
+			require.NotEmpty(ts.T(), data.ProviderAccessToken)
+			require.Equal(ts.T(), data.User.ID, user.ID)
+		})
+	}
+}
+
+func (ts *ExternalTestSuite) TestSignupExternalFlyDisableSignupErrorWhenNoUser() {
+	ts.Config.DisableSignup = true
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "")
+
+	assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", email)
+}
+
+func (ts *ExternalTestSuite) TestSignupExternalFlyDisableSignupErrorWhenEmptyEmail() {
+	ts.Config.DisableSignup = true
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := ""
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "")
+
+	assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "fly@example.com")
+}
+
+func (ts *ExternalTestSuite) TestSignupExternalFlyDisableSignupSuccessWithPrimaryEmail() {
+	ts.Config.DisableSignup = true
+
+	ts.createUser("test_user_id", "fly@example.com", "test_user", "", "")
+
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "")
+
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
+}
+
+func (ts *ExternalTestSuite) TestInviteTokenExternalFlySuccessWhenMatchingToken() {
+	// name and avatar should be populated from fly API
+	ts.createUser("test_user_id", "fly@example.com", "", "", "invite_token")
+
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "invite_token")
+
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
+}
+
+func (ts *ExternalTestSuite) TestInviteTokenExternalFlyErrorWhenNoMatchingToken() {
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	w := performAuthorizationRequest(ts, "fly", "invite_token")
+	ts.Require().Equal(http.StatusNotFound, w.Code)
+}
+
+func (ts *ExternalTestSuite) TestInviteTokenExternalFlyErrorWhenWrongToken() {
+	ts.createUser("test_user_id", "fly@example.com", "", "", "invite_token")
+
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	w := performAuthorizationRequest(ts, "fly", "wrong_token")
+	ts.Require().Equal(http.StatusNotFound, w.Code)
+}
+
+func (ts *ExternalTestSuite) TestInviteTokenExternalFlyErrorWhenEmailDoesntMatch() {
+	ts.createUser("test_user_id", "fly@example.com", "", "", "invite_token")
+
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "other@example.com"
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "invite_token")
+
+	assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
+}
+
+func (ts *ExternalTestSuite) TestSignupExternalFlyErrorWhenUserBanned() {
+	tokenCount, userCount := 0, 0
+	code := "authcode"
+	email := "fly@example.com"
+
+	server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
+	defer server.Close()
+
+	u := performAuthorization(ts, "fly", code, "")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
+
+	user, err := models.FindUserByEmailAndAudience(ts.API.db, "fly@example.com", ts.Config.JWT.Aud)
+	require.NoError(ts.T(), err)
+	t := time.Now().Add(24 * time.Hour)
+	user.BannedUntil = &t
+	require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until"))
+
+	u = performAuthorization(ts, "fly", code, "")
+	assertAuthorizationFailure(ts, u, "User is unauthorized", "unauthorized_client", "")
+}
diff --git a/internal/api/external_test.go b/internal/api/external_test.go
index 1f5c1288..be182e2f 100644
--- a/internal/api/external_test.go
+++ b/internal/api/external_test.go
@@ -44,7 +44,11 @@ func (ts *ExternalTestSuite) createUser(providerId string, email string, name st
 		require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user")
 	}
 
-	u, err := models.NewUser("", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"provider_id": providerId, "full_name": name, "avatar_url": avatar})
+	userData := map[string]interface{}{"provider_id": providerId, "full_name": name}
+	if avatar != "" {
+		userData["avatar_url"] = avatar
+	}
+	u, err := models.NewUser("", email, "test", ts.Config.JWT.Aud, userData)
 
 	if confirmationToken != "" {
 		u.ConfirmationToken = confirmationToken
diff --git a/internal/api/invite_test.go b/internal/api/invite_test.go
index 62332fd3..ba4d2f1d 100644
--- a/internal/api/invite_test.go
+++ b/internal/api/invite_test.go
@@ -94,6 +94,55 @@ func (ts *InviteTestSuite) TestInvite() {
 	assert.Equal(ts.T(), http.StatusOK, w.Code)
 }
 
+func (ts *InviteTestSuite) TestInviteAfterSignupShouldNotReturnSensitiveFields() {
+	// To allow us to send signup and invite request in succession
+	ts.Config.SMTP.MaxFrequency = 5
+	// Request body
+	var buffer bytes.Buffer
+	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+		"email": "test@example.com",
+		"data": map[string]interface{}{
+			"a": 1,
+		},
+	}))
+
+	// Setup request
+	req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
+
+	// Setup response recorder
+	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{}{
+		"email":    "test@example.com",
+		"password": "test123",
+		"data": map[string]interface{}{
+			"a": 1,
+		},
+	}))
+
+	// Setup request
+	req = httptest.NewRequest(http.MethodPost, "/signup", &buffer)
+	req.Header.Set("Content-Type", "application/json")
+
+	// Setup response recorder
+	x := httptest.NewRecorder()
+
+	ts.API.handler.ServeHTTP(x, req)
+
+	require.Equal(ts.T(), http.StatusOK, x.Code)
+
+	data := models.User{}
+	require.NoError(ts.T(), json.NewDecoder(x.Body).Decode(&data))
+	// Sensitive fields
+	require.Equal(ts.T(), 0, len(data.Identities))
+	require.Equal(ts.T(), 0, len(data.UserMetaData))
+}
+
 func (ts *InviteTestSuite) TestInvite_WithoutAccess() {
 	// Request body
 	var buffer bytes.Buffer
diff --git a/internal/api/phone_test.go b/internal/api/phone_test.go
index 9243b32a..7047de89 100644
--- a/internal/api/phone_test.go
+++ b/internal/api/phone_test.go
@@ -32,7 +32,7 @@ type TestSmsProvider struct {
 	SentMessages int
 }
 
-func (t *TestSmsProvider) SendMessage(phone, message, channel, otp string) (string, error) {
+func (t *TestSmsProvider) SendMessage(phone string, message string, channel string) (string, error) {
 	t.SentMessages += 1
 	return "", nil
 }
diff --git a/internal/api/sms_provider/sms_provider_test.go b/internal/api/sms_provider/sms_provider_test.go
index 831f66d6..5d17dbd0 100644
--- a/internal/api/sms_provider/sms_provider_test.go
+++ b/internal/api/sms_provider/sms_provider_test.go
@@ -84,7 +84,6 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 		Desc           string
 		TwilioResponse *gock.Response
 		ExpectedError  error
-		OTP            string
 	}{
 		{
 			Desc: "Successfully sent sms",
@@ -98,7 +97,6 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 				Body:       message,
 				MessageSID: "abcdef",
 			}),
-			OTP:           "123456",
 			ExpectedError: nil,
 		},
 		{
@@ -125,7 +123,6 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 				MoreInfo: "error",
 				Status:   500,
 			}),
-			OTP: "123456",
 			ExpectedError: &twilioErrResponse{
 				Code:     500,
 				Message:  "Internal server error",
@@ -137,7 +134,7 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 
 	for _, c := range cases {
 		ts.Run(c.Desc, func() {
-			_, err = twilioProvider.SendSms(phone, message, SMSProvider, c.OTP)
+			_, err = twilioProvider.SendSms(phone, message, SMSProvider)
 			require.Equal(ts.T(), c.ExpectedError, err)
 		})
 	}