Repository: supabase/gotrue
Pull request #210 (2021-09-08 18:43:52)

Messagebird SMS provider
Hi there! This PR, based on @kangmingtay solution for OTP, introduces a new SMS provider — Messagebird. Since I don't have much Golang experience, there is more (probably) to be done, but I hope that my code will kickstart the progress of adding a new provider.

## What is the current behavior?

The only available SMS provider is Twilio.

## What is the new behavior?

By changing `GOTRUE_SMS_PROVIDER` environment variable to `messagebird` and setting respective variables for access key and sender phone number, users will be able to send SMS with OTP codes through MessageBird API.



Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a24cb7e6..2ffc5cb7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,7 +9,7 @@ jobs:
   release:
     strategy:
       matrix:
-        node: ["12"]
+        node: ["16"]
 
     outputs:
       status: ${{ steps.pre-release.outputs.release != steps.post-release.outputs.release }}
@@ -26,7 +26,7 @@ jobs:
           excludes: prerelease, draft
 
       - name: Set up Node
-        uses: actions/setup-node@v1
+        uses: actions/setup-node@v2
         with:
           node-version: ${{ matrix.node }}
 
diff --git a/.gitignore b/.gitignore
index 6aa7c669..a249cc99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-.env
+.env*
 vendor/
 gotrue
 gotrue-arm64
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9c8b85c1..5523845b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,8 +31,10 @@ $ make build
   - gotrue runs any database migrations from `/migrations` on start 
 
 ## Testing
-- Currently, we don't use a test db. You can just create a new postgres container use `hack/postgresd.sh`
+- Currently, we don't use a test db. You can just create a new postgres container, make sure docker is running and do:
 ```sh
+$ ./hack/postgresd.sh
+$ make migrate_test
 $ make test
 ```
 
diff --git a/README.md b/README.md
index cd50cecb..6e5c0d9e 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,10 @@ API service for handling user registration and authentication for JAM projects.
 It's based on OAuth2 and JWT and will handle user signup, authentication and custom
 user data.
 
-## Quick Start 
-Create a `.env` file to store your own custom env vars. See [`example.env`](example.env) 
+## Quick Start
+
+Create a `.env` file to store your own custom env vars. See [`example.env`](example.env)
+
 ```sh
 ./hack/postgresd.sh
 make build
@@ -54,6 +56,10 @@ Use this to disable phone signups (users can still use external oauth providers
 
 Header on which to rate limit the `/token` endpoint.
 
+`GOTRUE_RATE_LIMIT_EMAIL_SENT` - `string`
+
+Rate limit the number of emails sent per hr on the following endpoints: `/signup`, `/invite`, `/magiclink`, `/recover`, `/otp`, & `/user`.
+
 `PASSWORD_MIN_LENGTH` - `int`
 
 Minimum password length, defaults to 6.
@@ -105,8 +111,8 @@ Adds a prefix to all table names.
 Migrations are not applied automatically, so you will need to run them after
 you've built gotrue.
 
-* If built locally: `./gotrue migrate`
-* Using Docker: `docker run --rm gotrue gotrue migrate`
+- If built locally: `./gotrue migrate`
+- Using Docker: `docker run --rm gotrue gotrue migrate`
 
 ### Logging
 
@@ -124,6 +130,7 @@ Controls what log levels are output. Choose from `panic`, `fatal`, `error`, `war
 If you wish logs to be written to a file, set `log_file` to a valid file path.
 
 ### Opentracing
+
 Currently, only the Datadog tracer is supported.
 
 ```properties
@@ -184,7 +191,7 @@ The default group to assign all new users to.
 
 ### External Authentication Providers
 
-We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch` and `twitter` for external authentication.
+We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `spotify`, `slack`, `twitch` and `twitter` for external authentication.
 
 Use the names as the keys underneath `external` to configure each separately.
 
@@ -219,6 +226,7 @@ The base URL used for constructing the URLs to request authorization and access
 #### Apple OAuth
 
 To try out external authentication with Apple locally, you will need to do the following:
+
 1. Remap localhost to \<my_custom_dns \> in your `/etc/hosts` config.
 2. Configure gotrue to serve HTTPS traffic over localhost by replacing `ListenAndServe` in [api.go](api/api.go) with:
    ```
@@ -340,7 +348,10 @@ Default Content (if template is unavailable):
 ```html
 <h2>You have been invited</h2>
 
-<p>You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:</p>
+<p>
+  You have been invited to create a user on {{ .SiteURL }}. Follow this link to
+  accept the invite:
+</p>
 <p><a href="{{ .ConfirmationURL }}">Accept the invite</a></p>
 ```
 
@@ -396,7 +407,10 @@ Default Content (if template is unavailable):
 ```html
 <h2>Confirm Change of Email</h2>
 
-<p>Follow this link to confirm the update of your email from {{ .Email }} to {{ .NewEmail }}:</p>
+<p>
+  Follow this link to confirm the update of your email from {{ .Email }} to {{
+  .NewEmail }}:
+</p>
 <p><a href="{{ .ConfirmationURL }}">Change Email</a></p>
 ```
 
@@ -406,7 +420,7 @@ Url of the webhook receiver endpoint. This will be called when events like `vali
 
 `WEBHOOK_SECRET` - `string`
 
-Shared secret to authorize webhook requests. This secret signs the [JSON Web Signature](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41) of the request. You *should* use this to verify the integrity of the request. Otherwise others can feed your webhook receiver with fake data.
+Shared secret to authorize webhook requests. This secret signs the [JSON Web Signature](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41) of the request. You _should_ use this to verify the integrity of the request. Otherwise others can feed your webhook receiver with fake data.
 
 `WEBHOOK_RETRIES` - `number`
 
@@ -433,23 +447,29 @@ Controls the minimum amount of time that must pass before sending another sms ot
 
 `SMS_OTP_EXP` - `number`
 
-Controls the duration an sms otp is valid for. 
+Controls the duration an sms otp is valid for.
 
 `SMS_OTP_LENGTH` - `number`
 
-Controls the number of digits of the sms otp sent. 
+Controls the number of digits of the sms otp sent.
 
 `SMS_PROVIDER` - `string`
 
-for now only option is: `twilio`
+Available options are: `twilio` and `messagebird`
 
 Then you can use your [twilio credentials](https://www.twilio.com/docs/usage/requests-to-twilio#credentials):
+
 - `SMS_TWILIO_ACCOUNT_SID`
 - `SMS_TWILIO_AUTH_TOKEN`
 - `SMS_TWILIO_MESSAGE_SERVICE_SID` - can be set to your twilio sender mobile number
 
+Or Messagebird credentials, which can be obtained in the [Dashboard](https://dashboard.messagebird.com/en/developers/access):
+- `SMS_MESSAGEBIRD_ACCESS_KEY` - your Messagebird access key
+- `SMS_MESSAGEBIRD_ORIGINATOR` - SMS sender (your Messagebird phone number with + or company name)
+
 ### CAPTCHA
-* If enabled, CAPTCHA will check the request body for the `hcaptcha_token` field and make a verification request to the CAPTCHA provider.
+
+- If enabled, CAPTCHA will check the request body for the `hcaptcha_token` field and make a verification request to the CAPTCHA provider.
 
 `SECURITY_CAPTCHA_ENABLED` - `string`
 
@@ -469,386 +489,401 @@ GoTrue exposes the following endpoints:
 
 ### **GET /settings**
 
-  Returns the publicly available settings for this gotrue instance.
-
-  ```json
-  {
-    "external": {
-      "apple": true,
-      "azure": true,
-      "bitbucket": true,
-      "discord": true,
-      "facebook": true,
-      "github": true,
-      "gitlab": true,
-      "google": true,
-      "twitch": true,
-      "twitter": true
-    },
-    "disable_signup": false,
-    "autoconfirm": false
-  }
-  ```
+Returns the publicly available settings for this gotrue instance.
+
+```json
+{
+  "external": {
+    "apple": true,
+    "azure": true,
+    "bitbucket": true,
+    "discord": true,
+    "facebook": true,
+    "github": true,
+    "gitlab": true,
+    "google": true,
+    "slack": true,
+    "spotify": true,
+    "twitch": true,
+    "twitter": true
+  },
+  "disable_signup": false,
+  "autoconfirm": false
+}
+```
 
 ### **POST /admin/generate_link**
-  Returns the corresponding email action link based on the type specified.
 
-  ```js
-  headers: 
-  {
-    "Authorization": "Bearer eyJhbGciOiJI...M3A90LCkxxtX9oNP9KZO" // admin role required
-  }
+Returns the corresponding email action link based on the type specified.
 
-  body: 
-  {
-    "type": "signup" or "magiclink" or "recovery" or "invite",
-    "email": "email@example.com",
-    "password": "secret", // only if type = signup
-    "data": {
-      ...
-    }, // only if type = signup
-    "redirect_to": "https://supabase.io" // Redirect URL to send the user to after an email action. Defaults to SITE_URL. 
+```js
+headers:
+{
+  "Authorization": "Bearer eyJhbGciOiJI...M3A90LCkxxtX9oNP9KZO" // admin role required
+}
 
-  }
-  ```
-  Returns
-  ```js
-  {
-    "action_link": "http://localhost:9999/verify?token=TOKEN&type=TYPE&redirect_to=REDIRECT_URL",
+body:
+{
+  "type": "signup" or "magiclink" or "recovery" or "invite",
+  "email": "email@example.com",
+  "password": "secret", // only if type = signup
+  "data": {
     ...
-  }
-  ```
+  }, // only if type = signup
+  "redirect_to": "https://supabase.io" // Redirect URL to send the user to after an email action. Defaults to SITE_URL.
+
+}
+```
+
+Returns
+
+```js
+{
+  "action_link": "http://localhost:9999/verify?token=TOKEN&type=TYPE&redirect_to=REDIRECT_URL",
+  ...
+}
+```
 
 ### **POST /signup**
 
-  Register a new user with an email and password.
+Register a new user with an email and password.
 
-  ```js
-  {
-    "email": "email@example.com",
-    "password": "secret"
-  }
-  ```
+```js
+{
+  "email": "email@example.com",
+  "password": "secret"
+}
+```
 
-  Returns:
+Returns:
 
-  ```json
-  {
-    "id": "11111111-2222-3333-4444-5555555555555",
-    "email": "email@example.com",
-    "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
-    "created_at": "2016-05-15T19:53:12.368652374-07:00",
-    "updated_at": "2016-05-15T19:53:12.368652374-07:00"
-  }
-  ```
+```json
+{
+  "id": "11111111-2222-3333-4444-5555555555555",
+  "email": "email@example.com",
+  "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
+  "created_at": "2016-05-15T19:53:12.368652374-07:00",
+  "updated_at": "2016-05-15T19:53:12.368652374-07:00"
+}
+```
 
-  Register a new user with a phone number and password.
+Register a new user with a phone number and password.
 
-  ```js
-  {
-    "phone": "12345678", // follows the E.164 format
-    "password": "secret"
-  }
-  ```
+```js
+{
+  "phone": "12345678", // follows the E.164 format
+  "password": "secret"
+}
+```
 
-  Returns:
+Returns:
 
-  ```json
-  {
-    "id": "11111111-2222-3333-4444-5555555555555",
-    "phone": "12345678",
-    "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
-    "created_at": "2016-05-15T19:53:12.368652374-07:00",
-    "updated_at": "2016-05-15T19:53:12.368652374-07:00"
-  }
-  ```
+```json
+{
+  "id": "11111111-2222-3333-4444-5555555555555",
+  "phone": "12345678",
+  "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
+  "created_at": "2016-05-15T19:53:12.368652374-07:00",
+  "updated_at": "2016-05-15T19:53:12.368652374-07:00"
+}
+```
 
 ### **POST /invite**
 
-  Invites a new user with an email.
-  This endpoint requires the `service_role` or `supabase_admin` JWT set as an Auth Bearer header:
+Invites a new user with an email.
+This endpoint requires the `service_role` or `supabase_admin` JWT set as an Auth Bearer header:
 
-  e.g.
-  ```json
-  headers: {
-    "Authorization" : "Bearer eyJhbGciOiJI...M3A90LCkxxtX9oNP9KZO"
-  }
-  ```
+e.g.
 
-  ```json
-  {
-    "email": "email@example.com"
-  }
-  ```
-
-  Returns:
-
-  ```json
-  {
-    "id": "11111111-2222-3333-4444-5555555555555",
-    "email": "email@example.com",
-    "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
-    "created_at": "2016-05-15T19:53:12.368652374-07:00",
-    "updated_at": "2016-05-15T19:53:12.368652374-07:00",
-    "invited_at": "2016-05-15T19:53:12.368652374-07:00"
-  }
-  ```
+```json
+headers: {
+  "Authorization" : "Bearer eyJhbGciOiJI...M3A90LCkxxtX9oNP9KZO"
+}
+```
+
+```json
+{
+  "email": "email@example.com"
+}
+```
+
+Returns:
+
+```json
+{
+  "id": "11111111-2222-3333-4444-5555555555555",
+  "email": "email@example.com",
+  "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
+  "created_at": "2016-05-15T19:53:12.368652374-07:00",
+  "updated_at": "2016-05-15T19:53:12.368652374-07:00",
+  "invited_at": "2016-05-15T19:53:12.368652374-07:00"
+}
+```
 
 ### **POST /verify**
 
-  Verify a registration or a password recovery. Type can be `signup` or `recovery` or `invite` 
-  and the `token` is a token returned from either `/signup` or `/recover`.
+Verify a registration or a password recovery. Type can be `signup` or `recovery` or `invite`
+and the `token` is a token returned from either `/signup` or `/recover`.
 
-  ```json
-  {
-    "type": "signup",
-    "token": "confirmation-code-delivered-in-email"
-  }
-  ```
+```json
+{
+  "type": "signup",
+  "token": "confirmation-code-delivered-in-email"
+}
+```
 
-  `password` is required for signup verification if no existing password exists.
+`password` is required for signup verification if no existing password exists.
 
-  Returns:
+Returns:
 
-  ```json
-  {
-    "access_token": "jwt-token-representing-the-user",
-    "token_type": "bearer",
-    "expires_in": 3600,
-    "refresh_token": "a-refresh-token",
-    "type": "signup | recovery | invite"
-  }
-  ```
-
-  Verify a phone signup or sms otp. Type should be set to `sms`.
-  ```json
-  {
-    "type": "sms",
-    "token": "confirmation-otp-delivered-in-sms",
-    "redirect_to": "https://supabase.io",
-    "phone": "phone-number-sms-otp-was-delivered-to"
-  }
-  ```
+```json
+{
+  "access_token": "jwt-token-representing-the-user",
+  "token_type": "bearer",
+  "expires_in": 3600,
+  "refresh_token": "a-refresh-token",
+  "type": "signup | recovery | invite"
+}
+```
 
-  Returns:
+Verify a phone signup or sms otp. Type should be set to `sms`.
 
-  ```json
-  {
-    "access_token": "jwt-token-representing-the-user",
-    "token_type": "bearer",
-    "expires_in": 3600,
-    "refresh_token": "a-refresh-token"
-  }
-  ```
+```json
+{
+  "type": "sms",
+  "token": "confirmation-otp-delivered-in-sms",
+  "redirect_to": "https://supabase.io",
+  "phone": "phone-number-sms-otp-was-delivered-to"
+}
+```
+
+Returns:
+
+```json
+{
+  "access_token": "jwt-token-representing-the-user",
+  "token_type": "bearer",
+  "expires_in": 3600,
+  "refresh_token": "a-refresh-token"
+}
+```
 
 ### **GET /verify**
 
-  Verify a registration or a password recovery. Type can be `signup` or `recovery` or `magiclink` or `invite`
-  and the `token` is a token returned from either `/signup` or `/recover` or `/magiclink`.
+Verify a registration or a password recovery. Type can be `signup` or `recovery` or `magiclink` or `invite`
+and the `token` is a token returned from either `/signup` or `/recover` or `/magiclink`.
 
-  query params:
-  ```json
-  {
-    "type": "signup",
-    "token": "confirmation-code-delivered-in-email",
-    "redirect_to": "https://supabase.io"
-  }
-  ```
+query params:
+
+```json
+{
+  "type": "signup",
+  "token": "confirmation-code-delivered-in-email",
+  "redirect_to": "https://supabase.io"
+}
+```
 
-  User will be logged in and redirected to:
+User will be logged in and redirected to:
 
-  ```json
-  SITE_URL/#access_token=jwt-token-representing-the-user&token_type=bearer&expires_in=3600&refresh_token=a-refresh-token&type=invite
-  ```
+```json
+SITE_URL/#access_token=jwt-token-representing-the-user&token_type=bearer&expires_in=3600&refresh_token=a-refresh-token&type=invite
+```
 
-  Your app should detect the query params in the fragment and use them to set the session (supabase-js does this automatically)
+Your app should detect the query params in the fragment and use them to set the session (supabase-js does this automatically)
 
-  You can use the `type` param to redirect the user to a password set form in the case of `invite` or `recovery`,
-  or show an account confirmed/welcome message in the case of `signup`, or direct them to some additional onboarding flow
+You can use the `type` param to redirect the user to a password set form in the case of `invite` or `recovery`,
+or show an account confirmed/welcome message in the case of `signup`, or direct them to some additional onboarding flow
 
 ### **POST /otp**
-  One-Time-Password. Will deliver a magiclink or sms otp to the user depending on whether the request body contains an "email" or "phone" key. 
 
-  ```js
-  {
-    "phone": "12345678" // follows the E.164 format
-  }
+One-Time-Password. Will deliver a magiclink or sms otp to the user depending on whether the request body contains an "email" or "phone" key.
 
-  OR 
+```js
+{
+  "phone": "12345678" // follows the E.164 format
+}
 
-  // exactly the same as /magiclink 
-  {
-    "email": "email@example.com"
-  }
-  ```
-  Returns:
-  ```
-  {}
-  ```
+OR
+
+// exactly the same as /magiclink
+{
+  "email": "email@example.com"
+}
+```
+
+Returns:
+
+```
+{}
+```
 
 ### **POST /magiclink** (recommended to use /otp instead. See above.)
 
-  Magic Link. Will deliver a link (e.g. `/verify?type=magiclink&token=fgtyuf68ddqdaDd`) to the user based on
-  email address which they can use to redeem an access_token.
+Magic Link. Will deliver a link (e.g. `/verify?type=magiclink&token=fgtyuf68ddqdaDd`) to the user based on
+email address which they can use to redeem an access_token.
 
-  By default Magic Links can only be sent once every 60 seconds
+By default Magic Links can only be sent once every 60 seconds
 
-  ```json
-  {
-    "email": "email@example.com"
-  }
-  ```
+```json
+{
+  "email": "email@example.com"
+}
+```
 
-  Returns:
+Returns:
 
-  ```json
-  {}
-  ```
+```json
+{}
+```
 
-  when clicked the magic link will redirect the user to `<SITE_URL>#access_token=x&refresh_token=y&expires_in=z&token_type=bearer&type=magiclink` (see `/verify` above)
+when clicked the magic link will redirect the user to `<SITE_URL>#access_token=x&refresh_token=y&expires_in=z&token_type=bearer&type=magiclink` (see `/verify` above)
 
 ### **POST /recover**
 
-  Password recovery. Will deliver a password recovery mail to the user based on
-  email address.
+Password recovery. Will deliver a password recovery mail to the user based on
+email address.
 
-  By default recovery links can only be sent once every 60 seconds
+By default recovery links can only be sent once every 60 seconds
 
-  ```json
-  {
-    "email": "email@example.com"
-  }
-  ```
+```json
+{
+  "email": "email@example.com"
+}
+```
 
-  Returns:
+Returns:
 
-  ```json
-  {}
-  ```
+```json
+{}
+```
 
 ### **POST /token**
 
-  This is an OAuth2 endpoint that currently implements
-  the password and refresh_token grant types
+This is an OAuth2 endpoint that currently implements
+the password and refresh_token grant types
 
-  query params:
-  ```
-  ?grant_type=password
-  ```
+query params:
 
-  body:
-  ```json
-  // Email login
-  {
-    "email": "name@domain.com",
-    "password": "somepassword"
-  }
-  
-  // Phone login
-  {
-    "phone": "12345678",
-    "password": "somepassword"
-  }
-  ```
+```
+?grant_type=password
+```
 
-  or
+body:
 
-  query params:
-  ```
-  grant_type=refresh_token
-  ```
+```json
+// Email login
+{
+  "email": "name@domain.com",
+  "password": "somepassword"
+}
 
-  body:
-  ```json
-  {
-    "refresh_token": "a-refresh-token"
-  }
-  ```
+// Phone login
+{
+  "phone": "12345678",
+  "password": "somepassword"
+}
+```
 
-  Once you have an access token, you can access the methods requiring authentication
-  by settings the `Authorization: Bearer YOUR_ACCESS_TOKEN_HERE` header.
+or
 
-  Returns:
+query params:
 
-  ```json
-  {
-    "access_token": "jwt-token-representing-the-user",
-    "token_type": "bearer",
-    "expires_in": 3600,
-    "refresh_token": "a-refresh-token"
-  }
-  ```
+```
+grant_type=refresh_token
+```
+
+body:
+
+```json
+{
+  "refresh_token": "a-refresh-token"
+}
+```
+
+Once you have an access token, you can access the methods requiring authentication
+by settings the `Authorization: Bearer YOUR_ACCESS_TOKEN_HERE` header.
+
+Returns:
+
+```json
+{
+  "access_token": "jwt-token-representing-the-user",
+  "token_type": "bearer",
+  "expires_in": 3600,
+  "refresh_token": "a-refresh-token"
+}
+```
 
 ### **GET /user**
 
-  Get the JSON object for the logged in user (requires authentication)
+Get the JSON object for the logged in user (requires authentication)
 
-  Returns:
+Returns:
 
-  ```json
-  {
-    "id": "11111111-2222-3333-4444-5555555555555",
-    "email": "email@example.com",
-    "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
-    "created_at": "2016-05-15T19:53:12.368652374-07:00",
-    "updated_at": "2016-05-15T19:53:12.368652374-07:00"
-  }
-  ```
+```json
+{
+  "id": "11111111-2222-3333-4444-5555555555555",
+  "email": "email@example.com",
+  "confirmation_sent_at": "2016-05-15T20:49:40.882805774-07:00",
+  "created_at": "2016-05-15T19:53:12.368652374-07:00",
+  "updated_at": "2016-05-15T19:53:12.368652374-07:00"
+}
+```
 
 ### **PUT /user**
 
-  Update a user (Requires authentication). Apart from changing email/password, this
-  method can be used to set custom user data. Changing the email will result in a magiclink being sent out.
-
-  ```json
-  {
-    "email": "new-email@example.com",
-    "password": "new-password",
-    "data": {
-      "key": "value",
-      "number": 10,
-      "admin": false
-    }
+Update a user (Requires authentication). Apart from changing email/password, this
+method can be used to set custom user data. Changing the email will result in a magiclink being sent out.
+
+```json
+{
+  "email": "new-email@example.com",
+  "password": "new-password",
+  "data": {
+    "key": "value",
+    "number": 10,
+    "admin": false
   }
-  ```
+}
+```
 
-  Returns:
+Returns:
 
-  ```json
-  {
-    "id": "11111111-2222-3333-4444-5555555555555",
-    "email": "email@example.com",
-    "email_change_sent_at": "2016-05-15T20:49:40.882805774-07:00",
-    "created_at": "2016-05-15T19:53:12.368652374-07:00",
-    "updated_at": "2016-05-15T19:53:12.368652374-07:00"
-  }
-  ```
+```json
+{
+  "id": "11111111-2222-3333-4444-5555555555555",
+  "email": "email@example.com",
+  "email_change_sent_at": "2016-05-15T20:49:40.882805774-07:00",
+  "created_at": "2016-05-15T19:53:12.368652374-07:00",
+  "updated_at": "2016-05-15T19:53:12.368652374-07:00"
+}
+```
 
 ### **POST /logout**
 
-  Logout a user (Requires authentication).
-
-  This will revoke all refresh tokens for the user. Remember that the JWT tokens
-  will still be valid for stateless auth until they expires.
+Logout a user (Requires authentication).
 
+This will revoke all refresh tokens for the user. Remember that the JWT tokens
+will still be valid for stateless auth until they expires.
 
 ### **GET /authorize**
 
-  Get access_token from external oauth provider
+Get access_token from external oauth provider
+
+query params:
 
-  query params:
-  ```
-  provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | twitch | twitter
-  scopes=<optional additional scopes depending on the provider (email and name are requested by default)>
-  ```
+```
+provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | slack | spotify | twitch | twitter
+scopes=<optional additional scopes depending on the provider (email and name are requested by default)>
+```
 
-  Redirects to provider and then to `/callback`
+Redirects to provider and then to `/callback`
 
-  For apple specific setup see: https://github.com/supabase/gotrue#apple-oauth
+For apple specific setup see: https://github.com/supabase/gotrue#apple-oauth
 
 ### **GET /callback**
 
-  External provider should redirect to here
+External provider should redirect to here
 
-  Redirects to `<GOTRUE_SITE_URL>#access_token=<access_token>&refresh_token=<refresh_token>&provider_token=<provider_oauth_token>&expires_in=3600&provider=<provider_name>`
-  If additional scopes were requested then `provider_token` will be populated, you can use this to fetch additional data from the provider or interact with their services
+Redirects to `<GOTRUE_SITE_URL>#access_token=<access_token>&refresh_token=<refresh_token>&provider_token=<provider_oauth_token>&expires_in=3600&provider=<provider_name>`
+If additional scopes were requested then `provider_token` will be populated, you can use this to fetch additional data from the provider or interact with their services
diff --git a/api/admin.go b/api/admin.go
index 5d7a6812..93f6f8db 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -237,6 +237,7 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error {
 		user.AppMetaData = make(map[string]interface{})
 	}
 	user.AppMetaData["provider"] = "email"
+	user.AppMetaData["providers"] = []string{"email"}
 
 	err = a.db.Transaction(func(tx *storage.Connection) error {
 		if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserSignedUpAction, map[string]interface{}{
diff --git a/api/api.go b/api/api.go
index e29e7f73..d0bbfa3d 100644
--- a/api/api.go
+++ b/api/api.go
@@ -114,12 +114,13 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 
 		r.Get("/authorize", api.ExternalProviderRedirect)
 
-		r.With(api.requireAdminCredentials).Post("/invite", api.Invite)
+		sharedLimiter := api.limitEmailSentHandler()
+		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).With(api.requireEmailProvider).Post("/recover", api.Recover)
+		r.With(sharedLimiter).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
 
-		r.With(api.verifyCaptcha).Post("/signup", api.Signup)
-		r.With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
-		r.With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
-		r.With(api.verifyCaptcha).Post("/otp", api.Otp)
+		r.With(sharedLimiter).With(api.verifyCaptcha).Post("/otp", api.Otp)
 
 		r.With(api.requireEmailProvider).With(api.limitHandler(
 			// Allow requests at a rate of 30 per 5 minutes.
@@ -143,7 +144,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 		r.Route("/user", func(r *router) {
 			r.Use(api.requireAuthentication)
 			r.Get("/", api.UserGet)
-			r.Put("/", api.UserUpdate)
+			r.With(sharedLimiter).Put("/", api.UserUpdate)
 		})
 
 		r.Route("/admin", func(r *router) {
diff --git a/api/errors.go b/api/errors.go
index cec79319..58dd6e64 100644
--- a/api/errors.go
+++ b/api/errors.go
@@ -6,11 +6,15 @@ import (
 	"net/http"
 	"os"
 	"runtime/debug"
+
+	"github.com/netlify/gotrue/conf"
+	"github.com/pkg/errors"
 )
 
 // Common error messages during signup flow
 var (
-	DuplicateEmailMsg = "A user with this email address has already been registered"
+	DuplicateEmailMsg       = "A user with this email address has already been registered"
+	UserExistsError   error = errors.New("User already exists")
 )
 
 var oauthErrorMap = map[int]string{
@@ -56,6 +60,21 @@ func (e *OAuthError) Cause() error {
 	return e
 }
 
+func invalidSignupError(config *conf.Configuration) *HTTPError {
+	var msg string
+	if config.External.Email.Enabled && config.External.Phone.Enabled {
+		msg = "To signup, please provide your email or phone number"
+	} else if config.External.Email.Enabled {
+		msg = "To signup, please provide your email"
+	} else if config.External.Phone.Enabled {
+		msg = "To signup, please provide your phone number"
+	} else {
+		// 3rd party OAuth signups
+		msg = "To signup, please provide required fields"
+	}
+	return unprocessableEntityError(msg)
+}
+
 func oauthError(err string, description string) *OAuthError {
 	return &OAuthError{Err: err, Description: description}
 }
diff --git a/api/external.go b/api/external.go
index ffa5c7da..6e2f12b8 100644
--- a/api/external.go
+++ b/api/external.go
@@ -11,12 +11,10 @@ import (
 
 	"github.com/gofrs/uuid"
 	jwt "github.com/golang-jwt/jwt"
-	"github.com/markbates/goth/gothic"
 	"github.com/netlify/gotrue/api/provider"
 	"github.com/netlify/gotrue/models"
 	"github.com/netlify/gotrue/storage"
 	"github.com/sirupsen/logrus"
-	"golang.org/x/oauth2"
 )
 
 // ExternalProviderClaims are the JWT claims sent as the state in the external oauth provider signup flow
@@ -83,20 +81,10 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e
 	switch externalProvider := p.(type) {
 	case *provider.TwitterProvider:
 		authURL = externalProvider.AuthCodeURL(tokenString)
-		err := gothic.StoreInSession(providerType, externalProvider.Marshal(), r, w)
+		err := storage.StoreInSession(providerType, externalProvider.Marshal(), r, w)
 		if err != nil {
 			return internalServerError("Error storing request token in session").WithInternalError(err)
 		}
-	case *provider.AppleProvider:
-		opts := make([]oauth2.AuthCodeOption, 0, 1)
-		opts = append(opts, oauth2.SetAuthURLParam("response_mode", "form_post"))
-		authURL = externalProvider.Config.AuthCodeURL(tokenString, opts...)
-		if authURL != "" {
-			if u, err := url.Parse(authURL); err == nil {
-				u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%20")
-				authURL = u.String()
-			}
-		}
 	default:
 		authURL = p.AuthCodeURL(tokenString)
 	}
@@ -153,53 +141,86 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 			}
 		} else {
 			aud := a.requestAud(ctx, r)
-
-			// search user using all available emails
 			var emailData provider.Email
-			for _, e := range userData.Emails {
-				if e.Verified || config.Mailer.Autoconfirm {
-					user, terr = models.FindUserByEmailAndAudience(tx, instanceID, e.Email, aud)
-					if terr != nil && !models.IsNotFoundError(terr) {
-						return internalServerError("Error checking for duplicate users").WithInternalError(terr)
-					}
-
-					if user != nil {
-						emailData = e
-						break
-					}
+			var identityData map[string]interface{}
+			if userData.Metadata != nil {
+				identityData, terr = userData.Metadata.ToMap()
+				if terr != nil {
+					return terr
 				}
 			}
 
-			if user == nil {
-				if config.DisableSignup {
-					return forbiddenError("Signups not allowed for this instance")
-				}
-
-				// prefer primary email for new signups
-				emailData = userData.Emails[0]
-				for _, e := range userData.Emails {
-					if e.Primary {
-						emailData = e
-						break
+			var identity *models.Identity
+			// check if identity exists
+			if identity, terr = models.FindIdentityByIdAndProvider(tx, userData.Metadata.Subject, providerType); terr != nil {
+				if models.IsNotFoundError(terr) {
+					// search user using all available emails
+					for _, e := range userData.Emails {
+						if e.Verified || config.Mailer.Autoconfirm {
+							user, terr = models.FindUserByEmailAndAudience(tx, instanceID, e.Email, aud)
+							if terr != nil && !models.IsNotFoundError(terr) {
+								return internalServerError("Error checking for duplicate users").WithInternalError(terr)
+							}
+							if user != nil {
+								emailData = e
+								break
+							}
+						}
 					}
-				}
-
-				params := &SignupParams{
-					Provider: providerType,
-					Email:    emailData.Email,
-					Aud:      aud,
-					Data:     make(map[string]interface{}),
-				}
-				for k, v := range userData.Metadata {
-					if v != "" {
-						params.Data[k] = v
+					if user != nil {
+						if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
+							return terr
+						}
+						if terr = user.UpdateAppMetaDataProviders(tx); terr != nil {
+							return terr
+						}
+					} else {
+						if config.DisableSignup {
+							return forbiddenError("Signups not allowed for this instance")
+						}
+
+						// prefer primary email for new signups
+						emailData = userData.Emails[0]
+						for _, e := range userData.Emails {
+							if e.Primary {
+								emailData = e
+								break
+							}
+						}
+
+						params := &SignupParams{
+							Provider: providerType,
+							Email:    emailData.Email,
+							Aud:      aud,
+							Data:     identityData,
+						}
+
+						user, terr = a.signupNewUser(ctx, tx, params)
+						if terr != nil {
+							return terr
+						}
+
+						if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
+							return terr
+						}
 					}
+				} else {
+					return terr
 				}
+			}
 
-				user, terr = a.signupNewUser(ctx, tx, params)
+			if identity != nil && user == nil {
+				// get user associated with identity
+				user, terr = models.FindUserByID(tx, identity.UserID)
 				if terr != nil {
 					return terr
 				}
+				if terr = tx.UpdateOnly(identity, "identity_data", "last_sign_in_at"); terr != nil {
+					return terr
+				}
+				if terr = user.UpdateAppMetaDataProviders(tx); terr != nil {
+					return terr
+				}
 			}
 
 			if !user.IsConfirmed() {
@@ -282,19 +303,25 @@ func (a *API) processInvite(ctx context.Context, tx *storage.Connection, userDat
 		return nil, badRequestError("Invited email does not match emails from external provider").WithInternalMessage("invited=%s external=%s", user.Email, strings.Join(emails, ", "))
 	}
 
-	if err := user.UpdateAppMetaData(tx, map[string]interface{}{
+	var identityData map[string]interface{}
+	if userData.Metadata != nil {
+		identityData, err = userData.Metadata.ToMap()
+		if err != nil {
+			return nil, internalServerError("Error serialising user metadata").WithInternalError(err)
+		}
+	}
+	if _, err := a.createNewIdentity(tx, user, providerType, identityData); err != nil {
+		return nil, err
+	}
+	if err = user.UpdateAppMetaData(tx, map[string]interface{}{
 		"provider": providerType,
 	}); err != nil {
-		return nil, internalServerError("Database error updating user").WithInternalError(err)
+		return nil, err
 	}
-
-	updates := make(map[string]interface{})
-	for k, v := range userData.Metadata {
-		if v != "" {
-			updates[k] = v
-		}
+	if err = user.UpdateAppMetaDataProviders(tx); err != nil {
+		return nil, err
 	}
-	if err := user.UpdateUserMetaData(tx, updates); err != nil {
+	if err := user.UpdateUserMetaData(tx, identityData); err != nil {
 		return nil, internalServerError("Database error updating user").WithInternalError(err)
 	}
 
@@ -355,6 +382,10 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide
 		return provider.NewGoogleProvider(config.External.Google, scopes)
 	case "facebook":
 		return provider.NewFacebookProvider(config.External.Facebook, scopes)
+	case "spotify":
+		return provider.NewSpotifyProvider(config.External.Spotify, scopes)
+	case "slack":
+		return provider.NewSlackProvider(config.External.Slack, scopes)
 	case "twitch":
 		return provider.NewTwitchProvider(config.External.Twitch, scopes)
 	case "twitter":
@@ -418,3 +449,23 @@ func (a *API) getExternalRedirectURL(r *http.Request) string {
 	}
 	return config.SiteURL
 }
+
+func (a *API) createNewIdentity(conn *storage.Connection, user *models.User, providerType string, identityData map[string]interface{}) (*models.Identity, error) {
+	identity, err := models.NewIdentity(user, providerType, identityData)
+	if err != nil {
+		return nil, err
+	}
+
+	err = conn.Transaction(func(tx *storage.Connection) error {
+		if terr := tx.Create(identity); terr != nil {
+			return internalServerError("Error creating identity").WithInternalError(terr)
+		}
+		return nil
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return identity, nil
+}
diff --git a/api/external_oauth.go b/api/external_oauth.go
index 0b245900..3a4899ef 100644
--- a/api/external_oauth.go
+++ b/api/external_oauth.go
@@ -5,9 +5,9 @@ import (
 	"net/http"
 	"net/url"
 
-	"github.com/markbates/goth/gothic"
 	"github.com/mrjones/oauth"
 	"github.com/netlify/gotrue/api/provider"
+	"github.com/netlify/gotrue/storage"
 	"github.com/sirupsen/logrus"
 )
 
@@ -87,7 +87,10 @@ func (a *API) oAuthCallback(ctx context.Context, r *http.Request, providerType s
 		// apple only returns user info the first time
 		oauthUser := rq.Get("user")
 		if oauthUser != "" {
-			userData.Metadata = externalProvider.ParseUser(oauthUser)
+			err := externalProvider.ParseUser(oauthUser, userData)
+			if err != nil {
+				return nil, err
+			}
 		}
 	}
 
@@ -102,7 +105,7 @@ func (a *API) oAuth1Callback(ctx context.Context, r *http.Request, providerType
 	if err != nil {
 		return nil, badRequestError("Unsupported provider: %+v", err).WithInternalError(err)
 	}
-	value, err := gothic.GetFromSession(providerType, r)
+	value, err := storage.GetFromSession(providerType, r)
 	if err != nil {
 		return &OAuthProviderData{}, err
 	}
diff --git a/api/external_saml.go b/api/external_saml.go
index cb447a1d..fa74d1cd 100644
--- a/api/external_saml.go
+++ b/api/external_saml.go
@@ -52,6 +52,9 @@ func (a *API) samlCallback(ctx context.Context, r *http.Request) (*provider.User
 			Email:    assertionInfo.NameID,
 			Verified: true,
 		}},
+		Metadata: &provider.Claims{
+			Subject: assertionInfo.NameID,
+		},
 	}
 	return userData, nil
 }
diff --git a/api/mail.go b/api/mail.go
index c479e620..edf8b0f6 100644
--- a/api/mail.go
+++ b/api/mail.go
@@ -229,10 +229,11 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile
 	return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery")
 }
 
-func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error {
+// sendSecureEmailChange sends out an email change token each to the old and new emails.
+func (a *API) sendSecureEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error {
 	u.EmailChangeTokenCurrent, u.EmailChangeTokenNew = crypto.SecureToken(), crypto.SecureToken()
 	u.EmailChange = email
-	u.EmailChangeConfirmStatus = noneConfirmed
+	u.EmailChangeConfirmStatus = zeroConfirmation
 	now := time.Now()
 	if err := mailer.EmailChangeMail(u, referrerURL); err != nil {
 		return err
@@ -249,6 +250,26 @@ func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mai
 	), "Database error updating user for email change")
 }
 
+// sendEmailChange sends out an email change token to the new email.
+func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error {
+	u.EmailChangeTokenNew = crypto.SecureToken()
+	u.EmailChange = email
+	u.EmailChangeConfirmStatus = zeroConfirmation
+	now := time.Now()
+	if err := mailer.EmailChangeMail(u, referrerURL); err != nil {
+		return err
+	}
+
+	u.EmailChangeSentAt = &now
+	return errors.Wrap(tx.UpdateOnly(
+		u,
+		"email_change_token_new",
+		"email_change",
+		"email_change_sent_at",
+		"email_change_confirm_status",
+	), "Database error updating user for email change")
+}
+
 func (a *API) validateEmail(ctx context.Context, email string) error {
 	if email == "" {
 		return unprocessableEntityError("An email address is required")
diff --git a/api/middleware.go b/api/middleware.go
index e2b7d1d5..54e6fdc0 100644
--- a/api/middleware.go
+++ b/api/middleware.go
@@ -4,12 +4,14 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"github.com/netlify/gotrue/security"
-	"github.com/sirupsen/logrus"
 	"io"
 	"io/ioutil"
 	"net/http"
 	"strings"
+	"time"
+
+	"github.com/netlify/gotrue/security"
+	"github.com/sirupsen/logrus"
 
 	"github.com/didip/tollbooth/v5"
 	"github.com/didip/tollbooth/v5/limiter"
@@ -154,6 +156,44 @@ func (a *API) limitHandler(lmt *limiter.Limiter) middlewareHandler {
 	}
 }
 
+func (a *API) limitEmailSentHandler() middlewareHandler {
+	// limit per hour
+	freq := a.config.RateLimitEmailSent / (60 * 60)
+	lmt := tollbooth.NewLimiter(freq, &limiter.ExpirableOptions{
+		DefaultExpirationTTL: time.Hour,
+	}).SetBurst(int(a.config.RateLimitEmailSent)).SetMethods([]string{"PUT", "POST"})
+	return func(w http.ResponseWriter, req *http.Request) (context.Context, error) {
+		c := req.Context()
+		config := a.getConfig(c)
+		if config.External.Email.Enabled && !config.Mailer.Autoconfirm {
+			if req.Method == "PUT" || req.Method == "POST" {
+				res := make(map[string]interface{})
+				bodyBytes, err := ioutil.ReadAll(req.Body)
+				if err != nil {
+					return c, internalServerError("Error invalid request body").WithInternalError(err)
+				}
+				req.Body.Close()
+				req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
+
+				jsonDecoder := json.NewDecoder(bytes.NewBuffer(bodyBytes))
+				if err := jsonDecoder.Decode(&res); err != nil {
+					return c, badRequestError("Error invalid request body").WithInternalError(err)
+				}
+
+				if _, ok := res["email"]; !ok {
+					// email not in POST body
+					return c, nil
+				}
+
+				if err := tollbooth.LimitByKeys(lmt, []string{"email_functions"}); err != nil {
+					return c, httpError(http.StatusTooManyRequests, "Rate limit exceeded")
+				}
+			}
+		}
+		return c, nil
+	}
+}
+
 func (a *API) requireAdminCredentials(w http.ResponseWriter, req *http.Request) (context.Context, error) {
 	ctx := req.Context()
 	t, err := a.extractBearerToken(w, req)
diff --git a/api/phone.go b/api/phone.go
index 03a8ce48..c3639733 100644
--- a/api/phone.go
+++ b/api/phone.go
@@ -36,7 +36,6 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection,
 		return MaxFrequencyLimitError
 	}
 
-	now := time.Now()
 	oldToken := user.ConfirmationToken
 	otp, err := crypto.GenerateOtp(config.Sms.OtpLength)
 	if err != nil {
@@ -61,6 +60,7 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection,
 		return serr
 	}
 
+	now := time.Now()
 	user.ConfirmationSentAt = &now
 
 	return errors.Wrap(tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at"), "Database error updating user for confirmation")
diff --git a/api/provider/apple.go b/api/provider/apple.go
index aaec711b..148841af 100644
--- a/api/provider/apple.go
+++ b/api/provider/apple.go
@@ -8,6 +8,8 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"net/url"
+	"strings"
 
 	"github.com/golang-jwt/jwt"
 	"github.com/lestrrat-go/jwx/jwk"
@@ -50,6 +52,7 @@ type idTokenClaims struct {
 	AuthTime        int    `json:"auth_time"`
 	Email           string `json:"email"`
 	IsPrivateEmail  bool   `json:"is_private_email,string"`
+	Sub             string `json:"sub"`
 }
 
 // NewAppleProvider creates a Apple account provider.
@@ -87,6 +90,19 @@ func (p AppleProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
 	return p.Exchange(oauth2.NoContext, code, opts...)
 }
 
+func (p AppleProvider) AuthCodeURL(state string, args ...oauth2.AuthCodeOption) string {
+	opts := make([]oauth2.AuthCodeOption, 0, 1)
+	opts = append(opts, oauth2.SetAuthURLParam("response_mode", "form_post"))
+	authURL := p.Config.AuthCodeURL(state, opts...)
+	if authURL != "" {
+		if u, err := url.Parse(authURL); err != nil {
+			u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%20")
+			authURL = u.String()
+		}
+	}
+	return authURL
+}
+
 // GetUserData returns the user data fetched from the apple provider
 func (p AppleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
 	var user *UserProvidedData
@@ -148,22 +164,29 @@ func (p AppleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
 				Verified: true,
 				Primary:  true,
 			}},
+			Metadata: &Claims{
+				Issuer:        p.UserInfoURL,
+				Subject:       idToken.Claims.(*idTokenClaims).Sub,
+				Email:         idToken.Claims.(*idTokenClaims).Email,
+				EmailVerified: true,
+
+				// To be deprecated
+				ProviderId: idToken.Claims.(*idTokenClaims).Sub,
+			},
 		}
-
 	}
 	return user, nil
 }
 
 // ParseUser parses the apple user's info
-func (p AppleProvider) ParseUser(data string) map[string]string {
-	userData := &appleUser{}
-	err := json.Unmarshal([]byte(data), userData)
+func (p AppleProvider) ParseUser(data string, userData *UserProvidedData) error {
+	u := &appleUser{}
+	err := json.Unmarshal([]byte(data), u)
 	if err != nil {
-		return nil
-	}
-	return map[string]string{
-		"firstName": userData.Name.FirstName,
-		"lastName":  userData.Name.LastName,
-		"email":     userData.Email,
+		return err
 	}
+
+	userData.Metadata.Name = strings.TrimSpace(u.Name.FirstName + " " + u.Name.LastName)
+	userData.Metadata.FullName = strings.TrimSpace(u.Name.FirstName + " " + u.Name.LastName)
+	return nil
 }
diff --git a/api/provider/azure.go b/api/provider/azure.go
index 903adbef..fe78cf92 100644
--- a/api/provider/azure.go
+++ b/api/provider/azure.go
@@ -22,6 +22,7 @@ type azureProvider struct {
 type azureUser struct {
 	Name  string `json:"name"`
 	Email string `json:"email"`
+	Sub   string `json:"sub"`
 }
 
 type azureEmail struct {
@@ -75,8 +76,16 @@ func (g azureProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
 	}
 
 	return &UserProvidedData{
-		Metadata: map[string]string{
-			nameKey: u.Name,
+		Metadata: &Claims{
+			Issuer:        g.APIPath,
+			Subject:       u.Sub,
+			Name:          u.Name,
+			Email:         u.Email,
+			EmailVerified: true,
+
+			// To be deprecated
+			FullName:   u.Name,
+			ProviderId: u.Sub,
 		},
 		Emails: []Email{{
 			Email:    u.Email,
diff --git a/api/provider/bitbucket.go b/api/provider/bitbucket.go
index 475c912e..6fb0f733 100644
--- a/api/provider/bitbucket.go
+++ b/api/provider/bitbucket.go
@@ -20,6 +20,7 @@ type bitbucketProvider struct {
 
 type bitbucketUser struct {
 	Name   string `json:"display_name"`
+	ID     string `json:"uuid"`
 	Avatar struct {
 		Href string `json:"href"`
 	} `json:"avatar"`
@@ -69,12 +70,7 @@ func (g bitbucketProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (
 		return nil, err
 	}
 
-	data := &UserProvidedData{
-		Metadata: map[string]string{
-			nameKey:      u.Name,
-			avatarURLKey: u.Avatar.Href,
-		},
-	}
+	data := &UserProvidedData{}
 
 	var emails bitbucketEmails
 	if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/user/emails", &emails); err != nil {
@@ -97,5 +93,17 @@ func (g bitbucketProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (
 		return nil, errors.New("Unable to find email with Bitbucket provider")
 	}
 
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.ID,
+		Name:    u.Name,
+		Picture: u.Avatar.Href,
+
+		// To be deprecated
+		AvatarURL:  u.Avatar.Href,
+		FullName:   u.Name,
+		ProviderId: u.ID,
+	}
+
 	return data, nil
 }
diff --git a/api/provider/discord.go b/api/provider/discord.go
index 760564de..b323abfc 100644
--- a/api/provider/discord.go
+++ b/api/provider/discord.go
@@ -92,10 +92,18 @@ func (g discordProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 	}
 
 	return &UserProvidedData{
-		Metadata: map[string]string{
-			avatarURLKey:  avatarURL,
-			nameKey:       u.Name,
-			providerIDKey: u.ID,
+		Metadata: &Claims{
+			Issuer:        g.APIPath,
+			Subject:       u.ID,
+			Name:          u.Name,
+			Picture:       avatarURL,
+			Email:         u.Email,
+			EmailVerified: u.Verified,
+
+			// To be deprecated
+			AvatarURL:  avatarURL,
+			FullName:   u.Name,
+			ProviderId: u.ID,
 		},
 		Emails: []Email{{
 			Email:    u.Email,
diff --git a/api/provider/facebook.go b/api/provider/facebook.go
index 9c9407b4..156552b2 100644
--- a/api/provider/facebook.go
+++ b/api/provider/facebook.go
@@ -24,6 +24,7 @@ type facebookProvider struct {
 }
 
 type facebookUser struct {
+	ID        string `json:"id"`
 	Email     string `json:"email"`
 	FirstName string `json:"first_name"`
 	LastName  string `json:"last_name"`
@@ -88,10 +89,20 @@ func (p facebookProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*
 	}
 
 	return &UserProvidedData{
-		Metadata: map[string]string{
-			aliasKey:     u.Alias,
-			nameKey:      strings.TrimSpace(u.FirstName + " " + u.LastName),
-			avatarURLKey: u.Avatar.Data.URL,
+		Metadata: &Claims{
+			Issuer:        p.ProfileURL,
+			Subject:       u.ID,
+			Name:          strings.TrimSpace(u.FirstName + " " + u.LastName),
+			NickName:      u.Alias,
+			Email:         u.Email,
+			EmailVerified: true, // if email is returned, the email is verified by facebook already
+			Picture:       u.Avatar.Data.URL,
+
+			// To be deprecated
+			Slug:       u.Alias,
+			AvatarURL:  u.Avatar.Data.URL,
+			FullName:   strings.TrimSpace(u.FirstName + " " + u.LastName),
+			ProviderId: u.ID,
 		},
 		Emails: []Email{{
 			Email:    u.Email,
diff --git a/api/provider/github.go b/api/provider/github.go
index 76e8bb45..cba57f10 100644
--- a/api/provider/github.go
+++ b/api/provider/github.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"context"
 	"errors"
+	"strconv"
 	"strings"
 
 	"github.com/netlify/gotrue/conf"
@@ -22,6 +23,7 @@ type githubProvider struct {
 }
 
 type githubUser struct {
+	ID        int    `json:"id"`
 	UserName  string `json:"login"`
 	Email     string `json:"email"`
 	Name      string `json:"name"`
@@ -80,10 +82,17 @@ func (g githubProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 	}
 
 	data := &UserProvidedData{
-		Metadata: map[string]string{
-			userNameKey:  u.UserName,
-			nameKey:      u.Name,
-			avatarURLKey: u.AvatarURL,
+		Metadata: &Claims{
+			Issuer:            g.APIHost,
+			Subject:           strconv.Itoa(u.ID),
+			Name:              u.Name,
+			PreferredUsername: u.UserName,
+
+			// To be deprecated
+			AvatarURL:   u.AvatarURL,
+			FullName:    u.Name,
+			ProviderId:  strconv.Itoa(u.ID),
+			UserNameKey: u.UserName,
 		},
 	}
 
@@ -96,6 +105,11 @@ func (g githubProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		if e.Email != "" {
 			data.Emails = append(data.Emails, Email{Email: e.Email, Verified: e.Verified, Primary: e.Primary})
 		}
+
+		if e.Primary {
+			data.Metadata.Email = e.Email
+			data.Metadata.EmailVerified = e.Verified
+		}
 	}
 
 	if len(data.Emails) <= 0 {
diff --git a/api/provider/gitlab.go b/api/provider/gitlab.go
index f63eefd7..1d5c7824 100644
--- a/api/provider/gitlab.go
+++ b/api/provider/gitlab.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"context"
 	"errors"
+	"strconv"
 	"strings"
 
 	"github.com/netlify/gotrue/conf"
@@ -23,6 +24,7 @@ type gitlabUser struct {
 	Name        string `json:"name"`
 	AvatarURL   string `json:"avatar_url"`
 	ConfirmedAt string `json:"confirmed_at"`
+	ID          int    `json:"id"`
 }
 
 type gitlabUserEmail struct {
@@ -71,12 +73,7 @@ func (g gitlabProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		return nil, err
 	}
 
-	data := &UserProvidedData{
-		Metadata: map[string]string{
-			nameKey:      u.Name,
-			avatarURLKey: u.AvatarURL,
-		},
-	}
+	data := &UserProvidedData{}
 
 	var emails []*gitlabUserEmail
 	if err := makeRequest(ctx, tok, g.Config, g.Host+"/api/v4/user/emails", &emails); err != nil {
@@ -99,5 +96,19 @@ func (g gitlabProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		return nil, errors.New("Unable to find email with GitLab provider")
 	}
 
+	data.Metadata = &Claims{
+		Issuer:        g.Host,
+		Subject:       strconv.Itoa(u.ID),
+		Name:          u.Name,
+		Picture:       u.AvatarURL,
+		Email:         u.Email,
+		EmailVerified: true,
+
+		// To be deprecated
+		AvatarURL:  u.AvatarURL,
+		FullName:   u.Name,
+		ProviderId: strconv.Itoa(u.ID),
+	}
+
 	return data, nil
 }
diff --git a/api/provider/google.go b/api/provider/google.go
index bf9de6d7..e2e060ec 100644
--- a/api/provider/google.go
+++ b/api/provider/google.go
@@ -20,6 +20,7 @@ type googleProvider struct {
 }
 
 type googleUser struct {
+	ID            string `json:"id"`
 	Name          string `json:"name"`
 	AvatarURL     string `json:"picture"`
 	Email         string `json:"email"`
@@ -69,12 +70,7 @@ func (g googleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		return nil, err
 	}
 
-	data := &UserProvidedData{
-		Metadata: map[string]string{
-			nameKey:      u.Name,
-			avatarURLKey: u.AvatarURL,
-		},
-	}
+	data := &UserProvidedData{}
 
 	if u.Email != "" {
 		data.Emails = append(data.Emails, Email{
@@ -88,5 +84,19 @@ func (g googleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		return nil, errors.New("Unable to find email with Google provider")
 	}
 
+	data.Metadata = &Claims{
+		Issuer:        g.APIPath,
+		Subject:       u.ID,
+		Name:          u.Name,
+		Picture:       u.AvatarURL,
+		Email:         u.Email,
+		EmailVerified: u.EmailVerified,
+
+		// To be deprecated
+		AvatarURL:  u.AvatarURL,
+		FullName:   u.Name,
+		ProviderId: u.ID,
+	}
+
 	return data, nil
 }
diff --git a/api/provider/provider.go b/api/provider/provider.go
index 410389e6..11964572 100644
--- a/api/provider/provider.go
+++ b/api/provider/provider.go
@@ -7,13 +7,58 @@ import (
 	"golang.org/x/oauth2"
 )
 
-const (
-	userNameKey   = "user_name"
-	avatarURLKey  = "avatar_url"
-	nameKey       = "full_name"
-	aliasKey      = "slug"
-	providerIDKey = "provider_id"
-)
+type Claims struct {
+	// Reserved claims
+	Issuer  string `json:"iss,omitempty"`
+	Subject string `json:"sub,omitempty"`
+	Aud     string `json:"aud,omitempty"`
+	Iat     string `json:"iat,omitempty"`
+	Exp     string `json:"exp,omitempty"`
+
+	// Default profile claims
+	Name              string `json:"name,omitempty"`
+	FamilyName        string `json:"family_name,omitempty"`
+	GivenName         string `json:"given_name,omitempty"`
+	MiddleName        string `json:"middle_name,omitempty"`
+	NickName          string `json:"nickname,omitempty"`
+	PreferredUsername string `json:"preferred_username,omitempty"`
+	Profile           string `json:"profile,omitempty"`
+	Picture           string `json:"picture,omitempty"`
+	Website           string `json:"website,omitempty"`
+	Gender            string `json:"gender,omitempty"`
+	Birthdate         string `json:"birthdate,omitempty"`
+	ZoneInfo          string `json:"zoneinfo,omitempty"`
+	Locale            string `json:"locale,omitempty"`
+	UpdatedAt         string `json:"updated_at,omitempty"`
+	Email             string `json:"email,omitempty"`
+	EmailVerified     bool   `json:"email_verified,omitempty"`
+	Phone             string `json:"phone,omitempty"`
+	PhoneVerified     bool   `json:"phone_verified,omitempty"`
+
+	// Custom profile claims that are provider specific
+	CustomClaims map[string]interface{} `json:"custom_claims,omitempty"`
+
+	// TODO: Deprecate in next major release
+	FullName    string `json:"full_name,omitempty"`
+	AvatarURL   string `json:"avatar_url,omitempty"`
+	Slug        string `json:"slug,omitempty"`
+	ProviderId  string `json:"provider_id,omitempty"`
+	UserNameKey string `json:"user_name,omitempty"`
+}
+
+// ToMap converts the Claims struct to a map[string]interface{}
+func (c *Claims) ToMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+	cBytes, err := json.Marshal(c)
+	if err != nil {
+		return nil, err
+	}
+	err = json.Unmarshal(cBytes, &m)
+	if err != nil {
+		return nil, err
+	}
+	return m, nil
+}
 
 // Email is a struct that provides information on whether an email is verified or is the primary email address
 type Email struct {
@@ -25,7 +70,7 @@ type Email struct {
 // UserProvidedData is a struct that contains the user's data returned from the oauth provider
 type UserProvidedData struct {
 	Emails   []Email
-	Metadata map[string]string
+	Metadata *Claims
 }
 
 // Provider is an interface for interacting with external account providers
diff --git a/api/provider/slack.go b/api/provider/slack.go
new file mode 100644
index 00000000..896bc8d0
--- /dev/null
+++ b/api/provider/slack.go
@@ -0,0 +1,95 @@
+package provider
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/netlify/gotrue/conf"
+	"golang.org/x/oauth2"
+)
+
+const defaultSlackApiBase = "slack.com"
+
+type slackProvider struct {
+	*oauth2.Config
+	APIPath string
+}
+
+type slackUser struct {
+	ID        string `json:"https://slack.com/user_id"`
+	Email     string `json:"email"`
+	Name      string `json:"name"`
+	AvatarURL string `json:"picture"`
+}
+
+// NewSlackProvider creates a Slack account provider.
+func NewSlackProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
+	if err := ext.Validate(); err != nil {
+		return nil, err
+	}
+
+	apiPath := chooseHost(ext.URL, defaultSlackApiBase) + "/api"
+	authPath := chooseHost(ext.URL, defaultSlackApiBase) + "/oauth"
+
+	oauthScopes := []string{
+		"profile",
+		"email",
+		"openid",
+	}
+
+	if scopes != "" {
+		oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
+	}
+
+	return &slackProvider{
+		Config: &oauth2.Config{
+			ClientID:     ext.ClientID,
+			ClientSecret: ext.Secret,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:  authPath + "/authorize",
+				TokenURL: apiPath + "/oauth.access",
+			},
+			Scopes:      oauthScopes,
+			RedirectURL: ext.RedirectURI,
+		},
+		APIPath: apiPath,
+	}, nil
+}
+
+func (g slackProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
+	return g.Exchange(oauth2.NoContext, code)
+}
+
+func (g slackProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
+	var u slackUser
+	if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/openid.connect.userInfo", &u); err != nil {
+		return nil, err
+	}
+	fmt.Printf("%+v\n", u)
+	if u.Email == "" {
+		return nil, errors.New("Unable to find email with Slack provider")
+	}
+
+	return &UserProvidedData{
+		Metadata: &Claims{
+			Issuer:        g.APIPath,
+			Subject:       u.ID,
+			Name:          u.Name,
+			Picture:       u.AvatarURL,
+			Email:         u.Email,
+			EmailVerified: true, // Slack dosen't provide data on if email is verified.
+
+			// To be deprecated
+			AvatarURL:  u.AvatarURL,
+			FullName:   u.Name,
+			ProviderId: u.ID,
+		},
+		Emails: []Email{{
+			Email:    u.Email,
+			Verified: true, // Slack dosen't provide data on if email is verified.
+			Primary:  true,
+		}},
+	}, nil
+}
diff --git a/api/provider/spotify.go b/api/provider/spotify.go
new file mode 100644
index 00000000..0334b21c
--- /dev/null
+++ b/api/provider/spotify.go
@@ -0,0 +1,107 @@
+package provider
+
+import (
+	"context"
+	"errors"
+	"strings"
+
+	"github.com/netlify/gotrue/conf"
+	"golang.org/x/oauth2"
+)
+
+const (
+	defaultSpotifyAPIBase  = "api.spotify.com/v1"   // Used to get user data
+	defaultSpotifyAuthBase = "accounts.spotify.com" // Used for OAuth flow
+)
+
+type spotifyProvider struct {
+	*oauth2.Config
+	APIPath string
+}
+
+type spotifyUser struct {
+	DisplayName string             `json:"display_name"`
+	Avatars     []spotifyUserImage `json:"images"`
+	Email       string             `json:"email"`
+	ID          string             `json:"id"`
+}
+
+type spotifyUserImage struct {
+	Url    string `json:"url"`
+	Height int    `json:"height"`
+	Width  int    `json:"width"`
+}
+
+// NewSpotifyProvider creates a Spotify account provider.
+func NewSpotifyProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
+	if err := ext.Validate(); err != nil {
+		return nil, err
+	}
+
+	apiPath := chooseHost(ext.URL, defaultSpotifyAPIBase)
+	authPath := chooseHost(ext.URL, defaultSpotifyAuthBase)
+
+	oauthScopes := []string{
+		"user-read-email",
+	}
+
+	if scopes != "" {
+		oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
+	}
+
+	return &spotifyProvider{
+		Config: &oauth2.Config{
+			ClientID:     ext.ClientID,
+			ClientSecret: ext.Secret,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:  authPath + "/authorize",
+				TokenURL: authPath + "/api/token",
+			},
+			Scopes:      oauthScopes,
+			RedirectURL: ext.RedirectURI,
+		},
+		APIPath: apiPath,
+	}, nil
+}
+
+func (g spotifyProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
+	return g.Exchange(oauth2.NoContext, code)
+}
+
+func (g spotifyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
+	var u spotifyUser
+	if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/me", &u); err != nil {
+		return nil, err
+	}
+
+	if u.Email == "" {
+		return nil, errors.New("Unable to find email with Spotify provider")
+	}
+
+	var avatarURL string
+
+	if len(u.Avatars) >= 1 {
+		avatarURL = u.Avatars[0].Url
+	}
+
+	return &UserProvidedData{
+		Metadata: &Claims{
+			Issuer:        g.APIPath,
+			Subject:       u.ID,
+			Name:          u.DisplayName,
+			Picture:       avatarURL,
+			Email:         u.Email,
+			EmailVerified: true, // Spotify dosen't provide data on if email is verified.
+
+			// To be deprecated
+			AvatarURL:  avatarURL,
+			FullName:   u.DisplayName,
+			ProviderId: u.ID,
+		},
+		Emails: []Email{{
+			Email:    u.Email,
+			Verified: true, // Spotify dosen't provide data on if email is verified.
+			Primary:  true,
+		}},
+	}, nil
+}
diff --git a/api/provider/twitch.go b/api/provider/twitch.go
index edcea510..01bb806d 100644
--- a/api/provider/twitch.go
+++ b/api/provider/twitch.go
@@ -113,10 +113,27 @@ func (t twitchProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 	}
 
 	data := &UserProvidedData{
-		Metadata: map[string]string{
-			nameKey:      user.Login,
-			aliasKey:     user.DisplayName,
-			avatarURLKey: user.ProfileImageURL,
+		Metadata: &Claims{
+			Issuer:        t.APIHost,
+			Subject:       user.ID,
+			Picture:       user.ProfileImageURL,
+			Name:          user.Login,
+			NickName:      user.DisplayName,
+			Email:         user.Email,
+			EmailVerified: true,
+			CustomClaims: map[string]interface{}{
+				"broadcaster_type":  user.BroadcasterType,
+				"description":       user.Description,
+				"type":              user.Type,
+				"offline_image_url": user.OfflineImageURL,
+				"view_count":        user.ViewCount,
+			},
+
+			// To be deprecated
+			Slug:       user.DisplayName,
+			AvatarURL:  user.ProfileImageURL,
+			FullName:   user.Login,
+			ProviderId: user.ID,
 		},
 		Emails: []Email{{
 			Email:    user.Email,
diff --git a/api/provider/twitter.go b/api/provider/twitter.go
index a046e976..a3a752c3 100644
--- a/api/provider/twitter.go
+++ b/api/provider/twitter.go
@@ -40,6 +40,7 @@ type twitterUser struct {
 	Name      string `json:"name"`
 	AvatarURL string `json:"profile_image_url_https"`
 	Email     string `json:"email"`
+	ID        string `json:"id_str"`
 }
 
 // NewTwitterProvider creates a Twitter account provider.
@@ -94,10 +95,20 @@ func (t TwitterProvider) FetchUserData(ctx context.Context, tok *oauth.AccessTok
 	}
 
 	data := &UserProvidedData{
-		Metadata: map[string]string{
-			userNameKey:  u.UserName,
-			nameKey:      u.Name,
-			avatarURLKey: u.AvatarURL,
+		Metadata: &Claims{
+			Issuer:            t.UserInfoURL,
+			Subject:           u.ID,
+			Name:              u.Name,
+			Picture:           u.AvatarURL,
+			PreferredUsername: u.UserName,
+			Email:             u.Email,
+			EmailVerified:     true,
+
+			// To be deprecated
+			UserNameKey: u.UserName,
+			FullName:    u.Name,
+			AvatarURL:   u.AvatarURL,
+			ProviderId:  u.ID,
 		},
 		Emails: []Email{{
 			Email:    u.Email,
@@ -105,6 +116,7 @@ func (t TwitterProvider) FetchUserData(ctx context.Context, tok *oauth.AccessTok
 			Primary:  true,
 		}},
 	}
+
 	return data, nil
 }
 
diff --git a/api/settings.go b/api/settings.go
index a407e52d..56b02a63 100644
--- a/api/settings.go
+++ b/api/settings.go
@@ -11,6 +11,8 @@ type ProviderSettings struct {
 	GitLab    bool `json:"gitlab"`
 	Google    bool `json:"google"`
 	Facebook  bool `json:"facebook"`
+	Spotify   bool `json:"spotify"`
+	Slack     bool `json:"slack"`
 	Twitch    bool `json:"twitch"`
 	Twitter   bool `json:"twitter"`
 	Email     bool `json:"email"`
@@ -44,6 +46,8 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
 			GitLab:    config.External.Gitlab.Enabled,
 			Google:    config.External.Google.Enabled,
 			Facebook:  config.External.Facebook.Enabled,
+			Spotify:   config.External.Spotify.Enabled,
+			Slack:     config.External.Slack.Enabled,
 			Twitch:    config.External.Twitch.Enabled,
 			Twitter:   config.External.Twitter.Enabled,
 			Email:     config.External.Email.Enabled,
diff --git a/api/signup.go b/api/signup.go
index 8e280bb1..6edde2bd 100644
--- a/api/signup.go
+++ b/api/signup.go
@@ -78,7 +78,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 		}
 		user, err = models.FindUserByPhoneAndAudience(a.db, instanceID, params.Phone, params.Aud)
 	default:
-		return unprocessableEntityError("Signup provider must be either email or phone")
+		return invalidSignupError(config)
 	}
 
 	if err != nil && !models.IsNotFoundError(err) {
@@ -89,11 +89,11 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 		var terr error
 		if user != nil {
 			if params.Provider == "email" && user.IsConfirmed() {
-				return badRequestError("Thanks for registering, now check your email to complete the process.")
+				return UserExistsError
 			}
 
 			if params.Provider == "phone" && user.IsPhoneConfirmed() {
-				return badRequestError("A user with this phone number has already been registered")
+				return UserExistsError
 			}
 
 			if err := user.UpdateUserMetaData(tx, params.Data); err != nil {
@@ -160,6 +160,12 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 		if errors.Is(err, MaxFrequencyLimitError) {
 			return tooManyRequestsError("For security purposes, you can only request this once every minute")
 		}
+		if errors.Is(err, UserExistsError) {
+			sanitizedUser := sanitizeUser(user, params)
+
+			// return sanitized user
+			return sendJSON(w, http.StatusOK, sanitizedUser)
+		}
 		return err
 	}
 
@@ -198,6 +204,34 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 	return sendJSON(w, http.StatusOK, user)
 }
 
+func sanitizeUser(user *models.User, params *SignupParams) *models.User {
+	u := user
+	now := time.Now()
+
+	u.CreatedAt, u.UpdatedAt, u.ConfirmationSentAt, u.LastSignInAt, u.ConfirmedAt = now, now, &now, &now, &now
+	u.Identities = make([]models.Identity, 0)
+	u.UserMetaData = params.Data
+	u.Aud = params.Aud
+
+	// sanitize app_metadata
+	u.AppMetaData = map[string]interface{}{
+		"provider":  params.Provider,
+		"providers": []string{params.Provider},
+	}
+
+	// sanitize param fields
+	switch params.Provider {
+	case "email":
+		u.PhoneConfirmedAt, u.EmailConfirmedAt, u.Phone = nil, &now, ""
+	case "phone":
+		u.PhoneConfirmedAt, u.EmailConfirmedAt, u.Email = &now, nil, ""
+	default:
+		u.Phone, u.EmailConfirmedAt, u.PhoneConfirmedAt, u.Email = "", nil, nil, ""
+	}
+
+	return u
+}
+
 func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, params *SignupParams) (*models.User, error) {
 	instanceID := getInstanceID(ctx)
 	config := a.getConfig(ctx)
@@ -221,8 +255,13 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param
 	if user.AppMetaData == nil {
 		user.AppMetaData = make(map[string]interface{})
 	}
+
+	user.Identities = make([]models.Identity, 0)
+
+	// TODO: Depcreate "provider" field
 	user.AppMetaData["provider"] = params.Provider
 
+	user.AppMetaData["providers"] = []string{params.Provider}
 	if params.Password == "" {
 		user.EncryptedPassword = ""
 	}
diff --git a/api/sms_provider/messagebird.go b/api/sms_provider/messagebird.go
new file mode 100644
index 00000000..5104bb61
--- /dev/null
+++ b/api/sms_provider/messagebird.go
@@ -0,0 +1,101 @@
+package sms_provider
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/netlify/gotrue/conf"
+)
+
+const (
+	defaultMessagebirdApiBase = "https://rest.messagebird.com"
+)
+
+type MessagebirdProvider struct {
+	Config  *conf.MessagebirdProviderConfiguration
+	APIPath string
+}
+
+type MessagebirdResponseRecipients struct {
+	TotalSentCount int `json:"totalSentCount"`
+}
+
+type MessagebirdResponse struct {
+	Recipients MessagebirdResponseRecipients `json:"recipients"`
+}
+
+type MessagebirdError struct {
+	Code        int    `json:"code"`
+	Description string `json:"description"`
+	parameter   string `json:"parameter"`
+}
+
+
+type MessagebirdErrResponse struct {
+	Errors []MessagebirdError `json:"errors"`
+}
+
+func (t MessagebirdErrResponse) Error() string {
+	return t.Errors[0].Description
+}
+
+// Creates a SmsProvider with the Messagebird Config
+func NewMessagebirdProvider(config conf.MessagebirdProviderConfiguration) (SmsProvider, error) {
+	if err := config.Validate(); err != nil {
+		return nil, err
+	}
+
+	apiPath := defaultMessagebirdApiBase + "/messages"
+	return &MessagebirdProvider{
+		Config:  &config,
+		APIPath: apiPath,
+	}, nil
+}
+
+// Send an SMS containing the OTP with Messagebird's API
+func (t MessagebirdProvider) SendSms(phone string, message string) error {
+	body := url.Values{
+		"originator": {t.Config.Originator},
+		"body":    {message},
+		"recipients":    {phone},
+		"type": {"sms"},
+		"datacoding": {"unicode"},
+	}
+
+	client := &http.Client{}
+	r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode()))
+	if err != nil {
+		return err
+	}
+	r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	r.Header.Add("Authorization", "AccessKey " + t.Config.AccessKey)
+	res, err := client.Do(r)
+	if err != nil {
+		return err
+	}
+
+	if res.StatusCode == http.StatusBadRequest || res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusUnprocessableEntity {
+		resp := &MessagebirdErrResponse{}
+		if err := json.NewDecoder(res.Body).Decode(resp); err != nil {
+			return err
+		}
+		return resp
+	}
+	defer res.Body.Close()
+
+	// validate sms status
+	resp := &MessagebirdResponse{}
+	derr := json.NewDecoder(res.Body).Decode(resp)
+	if derr != nil {
+		return derr
+	}
+
+	if resp.Recipients.TotalSentCount == 0 {
+		return fmt.Errorf("Messagebird error: total sent count is 0")
+	}
+
+	return nil
+}
diff --git a/api/sms_provider/sms_provider.go b/api/sms_provider/sms_provider.go
index 338b09e4..50181545 100644
--- a/api/sms_provider/sms_provider.go
+++ b/api/sms_provider/sms_provider.go
@@ -14,6 +14,8 @@ func GetSmsProvider(config conf.Configuration) (SmsProvider, error) {
 	switch name := config.Sms.Provider; name {
 	case "twilio":
 		return NewTwilioProvider(config.Sms.Twilio)
+	case "messagebird":
+		return NewMessagebirdProvider(config.Sms.Messagebird)
 	default:
 		return nil, fmt.Errorf("Sms Provider %s could not be found", name)
 	}
diff --git a/api/token.go b/api/token.go
index 404041df..6a23a0cf 100644
--- a/api/token.go
+++ b/api/token.go
@@ -46,6 +46,7 @@ type RefreshTokenGrantParams struct {
 
 const useCookieHeader = "x-use-cookie"
 const useSessionCookie = "session"
+const InvalidLoginMessage = "Invalid login credentials"
 
 // Token is the endpoint for OAuth access token requests
 func (a *API) Token(w http.ResponseWriter, r *http.Request) error {
@@ -88,12 +89,12 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 		params.Phone = a.formatPhoneNumber(params.Phone)
 		user, err = models.FindUserByPhoneAndAudience(a.db, instanceID, params.Phone, aud)
 	} else {
-		return oauthError("invalid_grant", "Invalid login credentials")
+		return oauthError("invalid_grant", InvalidLoginMessage)
 	}
 
 	if err != nil {
 		if models.IsNotFoundError(err) {
-			return oauthError("invalid_grant", "Invalid login credentials")
+			return oauthError("invalid_grant", InvalidLoginMessage)
 		}
 		return internalServerError("Database error finding user").WithInternalError(err)
 	}
@@ -105,7 +106,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 	}
 
 	if !user.Authenticate(params.Password) {
-		return oauthError("invalid_grant", "Invalid email or password")
+		return oauthError("invalid_grant", InvalidLoginMessage)
 	}
 
 	var token *AccessTokenResponse
diff --git a/api/user.go b/api/user.go
index 82fcfda6..bbe24df8 100644
--- a/api/user.go
+++ b/api/user.go
@@ -120,8 +120,14 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error {
 
 			mailer := a.Mailer(ctx)
 			referrer := a.getReferrer(r)
-			if terr = a.sendEmailChange(tx, user, mailer, params.Email, referrer); terr != nil {
-				return internalServerError("Error sending change email").WithInternalError(terr)
+			if config.Mailer.SecureEmailChangeEnabled {
+				if terr = a.sendSecureEmailChange(tx, user, mailer, params.Email, referrer); terr != nil {
+					return internalServerError("Error sending change email").WithInternalError(terr)
+				}
+			} else {
+				if terr = a.sendEmailChange(tx, user, mailer, params.Email, referrer); terr != nil {
+					return internalServerError("Error sending change email").WithInternalError(terr)
+				}
 			}
 		}
 
diff --git a/api/verify.go b/api/verify.go
index 98d6d485..4fed1239 100644
--- a/api/verify.go
+++ b/api/verify.go
@@ -29,8 +29,8 @@ const (
 )
 
 const (
-	noneConfirmed int = iota
-	oneConfirmed
+	zeroConfirmation int = iota
+	singleConfirmation
 )
 
 // VerifyParams are the parameters the Verify endpoint accepts
@@ -87,6 +87,12 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error {
 			user, terr = a.recoverVerify(ctx, tx, params)
 		case emailChangeVerification:
 			user, terr = a.emailChangeVerify(ctx, tx, params)
+			if user == nil && terr == nil {
+				// when double confirmation is required
+				rurl := a.prepRedirectURL("Confirmation link accepted. Please proceed to confirm link sent to the other email", params.RedirectTo)
+				http.Redirect(w, r, rurl, http.StatusFound)
+				return nil
+			}
 		case smsVerification:
 			if params.Phone == "" {
 				return unprocessableEntityError("Sms Verification requires a phone number")
@@ -297,6 +303,12 @@ func (a *API) prepErrorRedirectURL(err *HTTPError, r *http.Request, rurl string)
 	return rurl + "#" + q.Encode()
 }
 
+func (a *API) prepRedirectURL(message string, rurl string) string {
+	q := url.Values{}
+	q.Set("message", message)
+	return rurl + "#" + q.Encode()
+}
+
 func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, params *VerifyParams) (*models.User, error) {
 	instanceID := getInstanceID(ctx)
 	config := a.getConfig(ctx)
@@ -311,7 +323,7 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p
 	nextDay := user.EmailChangeSentAt.Add(24 * time.Hour)
 	if user.EmailChangeSentAt != nil && time.Now().After(nextDay) {
 		err = a.db.Transaction(func(tx *storage.Connection) error {
-			user.EmailChangeConfirmStatus = noneConfirmed
+			user.EmailChangeConfirmStatus = zeroConfirmation
 			return tx.UpdateOnly(user, "email_change_confirm_status")
 		})
 		if err != nil {
@@ -320,23 +332,25 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p
 		return nil, expiredTokenError("Email change token expired").WithInternalError(redirectWithQueryError)
 	}
 
-	if user.EmailChangeConfirmStatus == noneConfirmed {
-		err = a.db.Transaction(func(tx *storage.Connection) error {
-			user.EmailChangeConfirmStatus = oneConfirmed
-			if params.Token == user.EmailChangeTokenCurrent {
-				user.EmailChangeTokenCurrent = ""
-			} else if params.Token == user.EmailChangeTokenNew {
-				user.EmailChangeTokenNew = ""
-			}
-			if terr := tx.UpdateOnly(user, "email_change_confirm_status", "email_change_token_current", "email_change_token_new"); terr != nil {
-				return terr
+	if config.Mailer.SecureEmailChangeEnabled {
+		if user.EmailChangeConfirmStatus == zeroConfirmation {
+			err = a.db.Transaction(func(tx *storage.Connection) error {
+				user.EmailChangeConfirmStatus = singleConfirmation
+				if params.Token == user.EmailChangeTokenCurrent {
+					user.EmailChangeTokenCurrent = ""
+				} else if params.Token == user.EmailChangeTokenNew {
+					user.EmailChangeTokenNew = ""
+				}
+				if terr := tx.UpdateOnly(user, "email_change_confirm_status", "email_change_token_current", "email_change_token_new"); terr != nil {
+					return terr
+				}
+				return nil
+			})
+			if err != nil {
+				return nil, err
 			}
-			return nil
-		})
-		if err != nil {
-			return nil, err
+			return nil, nil
 		}
-		return nil, acceptedTokenError("Email change request accepted").WithInternalError(redirectWithQueryError)
 	}
 
 	// one email is confirmed at this point
@@ -351,7 +365,7 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p
 			return terr
 		}
 
-		if terr = user.ConfirmEmailChange(tx, noneConfirmed); terr != nil {
+		if terr = user.ConfirmEmailChange(tx, zeroConfirmation); terr != nil {
 			return internalServerError("Error confirm email").WithInternalError(terr)
 		}
 
diff --git a/conf/configuration.go b/conf/configuration.go
index e2cace74..454d3305 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -59,14 +59,15 @@ type GlobalConfiguration struct {
 		RequestIDHeader string `envconfig:"REQUEST_ID_HEADER"`
 		ExternalURL     string `json:"external_url" envconfig:"API_EXTERNAL_URL"`
 	}
-	DB                DBConfiguration
-	External          ProviderConfiguration
-	Logging           LoggingConfig `envconfig:"LOG"`
-	OperatorToken     string        `split_words:"true" required:"false"`
-	MultiInstanceMode bool
-	Tracing           TracingConfig
-	SMTP              SMTPConfiguration
-	RateLimitHeader   string `split_words:"true"`
+	DB                 DBConfiguration
+	External           ProviderConfiguration
+	Logging            LoggingConfig `envconfig:"LOG"`
+	OperatorToken      string        `split_words:"true" required:"false"`
+	MultiInstanceMode  bool
+	Tracing            TracingConfig
+	SMTP               SMTPConfiguration
+	RateLimitHeader    string  `split_words:"true"`
+	RateLimitEmailSent float64 `split_words:"true" default:"30"`
 }
 
 // EmailContentConfiguration holds the configuration for emails, both subjects and template URLs.
@@ -87,6 +88,8 @@ type ProviderConfiguration struct {
 	Github      OAuthProviderConfiguration `json:"github"`
 	Gitlab      OAuthProviderConfiguration `json:"gitlab"`
 	Google      OAuthProviderConfiguration `json:"google"`
+	Spotify     OAuthProviderConfiguration `json:"spotify"`
+	Slack       OAuthProviderConfiguration `json:"slack"`
 	Twitter     OAuthProviderConfiguration `json:"twitter"`
 	Twitch      OAuthProviderConfiguration `json:"twitch"`
 	Email       EmailProviderConfiguration `json:"email"`
@@ -106,10 +109,11 @@ type SMTPConfiguration struct {
 }
 
 type MailerConfiguration struct {
-	Autoconfirm bool                      `json:"autoconfirm"`
-	Subjects    EmailContentConfiguration `json:"subjects"`
-	Templates   EmailContentConfiguration `json:"templates"`
-	URLPaths    EmailContentConfiguration `json:"url_paths"`
+	Autoconfirm              bool                      `json:"autoconfirm"`
+	Subjects                 EmailContentConfiguration `json:"subjects"`
+	Templates                EmailContentConfiguration `json:"templates"`
+	URLPaths                 EmailContentConfiguration `json:"url_paths"`
+	SecureEmailChangeEnabled bool                      `json:"secure_email_change_enabled" split_words:"true" default:"true"`
 }
 
 type PhoneProviderConfiguration struct {
@@ -117,13 +121,14 @@ type PhoneProviderConfiguration struct {
 }
 
 type SmsProviderConfiguration struct {
-	Autoconfirm  bool                        `json:"autoconfirm"`
-	MaxFrequency time.Duration               `json:"max_frequency" split_words:"true"`
-	OtpExp       uint                        `json:"otp_exp" split_words:"true"`
-	OtpLength    int                         `json:"otp_length" split_words:"true"`
-	Provider     string                      `json:"provider"`
-	Template     string                      `json:"template"`
-	Twilio       TwilioProviderConfiguration `json:"twilio"`
+	Autoconfirm  bool                             `json:"autoconfirm"`
+	MaxFrequency time.Duration                    `json:"max_frequency" split_words:"true"`
+	OtpExp       uint                             `json:"otp_exp" split_words:"true"`
+	OtpLength    int                              `json:"otp_length" split_words:"true"`
+	Provider     string                           `json:"provider"`
+	Template     string                           `json:"template"`
+	Twilio       TwilioProviderConfiguration      `json:"twilio"`
+	Messagebird  MessagebirdProviderConfiguration `json:"messagebird"`
 }
 
 type TwilioProviderConfiguration struct {
@@ -132,6 +137,11 @@ type TwilioProviderConfiguration struct {
 	MessageServiceSid string `json:"message_service_sid" split_words:"true"`
 }
 
+type MessagebirdProviderConfiguration struct {
+	AccessKey  string `json:"access_key" split_words:"true"`
+	Originator string `json:"originator" split_words:"true"`
+}
+
 type CaptchaConfiguration struct {
 	Enabled  bool   `json:"enabled" default:"false"`
 	Provider string `json:"provider" default:"hcaptcha"`
@@ -343,3 +353,13 @@ func (t *TwilioProviderConfiguration) Validate() error {
 	}
 	return nil
 }
+
+func (t *MessagebirdProviderConfiguration) Validate() error {
+	if t.AccessKey == "" {
+		return errors.New("Missing Messagebird access key")
+	}
+	if t.Originator == "" {
+		return errors.New("Missing Messagebird originator")
+	}
+	return nil
+}
diff --git a/example.env b/example.env
index c0132899..eb4ddc2c 100644
--- a/example.env
+++ b/example.env
@@ -41,6 +41,12 @@ GOTRUE_EXTERNAL_AZURE_SECRET=""
 GOTRUE_EXTERNAL_FACEBOOK_ENABLED="false"
 GOTRUE_EXTERNAL_FACEBOOK_CLIENT_ID=""
 GOTRUE_EXTERNAL_FACEBOOK_SECRET=""
+GOTRUE_EXTERNAL_SPOTIFY_ENABLED="true"
+GOTRUE_EXTERNAL_SPOTIFY_CLIENT_ID=""
+GOTRUE_EXTERNAL_SPOTIFY_SECRET=""
+GOTRUE_EXTERNAL_SLACK_ENABLED="true"
+GOTRUE_EXTERNAL_SLACK_CLIENT_ID=""
+GOTRUE_EXTERNAL_SLACK_SECRET=""
 GOTRUE_EXTERNAL_TWITTER_ENABLED="false"
 GOTRUE_EXTERNAL_TWITTER_CLIENT_ID=""
 GOTRUE_EXTERNAL_TWITTER_SECRET=""
@@ -57,6 +63,12 @@ GOTRUE_EXTERNAL_GITLAB_ENABLED="false"
 GOTRUE_EXTERNAL_GITLAB_CLIENT_ID=""
 GOTRUE_EXTERNAL_GITLAB_SECRET=""
 GOTRUE_EXTERNAL_GITLAB_REDIRECT_URI=""
+GOTRUE_EXTERNAL_SAML_ENABLED="false"
+GOTRUE_EXTERNAL_SAML_METADATA_URL="/"
+GOTRUE_EXTERNAL_SAML_API_BASE="/"
+GOTRUE_EXTERNAL_SAML_NAME="auth0"
+GOTRUE_EXTERNAL_SAML_SIGNING_CERT="/"
+GOTRUE_EXTERNAL_SAML_SIGNING_KEY="/"
 GOTRUE_MAILER_TEMPLATES_INVITE="https://app.supabase.io/api/auth/example-project-ref/templates/invite"
 GOTRUE_MAILER_TEMPLATES_CONFIRMATION="https://app.supabase.io/api/auth/example-project-ref/templates/confirmation"
 GOTRUE_MAILER_TEMPLATES_RECOVERY="https://app.supabase.io/api/auth/example-project-ref/templates/recovery"
@@ -70,6 +82,8 @@ GOTRUE_EXTERNAL_AZURE_REDIRECT_URI="http://example.com/callback"
 GOTRUE_EXTERNAL_FACEBOOK_REDIRECT_URI="http://example.com/callback"
 GOTRUE_EXTERNAL_TWITTER_REDIRECT_URI="http://example.com/callback"
 GOTRUE_EXTERNAL_APPLE_REDIRECT_URI="http://example.com/callback"
+GOTRUE_EXTERNAL_SPOTIFY_REDIRECT_URI="http://example.com/callback"
+GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="http://example.com/callback"
 GOTRUE_EXTERNAL_TWITCH_ENABLED=true
 GOTRUE_EXTERNAL_TWITCH_CLIENT_ID="6ww3qbjo9luvlj22yxk0u0negp381s"
 GOTRUE_EXTERNAL_TWITCH_SECRET="ubu4pfs2ghk39cn0bsxmzyv7cykgu0"
@@ -87,6 +101,8 @@ GOTRUE_SMS_TWILIO_ACCOUNT_SID=""
 GOTRUE_SMS_TWILIO_AUTH_TOKEN=""
 GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID=""
 GOTRUE_SMS_TEMPLATE=""
+GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY=""
+GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR=""
 GOTRUE_WEBHOOK_URL=http://register-lambda:3000/
 GOTRUE_WEBHOOK_SECRET=test_secret
 GOTRUE_WEBHOOK_RETRIES=5
diff --git a/go.mod b/go.mod
index 9f02925f..e5fd1109 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,7 @@
 module github.com/netlify/gotrue
 
 require (
+	cloud.google.com/go v0.67.0 // indirect
 	github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240
 	github.com/Masterminds/semver/v3 v3.1.1 // indirect
 	github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62
@@ -19,6 +20,8 @@ require (
 	github.com/gobuffalo/validate/v3 v3.3.0 // indirect
 	github.com/gofrs/uuid v4.0.0+incompatible
 	github.com/golang-jwt/jwt v3.2.1+incompatible
+	github.com/gorilla/securecookie v1.1.1
+	github.com/gorilla/sessions v1.1.1
 	github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639
 	github.com/jackc/pgproto3/v2 v2.0.7 // indirect
 	github.com/jmoiron/sqlx v1.3.1 // indirect
@@ -26,17 +29,16 @@ require (
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/lestrrat-go/jwx v0.9.0
 	github.com/lib/pq v1.9.0 // indirect
-	github.com/markbates/goth v1.67.1
+	github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.5 // indirect
 	github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450
 	github.com/netlify/mailme v1.1.1
 	github.com/opentracing/opentracing-go v1.1.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pkg/errors v0.9.1
-	github.com/rogpeppe/go-internal v1.7.0 // indirect
 	github.com/rs/cors v1.6.0
 	github.com/russellhaering/gosaml2 v0.6.0
-	github.com/russellhaering/goxmldsig v1.1.0
+	github.com/russellhaering/goxmldsig v1.1.1
 	github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
 	github.com/sethvargo/go-password v0.2.0
 	github.com/sirupsen/logrus v1.7.0
diff --git a/go.sum b/go.sum
index 1b2671ff..d62c86ae 100644
--- a/go.sum
+++ b/go.sum
@@ -89,6 +89,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -232,9 +233,6 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
 github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
-github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
 github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
@@ -333,15 +331,15 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
 github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE=
 github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
 github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jonboulle/clockwork v0.2.0 h1:J2SLSdy7HgElq8ekSl2Mxh6vrRNFxqbXGenYH2I02Vs=
 github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -362,12 +360,15 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lestrrat-go/jwx v0.9.0 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM=
 github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -383,15 +384,13 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
 github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
-github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
-github.com/markbates/goth v1.67.1 h1:gU5B0pzHVyhnJPwGynfFnkfvaQ39C1Sy+ewdl+bhAOw=
-github.com/markbates/goth v1.67.1/go.mod h1:EyLFHGU5ySr2GXRDyJH5nu2dA7parbC8QwIYW/rGcWg=
 github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
 github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
 github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
 github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
-github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro=
 github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
+github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
+github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -413,8 +412,6 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
 github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
-github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
-github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
 github.com/microcosm-cc/bluemonday v1.0.5 h1:cF59UCKMmmUgqN1baLvqU/B1ZsMori+duLVTLpgiG3w=
 github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -428,7 +425,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -456,6 +452,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
 github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
 github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -483,8 +480,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.7.0 h1:3qqXGV8nn7GJT65debw77Dzrx9sfWYgP0DDo7xcMFRk=
-github.com/rogpeppe/go-internal v1.7.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
 github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
@@ -492,8 +490,9 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
 github.com/russellhaering/gosaml2 v0.6.0 h1:OED8FLgczXxXAPlKhnJHQfmEig52tDX2qeXdPtZRIKc=
 github.com/russellhaering/gosaml2 v0.6.0/go.mod h1:CtzxpPr4+bevsATaqR0rw3aqrNlX274b+3C6vFTLCk8=
-github.com/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk=
 github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o=
+github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM=
+github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -662,7 +661,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -894,8 +892,9 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/gomail.v2 v2.0.0-20150902115704-41f357289737/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
@@ -913,8 +912,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20190924164351-c8b7dadae555/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/mailer/template.go b/mailer/template.go
index 99c1c88c..d2764fff 100644
--- a/mailer/template.go
+++ b/mailer/template.go
@@ -23,9 +23,9 @@ const defaultInviteMail = `<h2>You have been invited</h2>
 <p>You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:</p>
 <p><a href="{{ .ConfirmationURL }}">Accept the invite</a></p>`
 
-const defaultConfirmationMail = `<h2>Confirm your signup</h2>
+const defaultConfirmationMail = `<h2>Confirm your email</h2>
 
-<p>Follow this link to confirm your user:</p>
+<p>Follow this link to confirm your email:</p>
 <p><a href="{{ .ConfirmationURL }}">Confirm your email address</a></p>`
 
 const defaultRecoveryMail = `<h2>Reset password</h2>
@@ -102,7 +102,7 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string)
 
 	return m.Mailer.Mail(
 		user.GetEmail(),
-		string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Your Signup")),
+		string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Your Email")),
 		m.Config.Mailer.Templates.Confirmation,
 		defaultConfirmationMail,
 		data,
@@ -111,6 +111,30 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string)
 
 // EmailChangeMail sends an email change confirmation mail to a user
 func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) error {
+	type Email struct {
+		Address  string
+		Token    string
+		Subject  string
+		Template string
+	}
+	emails := []Email{
+		{
+			Address:  user.EmailChange,
+			Token:    user.EmailChangeTokenNew,
+			Subject:  string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")),
+			Template: m.Config.Mailer.Templates.Confirmation,
+		},
+	}
+
+	if m.Config.Mailer.SecureEmailChangeEnabled {
+		emails = append(emails, Email{
+			Address:  user.GetEmail(),
+			Token:    user.EmailChangeTokenCurrent,
+			Subject:  string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Email Address")),
+			Template: m.Config.Mailer.Templates.EmailChange,
+		})
+	}
+
 	globalConfig, err := conf.LoadGlobal(configFile)
 	if err != nil {
 		return err
@@ -120,42 +144,37 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string)
 	if len(referrerURL) > 0 {
 		redirectParam = "&redirect_to=" + referrerURL
 	}
-
 	errors := make(chan error)
-	tokens := map[string]string{
-		user.GetEmail():  user.EmailChangeTokenCurrent,
-		user.EmailChange: user.EmailChangeTokenNew,
-	}
-	for email, token := range tokens {
+	for _, email := range emails {
 		url, err := getSiteURL(
 			referrerURL,
 			globalConfig.API.ExternalURL,
 			m.Config.Mailer.URLPaths.EmailChange,
-			"token="+token+"&type=email_change"+redirectParam,
+			"token="+email.Token+"&type=email_change"+redirectParam,
 		)
 		if err != nil {
 			return err
 		}
-		go func(e, t string) {
+		go func(address, token, template string) {
 			data := map[string]interface{}{
 				"SiteURL":         m.Config.SiteURL,
 				"ConfirmationURL": url,
 				"Email":           user.GetEmail(),
 				"NewEmail":        user.EmailChange,
-				"Token":           t,
+				"Token":           token,
 				"Data":            user.UserMetaData,
 			}
 			errors <- m.Mailer.Mail(
-				e,
+				address,
 				string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")),
-				m.Config.Mailer.Templates.EmailChange,
+				template,
 				defaultEmailChangeMail,
 				data,
 			)
-		}(email, token)
+		}(email.Address, email.Token, email.Template)
 	}
 
-	for i := 0; i < len(tokens); i++ {
+	for i := 0; i < len(emails); i++ {
 		e := <-errors
 		if e != nil {
 			return e
diff --git a/migrations/20210909172000_create_identities_table.up.sql b/migrations/20210909172000_create_identities_table.up.sql
new file mode 100644
index 00000000..10b1f2b9
--- /dev/null
+++ b/migrations/20210909172000_create_identities_table.up.sql
@@ -0,0 +1,14 @@
+-- adds identities table 
+
+CREATE TABLE IF NOT EXISTS auth.identities (
+    id text NOT NULL,
+    user_id uuid NOT NULL,
+    identity_data JSONB NOT NULL,
+    provider text NOT NULL,
+    last_sign_in_at timestamptz NULL,
+    created_at timestamptz NULL,
+    updated_at timestamptz NULL,
+    CONSTRAINT identities_pkey PRIMARY KEY (provider, id),
+    CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
+);
+COMMENT ON TABLE auth.identities is 'Auth: Stores identities associated to a user.';
diff --git a/models/connection.go b/models/connection.go
index 89a8fd5a..7c01a1de 100644
--- a/models/connection.go
+++ b/models/connection.go
@@ -32,15 +32,15 @@ type SortField struct {
 
 func TruncateAll(conn *storage.Connection) error {
 	return conn.Transaction(func(tx *storage.Connection) error {
-		if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: User{}}).TableName()).Exec(); err != nil {
+		if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: User{}}).TableName() + " CASCADE").Exec(); err != nil {
 			return err
 		}
-		if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: RefreshToken{}}).TableName()).Exec(); err != nil {
+		if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: RefreshToken{}}).TableName() + " CASCADE").Exec(); err != nil {
 			return err
 		}
-		if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: AuditLogEntry{}}).TableName()).Exec(); err != nil {
+		if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: AuditLogEntry{}}).TableName() + " CASCADE").Exec(); err != nil {
 			return err
 		}
-		return tx.RawQuery("TRUNCATE " + (&pop.Model{Value: Instance{}}).TableName()).Exec()
+		return tx.RawQuery("TRUNCATE " + (&pop.Model{Value: Instance{}}).TableName() + " CASCADE").Exec()
 	})
 }
diff --git a/models/errors.go b/models/errors.go
index f568364f..6c33c70c 100644
--- a/models/errors.go
+++ b/models/errors.go
@@ -13,6 +13,8 @@ func IsNotFoundError(err error) bool {
 		return true
 	case TotpSecretNotFoundError:
 		return true
+	case IdentityNotFoundError:
+		return true
 	}
 	return false
 }
@@ -24,6 +26,13 @@ func (e UserNotFoundError) Error() string {
 	return "User not found"
 }
 
+// IdentityNotFoundError represents when an identity is not found.
+type IdentityNotFoundError struct{}
+
+func (e IdentityNotFoundError) Error() string {
+	return "Identity not found"
+}
+
 // ConfirmationTokenNotFoundError represents when a confirmation token is not found.
 type ConfirmationTokenNotFoundError struct{}
 
diff --git a/models/identity.go b/models/identity.go
new file mode 100644
index 00000000..74b9383b
--- /dev/null
+++ b/models/identity.go
@@ -0,0 +1,84 @@
+package models
+
+import (
+	"database/sql"
+	"time"
+
+	"github.com/gofrs/uuid"
+	"github.com/netlify/gotrue/storage"
+	"github.com/pkg/errors"
+)
+
+type Identity struct {
+	ID           string     `json:"id" db:"id"`
+	UserID       uuid.UUID  `json:"user_id" db:"user_id"`
+	IdentityData JSONMap    `json:"identity_data,omitempty" db:"identity_data"`
+	Provider     string     `json:"provider" db:"provider"`
+	LastSignInAt *time.Time `json:"last_sign_in_at,omitempty" db:"last_sign_in_at"`
+	CreatedAt    time.Time  `json:"created_at" db:"created_at"`
+	UpdatedAt    time.Time  `json:"updated_at" db:"updated_at"`
+}
+
+func (Identity) TableName() string {
+	tableName := "identities"
+	return tableName
+}
+
+// NewIdentity returns an identity associated to the user's id.
+func NewIdentity(user *User, provider string, identityData map[string]interface{}) (*Identity, error) {
+	id, ok := identityData["sub"]
+	if !ok {
+		return nil, errors.New("Error missing provider id")
+	}
+	now := time.Now()
+
+	identity := &Identity{
+		ID:           id.(string),
+		UserID:       user.ID,
+		IdentityData: identityData,
+		Provider:     provider,
+		LastSignInAt: &now,
+	}
+
+	return identity, nil
+}
+
+// FindIdentityById searches for an identity with the matching provider_id and provider given.
+func FindIdentityByIdAndProvider(tx *storage.Connection, providerId, provider string) (*Identity, error) {
+	identity := &Identity{}
+	if err := tx.Q().Where("id = ? AND provider = ?", providerId, provider).First(identity); err != nil {
+		if errors.Cause(err) == sql.ErrNoRows {
+			return nil, IdentityNotFoundError{}
+		}
+		return nil, errors.Wrap(err, "error finding identity")
+	}
+	return identity, nil
+}
+
+// FindIdentitiesByUser returns all identities associated to a user
+func FindIdentitiesByUser(tx *storage.Connection, user *User) ([]*Identity, error) {
+	identities := []*Identity{}
+	if err := tx.Q().Where("user_id = ?", user.ID).All(&identities); err != nil {
+		if errors.Cause(err) == sql.ErrNoRows {
+			return identities, nil
+		}
+		return nil, errors.Wrap(err, "error finding identities")
+	}
+	return identities, nil
+}
+
+// FindProvidersByUser returns all providers associated to a user
+func FindProvidersByUser(tx *storage.Connection, user *User) ([]string, error) {
+	identities := []Identity{}
+	providers := make([]string, 0)
+	if err := tx.Q().Select("provider").Where("user_id = ?", user.ID).All(&identities); err != nil {
+		if errors.Cause(err) == sql.ErrNoRows {
+			return providers, nil
+		}
+		return nil, errors.Wrap(err, "error finding providers")
+	}
+	for _, identity := range identities {
+		providers = append(providers, identity.Provider)
+	}
+	return providers, nil
+}
diff --git a/models/user.go b/models/user.go
index f0e63461..c7304465 100644
--- a/models/user.go
+++ b/models/user.go
@@ -44,7 +44,7 @@ type User struct {
 	EmailChangeTokenNew      string     `json:"-" db:"email_change_token_new"`
 	EmailChange              string     `json:"new_email,omitempty" db:"email_change"`
 	EmailChangeSentAt        *time.Time `json:"email_change_sent_at,omitempty" db:"email_change_sent_at"`
-	EmailChangeConfirmStatus int        `json:"email_change_confirm_status" db:"email_change_confirm_status"`
+	EmailChangeConfirmStatus int        `json:"-" db:"email_change_confirm_status"`
 
 	PhoneChangeToken  string     `json:"-" db:"phone_change_token"`
 	PhoneChange       string     `json:"new_phone,omitempty" db:"phone_change"`
@@ -55,7 +55,8 @@ type User struct {
 	AppMetaData  JSONMap `json:"app_metadata" db:"raw_app_meta_data"`
 	UserMetaData JSONMap `json:"user_metadata" db:"raw_user_meta_data"`
 
-	IsSuperAdmin bool `json:"-" db:"is_super_admin"`
+	IsSuperAdmin bool       `json:"-" db:"is_super_admin"`
+	Identities   []Identity `json:"identities" has_many:"identities"`
 
 	CreatedAt time.Time `json:"created_at" db:"created_at"`
 	UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
@@ -72,7 +73,9 @@ func NewUser(instanceID uuid.UUID, email, password, aud string, userData map[str
 	if err != nil {
 		return nil, err
 	}
-
+	if userData == nil {
+		userData = make(map[string]interface{})
+	}
 	user := &User{
 		InstanceID:        instanceID,
 		ID:                id,
@@ -214,6 +217,17 @@ func (u *User) UpdateAppMetaData(tx *storage.Connection, updates map[string]inte
 	return tx.UpdateOnly(u, "raw_app_meta_data")
 }
 
+// UpdateAppMetaDataProviders updates the provider field in AppMetaData column
+func (u *User) UpdateAppMetaDataProviders(tx *storage.Connection) error {
+	providers, terr := FindProvidersByUser(tx, u)
+	if terr != nil {
+		return terr
+	}
+	return u.UpdateAppMetaData(tx, map[string]interface{}{
+		"providers": providers,
+	})
+}
+
 // SetEmail sets the user's email
 func (u *User) SetEmail(tx *storage.Connection, email string) error {
 	u.Email = storage.NullString(email)
@@ -319,7 +333,7 @@ func CountOtherUsers(tx *storage.Connection, instanceID, id uuid.UUID) (int, err
 
 func findUser(tx *storage.Connection, query string, args ...interface{}) (*User, error) {
 	obj := &User{}
-	if err := tx.Q().Where(query, args...).First(obj); err != nil {
+	if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil {
 		if errors.Cause(err) == sql.ErrNoRows {
 			return nil, UserNotFoundError{}
 		}
diff --git a/security/hcaptcha.go b/security/hcaptcha.go
index 93418fc2..4017f857 100644
--- a/security/hcaptcha.go
+++ b/security/hcaptcha.go
@@ -4,17 +4,22 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
 	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
 )
 
-type CaptchaResponse struct {
+type GotrueRequest struct {
+	Security GotrueSecurity `json:"gotrue_meta_security"`
+}
+
+type GotrueSecurity struct {
 	Token string `json:"hcaptcha_token"`
 }
 
@@ -25,11 +30,13 @@ type VerificationResponse struct {
 }
 
 type VerificationResult int
+
 const (
 	UserRequestFailed VerificationResult = iota
 	VerificationProcessFailure
 	SuccessfullyVerified
 )
+
 var Client *http.Client
 
 func init() {
@@ -38,7 +45,7 @@ func init() {
 }
 
 func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error) {
-	res := CaptchaResponse{}
+	res := GotrueRequest{}
 	bodyBytes, err := ioutil.ReadAll(r.Body)
 	if err != nil {
 		return UserRequestFailed, err
@@ -49,11 +56,11 @@ func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error
 
 	jsonDecoder := json.NewDecoder(bytes.NewBuffer(bodyBytes))
 	err = jsonDecoder.Decode(&res)
-	if err != nil || strings.TrimSpace(res.Token) == "" {
+	if err != nil || strings.TrimSpace(res.Security.Token) == "" {
 		return UserRequestFailed, errors.Wrap(err, "couldn't decode captcha info")
 	}
 	clientIP := strings.Split(r.RemoteAddr, ":")[0]
-	return verifyCaptchaCode(res.Token, secretKey, clientIP)
+	return verifyCaptchaCode(res.Security.Token, secretKey, clientIP)
 }
 
 func verifyCaptchaCode(token string, secretKey string, clientIP string) (VerificationResult, error) {
diff --git a/storage/session.go b/storage/session.go
new file mode 100644
index 00000000..e6189690
--- /dev/null
+++ b/storage/session.go
@@ -0,0 +1,42 @@
+package storage
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+	"github.com/kelseyhightower/envconfig"
+)
+
+var sessionName = "_gotrue_session"
+var Store sessions.Store
+
+type SessionConfig struct {
+	Key []byte `envconfig:"GOTRUE_SESSION_KEY"`
+}
+
+func init() {
+	var sessionConfig SessionConfig
+	err := envconfig.Process("GOTRUE_SESSION_KEY", &sessionConfig)
+	if err != nil || len(sessionConfig.Key) == 0 {
+		sessionConfig.Key = securecookie.GenerateRandomKey(32)
+	}
+	Store = sessions.NewCookieStore(sessionConfig.Key)
+}
+
+func StoreInSession(key string, value string, req *http.Request, res http.ResponseWriter) error {
+	session, _ := Store.New(req, sessionName)
+	session.Values[key] = value
+	return session.Save(req, res)
+}
+
+func GetFromSession(key string, req *http.Request) (string, error) {
+	session, _ := Store.Get(req, sessionName)
+	value, ok := session.Values[key]
+	if !ok {
+		return "", errors.New("session could not be found for this request")
+	}
+
+	return value.(string), nil
+}


Test patch:
diff --git a/api/admin_test.go b/api/admin_test.go
index 36bd35aa..2ca648a3 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -69,6 +69,7 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string {
 func (ts *AdminTestSuite) makeSystemUser() string {
 	u := models.NewSystemUser(uuid.Nil, ts.Config.JWT.Aud)
 	u.Role = "service_role"
+
 	token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err, "Error generating access token")
 
@@ -279,6 +280,7 @@ func (ts *AdminTestSuite) TestAdminUserCreate() {
 	assert.Equal(ts.T(), "test1@example.com", data.GetEmail())
 	assert.Equal(ts.T(), "123456789", data.GetPhone())
 	assert.Equal(ts.T(), "email", data.AppMetaData["provider"])
+	assert.Equal(ts.T(), []interface{}{"email"}, data.AppMetaData["providers"])
 }
 
 // TestAdminUserGet tests API /admin/user route (GET)
diff --git a/api/external_azure_test.go b/api/external_azure_test.go
index 40874db2..0626e9a7 100644
--- a/api/external_azure_test.go
+++ b/api/external_azure_test.go
@@ -9,6 +9,11 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	azureUser        string = `{"name":"Azure Test","email":"azure@example.com","sub":"azuretestid"}`
+	azureUserNoEmail string = `{"name":"Azure Test","sub":"azuretestid"}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalAzure() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=azure", nil)
 	w := httptest.NewRecorder()
@@ -61,23 +66,21 @@ func AzureTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int
 
 func (ts *ExternalTestSuite) TestSignupExternalAzure_AuthorizationCode() {
 	ts.Config.DisableSignup = false
-	ts.createUser("azure@example.com", "Azure Test", "", "")
+	ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "")
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	azureUser := `{"name":"Azure Test","email":"azure@example.com"}`
 	server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "azure", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoUser() {
 	ts.Config.DisableSignup = true
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	azureUser := `{"name":"Azure Test","email":"azure@example.com"}`
 	server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser)
 	defer server.Close()
 
@@ -90,8 +93,7 @@ func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmai
 	ts.Config.DisableSignup = true
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	azureUser := `{"name":"Azure Test"}`
-	server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser)
+	server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUserNoEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "azure", code, "")
@@ -103,32 +105,30 @@ func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmai
 func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("azure@example.com", "Azure Test", "", "")
+	ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	azureUser := `{"name":"Azure Test","email":"azure@example.com"}`
 	server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "azure", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalAzureSuccessWhenMatchingToken() {
 	// name and avatar should be populated from Azure API
-	ts.createUser("azure@example.com", "", "", "invite_token")
+	ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	azureUser := `{"name":"Azure Test","email":"azure@example.com"}}`
 	server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "azure", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToken() {
@@ -143,7 +143,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToke
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenWrongToken() {
-	ts.createUser("azure@example.com", "", "", "invite_token")
+	ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
@@ -156,7 +156,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenWrongToken() {
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenEmailDoesntMatch() {
-	ts.createUser("azure@example.com", "", "", "invite_token")
+	ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
diff --git a/api/external_bitbucket_test.go b/api/external_bitbucket_test.go
index 29d9d278..ab48a1e1 100644
--- a/api/external_bitbucket_test.go
+++ b/api/external_bitbucket_test.go
@@ -9,6 +9,10 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	bitbucketUser string = `{"uuid":"bitbucketTestId","display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalBitbucket() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=bitbucket", nil)
 	w := httptest.NewRecorder()
@@ -66,14 +70,13 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucket_AuthorizationCode() {
 	ts.Config.DisableSignup = false
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
 	emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
 	server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "bitbucket", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNoUser() {
@@ -94,7 +97,6 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNo
 	ts.Config.DisableSignup = true
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
 	emails := `{"values":[{}]}`
 	server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
 	defer server.Close()
@@ -108,51 +110,48 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNo
 func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar", "")
+	ts.createUser("bitbucketTestId", "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
 	emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
 	server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "bitbucket", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupSuccessWithSecondaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("secondary@example.com", "Bitbucket Test", "http://example.com/avatar", "")
+	ts.createUser("bitbucketTestId", "secondary@example.com", "Bitbucket Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
 	emails := `{"values":[{"email":"primary@example.com","is_primary":true,"is_confirmed":true},{"email":"secondary@example.com","is_primary":false,"is_confirmed":true}]}`
 	server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "bitbucket", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "Bitbucket Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketSuccessWhenMatchingToken() {
 	// name and avatar should be populated from Bitbucket API
-	ts.createUser("bitbucket@example.com", "", "", "invite_token")
+	ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
 	emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
 	server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "bitbucket", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenNoMatchingToken() {
@@ -168,7 +167,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenNoMatching
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenWrongToken() {
-	ts.createUser("bitbucket@example.com", "", "", "invite_token")
+	ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
@@ -182,11 +181,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenWrongToken
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenEmailDoesntMatch() {
-	ts.createUser("bitbucket@example.com", "", "", "invite_token")
+	ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
 	emails := `{"values":[{"email":"other@example.com","is_primary":true,"is_confirmed":true}]}`
 	server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
 	defer server.Close()
diff --git a/api/external_discord_test.go b/api/external_discord_test.go
index e6156212..057cd9c5 100644
--- a/api/external_discord_test.go
+++ b/api/external_discord_test.go
@@ -9,6 +9,12 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	discordUser           string = `{"id":"discordTestId","avatar":"abc","email":"discord@example.com","username":"Discord Test","verified":true}}`
+	discordUserWrongEmail string = `{"id":"discordTestId","avatar":"abc","email":"other@example.com","username":"Discord Test","verified":true}}`
+	discordUserNoEmail    string = `{"id":"discordTestId","avatar":"abc","username":"Discord Test","verified":true}}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalDiscord() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=discord", nil)
 	w := httptest.NewRecorder()
@@ -63,13 +69,12 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscord_AuthorizationCode() {
 	ts.Config.DisableSignup = false
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}`
 	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "discord", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenNoUser() {
@@ -77,7 +82,6 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenNoUs
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}`
 	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
 	defer server.Close()
 
@@ -90,8 +94,7 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenEmpt
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","id":"123","username":"Discord Test","verified":true}}`
-	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
+	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUserNoEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "discord", code, "")
@@ -102,38 +105,35 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenEmpt
 func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png", "")
+	ts.createUser("discordTestId", "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/discordTestId/abc.png", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}`
 	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "discord", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordSuccessWhenMatchingToken() {
 	// name and avatar should be populated from Discord API
-	ts.createUser("discord@example.com", "", "", "invite_token")
+	ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}`
 	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "discord", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenNoMatchingToken() {
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}`
 	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
 	defer server.Close()
 
@@ -142,11 +142,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenNoMatchingTo
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenWrongToken() {
-	ts.createUser("discord@example.com", "", "", "invite_token")
+	ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}`
 	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
 	defer server.Close()
 
@@ -155,12 +154,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenWrongToken()
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenEmailDoesntMatch() {
-	ts.createUser("discord@example.com", "", "", "invite_token")
+	ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	discordUser := `{"avatar":"abc","email":"other@example.com","id":"123","username":"Discord Test","verified":true}}`
-	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
+	server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUserWrongEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "discord", code, "invite_token")
diff --git a/api/external_facebook_test.go b/api/external_facebook_test.go
index adddfc22..25371543 100644
--- a/api/external_facebook_test.go
+++ b/api/external_facebook_test.go
@@ -9,6 +9,12 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	facebookUser           string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
+	facebookUserWrongEmail string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"other@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
+	facebookUserNoEmail    string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","picture":{"data":{"url":"http://example.com/avatar"}}}}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalFacebook() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=facebook", nil)
 	w := httptest.NewRecorder()
@@ -63,13 +69,12 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebook_AuthorizationCode() {
 	ts.Config.DisableSignup = false
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
 	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "facebook", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenNoUser() {
@@ -77,7 +82,6 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenNoU
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
 	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
 	defer server.Close()
 
@@ -90,8 +94,7 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenEmp
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","picture":{"data":{"url":"http://example.com/avatar"}}}}`
-	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
+	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUserNoEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "facebook", code, "")
@@ -102,38 +105,35 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenEmp
 func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("facebook@example.com", "Facebook Test", "http://example.com/avatar", "")
+	ts.createUser("facebookTestId", "facebook@example.com", "Facebook Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
 	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "facebook", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookSuccessWhenMatchingToken() {
 	// name and avatar should be populated from Facebook API
-	ts.createUser("facebook@example.com", "", "", "invite_token")
+	ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
 	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "facebook", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenNoMatchingToken() {
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
 	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
 	defer server.Close()
 
@@ -142,11 +142,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenNoMatchingT
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenWrongToken() {
-	ts.createUser("facebook@example.com", "", "", "invite_token")
+	ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
 	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
 	defer server.Close()
 
@@ -155,12 +154,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenWrongToken(
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenEmailDoesntMatch() {
-	ts.createUser("facebook@example.com", "", "", "invite_token")
+	ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"other@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
-	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
+	server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUserWrongEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "facebook", code, "invite_token")
diff --git a/api/external_github_test.go b/api/external_github_test.go
index bd2831a7..03fce76d 100644
--- a/api/external_github_test.go
+++ b/api/external_github_test.go
@@ -47,7 +47,7 @@ func GitHubTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *in
 		case "/api/v3/user":
 			*userCount++
 			w.Header().Add("Content-Type", "application/json")
-			fmt.Fprint(w, `{"name":"GitHub Test","avatar_url":"http://example.com/avatar"}`)
+			fmt.Fprint(w, `{"id":123, "name":"GitHub Test","avatar_url":"http://example.com/avatar"}`)
 		case "/api/v3/user/emails":
 			w.Header().Add("Content-Type", "application/json")
 			fmt.Fprint(w, emails)
@@ -71,7 +71,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHub_AuthorizationCode() {
 
 	u := performAuthorization(ts, "github", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupErrorWhenNoUser() {
@@ -103,7 +103,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupErrorWhenEmpty
 func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("github@example.com", "GitHub Test", "http://example.com/avatar", "")
+	ts.createUser("123", "github@example.com", "GitHub Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
@@ -113,13 +113,13 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithPri
 
 	u := performAuthorization(ts, "github", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithNonPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("secondary@example.com", "GitHub Test", "http://example.com/avatar", "")
+	ts.createUser("123", "secondary@example.com", "GitHub Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
@@ -129,12 +129,12 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithNon
 
 	u := performAuthorization(ts, "github", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitHub Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitHub Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubSuccessWhenMatchingToken() {
 	// name and avatar should be populated from GitHub API
-	ts.createUser("github@example.com", "", "", "invite_token")
+	ts.createUser("123", "github@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
@@ -144,7 +144,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubSuccessWhenMatchingTok
 
 	u := performAuthorization(ts, "github", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenNoMatchingToken() {
@@ -159,7 +159,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenNoMatchingTok
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenWrongToken() {
-	ts.createUser("github@example.com", "", "", "invite_token")
+	ts.createUser("123", "github@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
@@ -172,7 +172,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenWrongToken()
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenEmailDoesntMatch() {
-	ts.createUser("github@example.com", "", "", "invite_token")
+	ts.createUser("123", "github@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
diff --git a/api/external_gitlab_test.go b/api/external_gitlab_test.go
index ba6c4f86..8a8b0fbf 100644
--- a/api/external_gitlab_test.go
+++ b/api/external_gitlab_test.go
@@ -9,6 +9,12 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	gitlabUser           string = `{"id":123,"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
+	gitlabUserWrongEmail string = `{"id":123,"email":"other@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
+	gitlabUserNoEmail    string = `{"id":123,"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalGitlab() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=gitlab", nil)
 	w := httptest.NewRecorder()
@@ -68,14 +74,13 @@ func (ts *ExternalTestSuite) TestSignupExternalGitlab_AuthorizationCode() {
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"name":"GitLab Test","avatar_url":"http://example.com/avatar"}`
 	emails := `[{"id":1,"email":"gitlab@example.com"}]`
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "gitlab", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenNoUser() {
@@ -83,7 +88,6 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenNoUse
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}`
 	emails := `[{"id":1,"email":"gitlab@example.com"}]`
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
@@ -98,9 +102,8 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenEmpty
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}`
 	emails := `[]`
-	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
+	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUserNoEmail, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "gitlab", code, "")
@@ -111,18 +114,17 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenEmpty
 func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("gitlab@example.com", "GitLab Test", "http://example.com/avatar", "")
+	ts.createUser("123", "gitlab@example.com", "GitLab Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
 	emails := "[]"
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "gitlab", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithSecondaryEmail() {
@@ -130,40 +132,37 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithSec
 	ts.Config.Mailer.Autoconfirm = true
 	ts.Config.DisableSignup = true
 
-	ts.createUser("secondary@example.com", "GitLab Test", "http://example.com/avatar", "")
+	ts.createUser("123", "secondary@example.com", "GitLab Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"email":"primary@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar"}`
 	emails := `[{"id":1,"email":"secondary@example.com"}]`
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "gitlab", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitLab Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitLab Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabSuccessWhenMatchingToken() {
 	// name and avatar should be populated from GitLab API
-	ts.createUser("gitlab@example.com", "", "", "invite_token")
+	ts.createUser("123", "gitlab@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
 	emails := "[]"
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "gitlab", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenNoMatchingToken() {
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
 	emails := "[]"
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
@@ -173,11 +172,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenNoMatchingTok
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenWrongToken() {
-	ts.createUser("gitlab@example.com", "", "", "invite_token")
+	ts.createUser("123", "gitlab@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
 	emails := "[]"
 	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
 	defer server.Close()
@@ -187,13 +185,12 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenWrongToken()
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenEmailDoesntMatch() {
-	ts.createUser("gitlab@example.com", "", "", "invite_token")
+	ts.createUser("123", "gitlab@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	gitlabUser := `{"email":"other@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
 	emails := "[]"
-	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
+	server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUserWrongEmail, emails)
 	defer server.Close()
 
 	u := performAuthorization(ts, "gitlab", code, "invite_token")
diff --git a/api/external_google_test.go b/api/external_google_test.go
index 27288087..8992d0a6 100644
--- a/api/external_google_test.go
+++ b/api/external_google_test.go
@@ -9,6 +9,12 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	googleUser           string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
+	googleUserWrongEmail string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","email":"other@example.com","verified_email":true}}`
+	googleUserNoEmail    string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","verified_email":true}}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalGoogle() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=google", nil)
 	w := httptest.NewRecorder()
@@ -63,13 +69,12 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogle_AuthorizationCode() {
 	ts.Config.DisableSignup = false
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
 	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "google", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenNoUser() {
@@ -77,7 +82,6 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenNoUse
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
 	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
 	defer server.Close()
 
@@ -90,8 +94,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenEmpty
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","verified_email":true}}`
-	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
+	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUserNoEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "google", code, "")
@@ -102,38 +105,35 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenEmpty
 func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("google@example.com", "Google Test", "http://example.com/avatar", "")
+	ts.createUser("googleTestId", "google@example.com", "Google Test", "http://example.com/avatar", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
 	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "google", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleSuccessWhenMatchingToken() {
 	// name and avatar should be populated from Google API
-	ts.createUser("google@example.com", "", "", "invite_token")
+	ts.createUser("googleTestId", "google@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
 	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "google", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "http://example.com/avatar")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenNoMatchingToken() {
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
 	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
 	defer server.Close()
 
@@ -142,11 +142,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenNoMatchingTok
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenWrongToken() {
-	ts.createUser("google@example.com", "", "", "invite_token")
+	ts.createUser("googleTestId", "google@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
 	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
 	defer server.Close()
 
@@ -155,12 +154,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenWrongToken()
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenEmailDoesntMatch() {
-	ts.createUser("google@example.com", "", "", "invite_token")
+	ts.createUser("googleTestId", "google@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"other@example.com","verified_email":true}}`
-	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
+	server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUserWrongEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "google", code, "invite_token")
diff --git a/api/external_test.go b/api/external_test.go
index 0cb7e326..9aee2c30 100644
--- a/api/external_test.go
+++ b/api/external_test.go
@@ -41,13 +41,13 @@ func (ts *ExternalTestSuite) SetupTest() {
 	models.TruncateAll(ts.API.db)
 }
 
-func (ts *ExternalTestSuite) createUser(email string, name string, avatar string, confirmationToken string) (*models.User, error) {
+func (ts *ExternalTestSuite) createUser(providerId string, email string, name string, avatar string, confirmationToken string) (*models.User, error) {
 	// Cleanup existing user, if they already exist
 	if u, _ := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, email, ts.Config.JWT.Aud); u != nil {
 		require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user")
 	}
 
-	u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": name, "avatar_url": avatar})
+	u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"provider_id": providerId, "full_name": name, "avatar_url": avatar})
 
 	if confirmationToken != "" {
 		u.ConfirmationToken = confirmationToken
@@ -98,7 +98,7 @@ func performAuthorization(ts *ExternalTestSuite, provider string, code string, i
 	return u
 }
 
-func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount int, userCount int, email string, name string, avatar string) {
+func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount int, userCount int, email string, name string, providerId string, avatar string) {
 	// ensure redirect has #access_token=...
 	v, err := url.ParseQuery(u.RawQuery)
 	ts.Require().NoError(err)
@@ -118,6 +118,7 @@ func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount in
 	// ensure user has been created with metadata
 	user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, email, ts.Config.JWT.Aud)
 	ts.Require().NoError(err)
+	ts.Equal(providerId, user.UserMetaData["provider_id"])
 	ts.Equal(name, user.UserMetaData["full_name"])
 	ts.Equal(avatar, user.UserMetaData["avatar_url"])
 }
diff --git a/api/external_twitch_test.go b/api/external_twitch_test.go
index f8f8b340..a4e473ca 100644
--- a/api/external_twitch_test.go
+++ b/api/external_twitch_test.go
@@ -9,6 +9,11 @@ import (
 	jwt "github.com/golang-jwt/jwt"
 )
 
+const (
+	twitchUser           string = `{"data":[{"id":"twitchTestId","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
+	twitchUserWrongEmail string = `{"data":[{"id":"twitchTestId","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"other@example.com"}]}`
+)
+
 func (ts *ExternalTestSuite) TestSignupExternalTwitch() {
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=twitch", nil)
 	w := httptest.NewRecorder()
@@ -63,13 +68,12 @@ func (ts *ExternalTestSuite) TestSignupExternalTwitch_AuthorizationCode() {
 	ts.Config.DisableSignup = false
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	TwitchUser := `{"data":[{"id":"1","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
-	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
+	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "twitch", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch user", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
 }
 
 func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupErrorWhenNoUser() {
@@ -103,32 +107,31 @@ func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupErrorWhenEmpty
 func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupSuccessWithPrimaryEmail() {
 	ts.Config.DisableSignup = true
 
-	ts.createUser("twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8", "")
+	ts.createUser("twitchTestId", "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8", "")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
-	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
+	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "twitch", code, "")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchSuccessWhenMatchingToken() {
 	// name and avatar should be populated from Twitch API
-	ts.createUser("twitch@example.com", "", "", "invite_token")
+	ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	TwitchUser := `{"data":[{"id":"1","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
+	TwitchUser := `{"data":[{"id":"twitchTestId","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
 	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
 	defer server.Close()
 
 	u := performAuthorization(ts, "twitch", code, "invite_token")
 
-	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
+	assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenNoMatchingToken() {
@@ -143,12 +146,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenNoMatchingTok
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenWrongToken() {
-	ts.createUser("twitch@example.com", "", "", "invite_token")
+	ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
-	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
+	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser)
 	defer server.Close()
 
 	w := performAuthorizationRequest(ts, "twitch", "wrong_token")
@@ -156,12 +158,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenWrongToken()
 }
 
 func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenEmailDoesntMatch() {
-	ts.createUser("twitch@example.com", "", "", "invite_token")
+	ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token")
 
 	tokenCount, userCount := 0, 0
 	code := "authcode"
-	TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"other@example.com"}]}`
-	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
+	server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUserWrongEmail)
 	defer server.Close()
 
 	u := performAuthorization(ts, "twitch", code, "invite_token")
diff --git a/api/invite_test.go b/api/invite_test.go
index 14d58f21..8d105213 100644
--- a/api/invite_test.go
+++ b/api/invite_test.go
@@ -265,6 +265,7 @@ func (ts *InviteTestSuite) TestInviteExternalGitlab() {
 	ts.Equal("Gitlab Test", user.UserMetaData["full_name"])
 	ts.Equal("http://example.com/avatar", user.UserMetaData["avatar_url"])
 	ts.Equal("gitlab", user.AppMetaData["provider"])
+	ts.Equal([]interface{}{"gitlab"}, user.AppMetaData["providers"])
 }
 
 func (ts *InviteTestSuite) TestInviteExternalGitlab_MismatchedEmails() {
diff --git a/api/middleware_test.go b/api/middleware_test.go
index 01b25828..1c99037c 100644
--- a/api/middleware_test.go
+++ b/api/middleware_test.go
@@ -45,9 +45,11 @@ func TestHCaptcha(t *testing.T) {
 func (ts *MiddlewareTestSuite) TestVerifyCaptchaValid() {
 	var buffer bytes.Buffer
 	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
-		"email":          "test@example.com",
-		"password":       "secret",
-		"hcaptcha_token": HCaptchaResponse,
+		"email":    "test@example.com",
+		"password": "secret",
+		"gotrue_meta_security": map[string]interface{}{
+			"hcaptcha_token": HCaptchaResponse,
+		},
 	}))
 
 	ts.Config.Security.Captcha.Enabled = true
@@ -70,9 +72,11 @@ func (ts *MiddlewareTestSuite) TestVerifyCaptchaValid() {
 
 	// re-initialize buffer
 	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
-		"email":          "test@example.com",
-		"password":       "secret",
-		"hcaptcha_token": HCaptchaResponse,
+		"email":    "test@example.com",
+		"password": "secret",
+		"gotrue_meta_security": map[string]interface{}{
+			"hcaptcha_token": HCaptchaResponse,
+		},
 	}))
 
 	// check if body is the same
@@ -122,9 +126,11 @@ func (ts *MiddlewareTestSuite) TestVerifyCaptchaInvalid() {
 			ts.Config.Security.Captcha = *c.captchaConf
 			var buffer bytes.Buffer
 			require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
-				"email":          "test@example.com",
-				"password":       "secret",
-				"hcaptcha_token": HCaptchaResponse,
+				"email":    "test@example.com",
+				"password": "secret",
+				"gotrue_meta_security": map[string]interface{}{
+					"hcaptcha_token": HCaptchaResponse,
+				},
 			}))
 			req := httptest.NewRequest(http.MethodPost, "http://localhost", &buffer)
 			req.Header.Set("Content-Type", "application/json")
@@ -136,8 +142,8 @@ func (ts *MiddlewareTestSuite) TestVerifyCaptchaInvalid() {
 			w := httptest.NewRecorder()
 
 			_, err = ts.API.verifyCaptcha(w, req)
-			require.Equal(ts.T(), err.(*HTTPError).Code, c.expectedCode)
-			require.Equal(ts.T(), err.(*HTTPError).Message, c.expectedMsg)
+			require.Equal(ts.T(), c.expectedCode, err.(*HTTPError).Code)
+			require.Equal(ts.T(), c.expectedMsg, err.(*HTTPError).Message)
 		})
 	}
 }
diff --git a/api/settings_test.go b/api/settings_test.go
index 61839b4c..f8698749 100644
--- a/api/settings_test.go
+++ b/api/settings_test.go
@@ -32,6 +32,8 @@ func TestSettings_DefaultProviders(t *testing.T) {
 	require.True(t, p.Bitbucket)
 	require.True(t, p.Discord)
 	require.True(t, p.Facebook)
+	require.True(t, p.Spotify)
+	require.True(t, p.Slack)
 	require.True(t, p.Google)
 	require.True(t, p.GitHub)
 	require.True(t, p.GitLab)
diff --git a/api/signup_test.go b/api/signup_test.go
index 60e33dd3..8f76f11f 100644
--- a/api/signup_test.go
+++ b/api/signup_test.go
@@ -74,6 +74,7 @@ func (ts *SignupTestSuite) TestSignup() {
 	assert.Equal(ts.T(), ts.Config.JWT.Aud, data.Aud)
 	assert.Equal(ts.T(), 1.0, data.UserMetaData["a"])
 	assert.Equal(ts.T(), "email", data.AppMetaData["provider"])
+	assert.Equal(ts.T(), []interface{}{"email"}, data.AppMetaData["providers"])
 }
 
 func (ts *SignupTestSuite) TestWebhookTriggered() {
@@ -119,8 +120,9 @@ func (ts *SignupTestSuite) TestWebhookTriggered() {
 
 		appmeta, ok := u["app_metadata"].(map[string]interface{})
 		require.True(ok)
-		assert.Len(appmeta, 1)
+		assert.Len(appmeta, 2)
 		assert.EqualValues("email", appmeta["provider"])
+		assert.EqualValues([]interface{}{"email"}, appmeta["providers"])
 
 		usermeta, ok := u["user_metadata"].(map[string]interface{})
 		require.True(ok)
@@ -217,11 +219,16 @@ func (ts *SignupTestSuite) TestSignupTwice() {
 	encode()
 	ts.API.handler.ServeHTTP(w, req)
 
-	data := make(map[string]interface{})
+	data := models.User{}
 	require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
 
-	assert.Equal(ts.T(), http.StatusBadRequest, w.Code)
-	assert.Equal(ts.T(), float64(http.StatusBadRequest), data["code"])
+	require.Equal(ts.T(), http.StatusOK, w.Code)
+
+	assert.Equal(ts.T(), "test1@example.com", data.GetEmail())
+	assert.Equal(ts.T(), ts.Config.JWT.Aud, data.Aud)
+	assert.Equal(ts.T(), 1.0, data.UserMetaData["a"])
+	assert.Equal(ts.T(), "email", data.AppMetaData["provider"])
+	assert.Equal(ts.T(), []interface{}{"email"}, data.AppMetaData["providers"])
 }
 
 func (ts *SignupTestSuite) TestVerifySignup() {
diff --git a/api/user_test.go b/api/user_test.go
index 3510c3e4..f74bc955 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -9,9 +9,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/gofrs/uuid"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/models"
-	"github.com/gofrs/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
diff --git a/hack/test.env b/hack/test.env
index 9e4f0742..6b5b6c25 100644
--- a/hack/test.env
+++ b/hack/test.env
@@ -45,6 +45,14 @@ GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
 GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=testclientid
 GOTRUE_EXTERNAL_GOOGLE_SECRET=testsecret
 GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://identity.services.netlify.com/callback
+GOTRUE_EXTERNAL_SPOTIFY_ENABLED=true
+GOTRUE_EXTERNAL_SPOTIFY_CLIENT_ID=testclientid
+GOTRUE_EXTERNAL_SPOTIFY_SECRET=testsecret
+GOTRUE_EXTERNAL_SPOTIFY_REDIRECT_URI=https://identity.services.netlify.com/callback
+GOTRUE_EXTERNAL_SLACK_ENABLED=true
+GOTRUE_EXTERNAL_SLACK_CLIENT_ID=testclientid
+GOTRUE_EXTERNAL_SLACK_SECRET=testsecret
+GOTRUE_EXTERNAL_SLACK_REDIRECT_URI=https://identity.services.netlify.com/callback
 GOTRUE_EXTERNAL_TWITCH_ENABLED=true
 GOTRUE_EXTERNAL_TWITCH_CLIENT_ID=testclientid
 GOTRUE_EXTERNAL_TWITCH_SECRET=testsecret
diff --git a/models/identity_test.go b/models/identity_test.go
new file mode 100644
index 00000000..c94b6a56
--- /dev/null
+++ b/models/identity_test.go
@@ -0,0 +1,94 @@
+package models
+
+import (
+	"testing"
+
+	"github.com/gofrs/uuid"
+	"github.com/netlify/gotrue/conf"
+	"github.com/netlify/gotrue/storage"
+	"github.com/netlify/gotrue/storage/test"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+)
+
+type IdentityTestSuite struct {
+	suite.Suite
+	db *storage.Connection
+}
+
+func (ts *IdentityTestSuite) SetupTest() {
+	TruncateAll(ts.db)
+}
+
+func TestIdentity(t *testing.T) {
+	globalConfig, err := conf.LoadGlobal(modelsTestConfig)
+	require.NoError(t, err)
+
+	conn, err := test.SetupDBConnection(globalConfig)
+	require.NoError(t, err)
+
+	ts := &IdentityTestSuite{
+		db: conn,
+	}
+	defer ts.db.Close()
+
+	suite.Run(t, ts)
+}
+
+func (ts *IdentityTestSuite) TestNewIdentity() {
+	u := ts.createUserWithEmail("test@supabase.io")
+	ts.Run("Test create identity with no provider id", func() {
+		identityData := map[string]interface{}{}
+		_, err := NewIdentity(u, "email", identityData)
+		require.Error(ts.T(), err, "Error missing provider id")
+	})
+
+	ts.Run("Test create identity successfully", func() {
+		identityData := map[string]interface{}{"sub": uuid.Nil.String()}
+		identity, err := NewIdentity(u, "email", identityData)
+		require.NoError(ts.T(), err)
+		require.Equal(ts.T(), u.ID, identity.UserID)
+	})
+}
+
+func (ts *IdentityTestSuite) TestFindUserIdentities() {
+	u := ts.createUserWithIdentity("test@supabase.io")
+	identities, err := FindIdentitiesByUser(ts.db, u)
+	require.NoError(ts.T(), err)
+
+	require.Len(ts.T(), identities, 1)
+
+}
+
+func (ts *IdentityTestSuite) createUserWithEmail(email string) *User {
+	user, err := NewUser(uuid.Nil, email, "secret", "test", nil)
+	require.NoError(ts.T(), err)
+
+	err = ts.db.Create(user)
+	require.NoError(ts.T(), err)
+
+	return user
+}
+
+func (ts *IdentityTestSuite) createUserWithIdentity(email string) *User {
+	user, err := NewUser(uuid.Nil, email, "secret", "test", nil)
+	require.NoError(ts.T(), err)
+
+	err = ts.db.Create(user)
+	require.NoError(ts.T(), err)
+
+	identityData := map[string]interface{}{
+		"sub":   uuid.Nil.String(),
+		"name":  "test",
+		"email": email,
+	}
+	require.NoError(ts.T(), err)
+
+	identity, err := NewIdentity(user, "email", identityData)
+	require.NoError(ts.T(), err)
+
+	err = ts.db.Create(identity)
+	require.NoError(ts.T(), err)
+
+	return user
+}