Repository: supabase/gotrue
Pull request #254 (2021-10-28 06:24:44)

chore: update testing docs


Patch:
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 a0dcef2c..a8869973 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
@@ -56,7 +58,7 @@ 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`. 
+Rate limit the number of emails sent per hr on the following endpoints: `/signup`, `/invite`, `/magiclink`, `/recover`, `/otp`, & `/user`.
 
 `PASSWORD_MIN_LENGTH` - `int`
 
@@ -109,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
 
@@ -128,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
@@ -188,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.
 
@@ -223,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:
    ```
@@ -344,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>
 ```
 
@@ -400,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>
 ```
 
@@ -410,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`
 
@@ -437,23 +447,25 @@ 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`
 
 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
 
 ### 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`
 
@@ -473,386 +485,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:
 
-  User will be logged in and redirected to:
+```json
+{
+  "type": "signup",
+  "token": "confirmation-code-delivered-in-email",
+  "redirect_to": "https://supabase.io"
+}
+```
+
+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/external.go b/api/external.go
index 763056e2..6e2f12b8 100644
--- a/api/external.go
+++ b/api/external.go
@@ -382,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":
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/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 0ea24247..6edde2bd 100644
--- a/api/signup.go
+++ b/api/signup.go
@@ -256,6 +256,8 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param
 		user.AppMetaData = make(map[string]interface{})
 	}
 
+	user.Identities = make([]models.Identity, 0)
+
 	// TODO: Depcreate "provider" field
 	user.AppMetaData["provider"] = params.Provider
 
diff --git a/api/token.go b/api/token.go
index cce175af..6a23a0cf 100644
--- a/api/token.go
+++ b/api/token.go
@@ -20,7 +20,6 @@ type GoTrueClaims struct {
 	Phone        string                 `json:"phone"`
 	AppMetaData  map[string]interface{} `json:"app_metadata"`
 	UserMetaData map[string]interface{} `json:"user_metadata"`
-	Identities   []*models.Identity     `json:"identities"`
 	Role         string                 `json:"role"`
 }
 
@@ -185,13 +184,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 			return internalServerError(terr.Error())
 		}
 
-		identities := make([]*models.Identity, 0)
-		identities, terr = models.FindIdentitiesByUser(tx, user)
-		if terr != nil {
-			return internalServerError("error retrieving identities").WithInternalError(terr)
-		}
-
-		tokenString, terr = generateAccessToken(user, identities, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+		tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
 		if terr != nil {
 			return internalServerError("error generating jwt token").WithInternalError(terr)
 		}
@@ -216,7 +209,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 	})
 }
 
-func generateAccessToken(user *models.User, identities []*models.Identity, expiresIn time.Duration, secret string) (string, error) {
+func generateAccessToken(user *models.User, expiresIn time.Duration, secret string) (string, error) {
 	claims := &GoTrueClaims{
 		StandardClaims: jwt.StandardClaims{
 			Subject:   user.ID.String(),
@@ -227,7 +220,6 @@ func generateAccessToken(user *models.User, identities []*models.Identity, expir
 		Phone:        user.GetPhone(),
 		AppMetaData:  user.AppMetaData,
 		UserMetaData: user.UserMetaData,
-		Identities:   identities,
 		Role:         user.Role,
 	}
 
@@ -250,12 +242,8 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u
 		if terr != nil {
 			return internalServerError("Database error granting user").WithInternalError(terr)
 		}
-		identities, terr := models.FindIdentitiesByUser(tx, user)
-		if terr != nil {
-			return internalServerError("Database error granting user").WithInternalError(terr)
-		}
 
-		tokenString, terr = generateAccessToken(user, identities, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+		tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
 		if terr != nil {
 			return internalServerError("error generating jwt token").WithInternalError(terr)
 		}
diff --git a/conf/configuration.go b/conf/configuration.go
index ebe311de..c17fddb5 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -88,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"`
diff --git a/example.env b/example.env
index 73b973a9..df2578ce 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=""
@@ -76,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"
diff --git a/models/user.go b/models/user.go
index 647de21e..c7304465 100644
--- a/models/user.go
+++ b/models/user.go
@@ -73,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,


Test patch:
diff --git a/api/admin_test.go b/api/admin_test.go
index 2480307d..2ca648a3 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -54,10 +54,7 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string {
 
 	u.Role = "supabase_admin"
 
-	identities, err := models.FindIdentitiesByUser(ts.API.db, u)
-	require.NoError(ts.T(), err, "Error retrieving identities")
-
-	token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	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")
 
 	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
@@ -73,10 +70,7 @@ func (ts *AdminTestSuite) makeSystemUser() string {
 	u := models.NewSystemUser(uuid.Nil, ts.Config.JWT.Aud)
 	u.Role = "service_role"
 
-	identities, err := models.FindIdentitiesByUser(ts.API.db, u)
-	require.NoError(ts.T(), err, "Error retrieving identities")
-
-	token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	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")
 
 	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
diff --git a/api/audit_test.go b/api/audit_test.go
index a7972330..b6a3fdd0 100644
--- a/api/audit_test.go
+++ b/api/audit_test.go
@@ -51,9 +51,7 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string {
 
 	u.Role = "supabase_admin"
 
-	identities, err := models.FindIdentitiesByUser(ts.API.db, u)
-	require.NoError(ts.T(), err, "Error retrieving identities")
-	token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	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")
 
 	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
diff --git a/api/invite_test.go b/api/invite_test.go
index 6045caa7..8d105213 100644
--- a/api/invite_test.go
+++ b/api/invite_test.go
@@ -60,9 +60,7 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string {
 
 	u.Role = "supabase_admin"
 
-	identities, err := models.FindIdentitiesByUser(ts.API.db, u)
-	require.NoError(ts.T(), err, "Error retrieving identities")
-	token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	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")
 
 	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
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/user_test.go b/api/user_test.go
index bb8d22b4..f74bc955 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -62,10 +62,7 @@ func (ts *UserTestSuite) TestUser_UpdatePassword() {
 	req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer)
 	req.Header.Set("Content-Type", "application/json")
 
-	identities, err := models.FindIdentitiesByUser(ts.API.db, u)
-	require.NoError(ts.T(), err)
-
-	token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err)
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 
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