Repository: supabase/gotrue
Pull request #1266 (2023-10-09 02:31:26)

feat: reinstate upgrade whatsapp support on Twilio Programmable Messaging to support Content API
Resinstates WhatsApp support for the content API by reinstating: https://github.com/supabase/gotrue/pull/1249


After further discussion, it looks like we do have users who are using custom SMS templates (e.g. "Your <company> code is {{.code}} ")

We were notified that this is required as alphanumeric senders require the company name in the  message. This means we will need to support the Content API as custom messages aren't allowed under Programmable Messaging API + WhatsApp Template


There will be 2 more PRs after this:
1. PR to allow for backward compatibility with Basic Authentication Template
2. PR to add ContentSid env var
3. Dashboard PR to expose variable


Patch:
diff --git a/internal/api/phone.go b/internal/api/phone.go
index 4599e8f2..3586ca78 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)
+		messageID, err = smsProvider.SendMessage(phone, message, channel, otp)
 		if err != nil {
 			return messageID, err
 		}
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 0125b9d1..d65ce472 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 string, message string, channel string) (string, error) {
+func (t *MessagebirdProvider) SendMessage(phone, message, channel, otp 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 4337ae37..4f4a0e13 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 string) (string, error)
+	SendMessage(phone, message, channel, otp 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 78cff912..f3d5e8de 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 string, message string, channel string) (string, error) {
+func (t *TextlocalProvider) SendMessage(phone, message, channel, otp 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 ee20b65f..c9019746 100644
--- a/internal/api/sms_provider/twilio.go
+++ b/internal/api/sms_provider/twilio.go
@@ -63,31 +63,41 @@ func NewTwilioProvider(config conf.TwilioProviderConfiguration) (SmsProvider, er
 	}, nil
 }
 
-func (t *TwilioProvider) SendMessage(phone string, message string, channel string) (string, error) {
+func (t *TwilioProvider) SendMessage(phone, message, channel, otp string) (string, error) {
 	switch channel {
 	case SMSProvider, WhatsappProvider:
-		return t.SendSms(phone, message, channel)
+		return t.SendSms(phone, message, channel, otp)
 	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 string) (string, error) {
+func (t *TwilioProvider) SendSms(phone, message, channel, otp string) (string, error) {
 	sender := t.Config.MessageServiceSid
 	receiver := "+" + phone
-	if channel == WhatsappProvider {
-		receiver = channel + ":" + receiver
-		if isPhoneNumber.MatchString(formatPhoneNumber(sender)) {
-			sender = channel + ":" + sender
-		}
-	}
 	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},
+		}
+	}
 	client := &http.Client{Timeout: defaultTimeout}
 	r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode()))
 	if err != nil {
diff --git a/internal/api/sms_provider/twilio_verify.go b/internal/api/sms_provider/twilio_verify.go
index 0f9599ee..ba3c5ac9 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 string, message string, channel string) (string, error) {
+func (t *TwilioVerifyProvider) SendMessage(phone, message, channel, otp 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 a41bc0a1..fc82a3ac 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 string, message string, channel string) (string, error) {
+func (t *VonageProvider) SendMessage(phone, message, channel, otp 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 32fe877f..b6563fd0 100644
--- a/internal/conf/configuration.go
+++ b/internal/conf/configuration.go
@@ -271,6 +271,7 @@ 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/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 7047de89..9243b32a 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 string, message string, channel string) (string, error) {
+func (t *TestSmsProvider) SendMessage(phone, message, channel, otp 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 5d17dbd0..831f66d6 100644
--- a/internal/api/sms_provider/sms_provider_test.go
+++ b/internal/api/sms_provider/sms_provider_test.go
@@ -84,6 +84,7 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 		Desc           string
 		TwilioResponse *gock.Response
 		ExpectedError  error
+		OTP            string
 	}{
 		{
 			Desc: "Successfully sent sms",
@@ -97,6 +98,7 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 				Body:       message,
 				MessageSID: "abcdef",
 			}),
+			OTP:           "123456",
 			ExpectedError: nil,
 		},
 		{
@@ -123,6 +125,7 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 				MoreInfo: "error",
 				Status:   500,
 			}),
+			OTP: "123456",
 			ExpectedError: &twilioErrResponse{
 				Code:     500,
 				Message:  "Internal server error",
@@ -134,7 +137,7 @@ func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
 
 	for _, c := range cases {
 		ts.Run(c.Desc, func() {
-			_, err = twilioProvider.SendSms(phone, message, SMSProvider)
+			_, err = twilioProvider.SendSms(phone, message, SMSProvider, c.OTP)
 			require.Equal(ts.T(), c.ExpectedError, err)
 		})
 	}