Repository: supabase/gotrue
Pull request #56 (2020-12-29 11:34:05)

Netlify Sync
get to a position where we can start contributing back


Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..bf079262
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,30 @@
+<!--
+If you are reporting a new issue, make sure that we do not have any duplicates
+already open. You can ensure this by searching the issue list for this
+repository. If there is a duplicate, please close your issue and add a comment
+to the existing issue instead.
+
+If you suspect your issue is a bug, please edit your issue description to
+include the BUG REPORT INFORMATION shown below. If you fail to provide this
+information within 7 days, we cannot debug your issue and will close it. We
+will, however, reopen it if you later provide the information.
+
+If you have an issue that can be shown visually, please provide a screenshot or
+gif of the problem as well.
+
+---------------------------------------------------
+BUG REPORT INFORMATION
+---------------------------------------------------
+Use the commands below to provide key information from your environment:
+You do NOT have to include this information if this is a FEATURE REQUEST
+-->
+
+**- Do you want to request a *feature* or report a *bug*?**
+
+**- What is the current behavior?**
+
+**- If the current behavior is a bug, please provide the steps to reproduce.**
+
+**- What is the expected behavior?**
+
+**- Please mention your Go version, and operating system version.**
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..72b16a33
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,35 @@
+<!--
+Thanks for submitting a pull request!
+
+Please make sure you've read and understood our contributing guidelines;
+https://github.com/netlify/gotrue/blob/master/CONTRIBUTING.md
+
+If this is a bug fix, make sure your description includes "fixes #xxxx", or
+"closes #xxxx", where #xxxx is the issue number.
+
+Please provide enough information so that others can review your pull request.
+The first three fields are mandatory:
+-->
+
+**- Summary**
+
+<!--
+Explain the **motivation** for making this change.
+What existing problem does the pull request solve?
+-->
+
+**- Test plan**
+
+<!--
+Demonstrate the code is solid.
+Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI.
+-->
+
+**- Description for the changelog**
+
+<!--
+Write a short (one line) summary that describes the changes in this
+pull request for inclusion in the changelog:
+-->
+
+**- A picture of a cute animal (not mandatory but encouraged)**
diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml
new file mode 100644
index 00000000..2cd74b45
--- /dev/null
+++ b/.github/workflows/fossa.yml
@@ -0,0 +1,31 @@
+name: Dependency License Scanning
+
+on:
+  push:
+    branches:
+      - master
+
+defaults:
+  run:
+    shell: bash
+
+jobs:
+  fossa:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Fossa init
+        run: |-
+          curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash
+          fossa init
+      - name: Set env
+        run: echo "line_number=$(grep -n "project" .fossa.yml | cut -f1 -d:)" >> $GITHUB_ENV
+      - name: Configuration
+        run: |-
+          sed -i "${line_number}s|.*|  project: git@github.com:${GITHUB_REPOSITORY}.git|" .fossa.yml
+          cat .fossa.yml
+      - name: Upload dependencies
+        run: fossa analyze --debug
+        env:
+          FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index e422b165..00000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-name: Build and Release on Push to Master
-
-on:
-  push:
-    branches:
-      - master
-
-jobs:
-  release:
-    strategy:
-      matrix:
-        node: ["12"]
-
-    outputs:
-      status: ${{ steps.pre-release.outputs.release != steps.post-release.outputs.release }}
-
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-
-      - id: pre-release
-        uses: pozetroninc/github-action-get-latest-release@master
-        with:
-          owner: supabase
-          repo: gotrue
-          excludes: prerelease, draft
-
-      - name: Set up Node
-        uses: actions/setup-node@v1
-        with:
-          node-version: ${{ matrix.node }}
-
-      - name: Release on GitHub
-        id: github-release
-        run: npx semantic-release -p        \
-          @semantic-release/commit-analyzer \
-          @semantic-release/github         \
-          @semantic-release/release-notes-generator
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-      - id: post-release
-        uses: pozetroninc/github-action-get-latest-release@master
-        with:
-          owner: supabase
-          repo: gotrue
-          excludes: prerelease, draft
-
-  deploy:
-    needs: release
-    runs-on: ubuntu-18.04
-    if: success() && needs.release.outputs.status == 'true'
-    steps:
-      - uses: actions/checkout@v2
-
-      - uses: actions/setup-go@v2
-        with:
-          go-version: "^1.14.4" # The Go version to download (if necessary) and use.
-
-      - run: make deps
-
-      - run: make all
-
-      - id: releases
-        uses: pozetroninc/github-action-get-latest-release@master
-        with:
-          owner: supabase
-          repo: gotrue
-          excludes: prerelease, draft
-
-      - run: tar -czvf gotrue-${{ steps.releases.outputs.release }}-x86.tar.gz gotrue
-      - run: mv gotrue-arm64 gotrue
-      - run: tar -czvf gotrue-${{ steps.releases.outputs.release }}-arm64.tar.gz gotrue
-
-      - uses: AButler/upload-release-assets@v2.0
-        with:
-          files: "gotrue-${{ steps.releases.outputs.release }}*.tar.gz"
-          release-tag: ${{ steps.releases.outputs.release }}
-          repo-token: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Upload image to Docker Hub
-        uses: docker/build-push-action@v1
-        with:
-          username: ${{ secrets.DOCKER_USERNAME }}
-          password: ${{ secrets.DOCKER_PASSWORD }}
-          repository: ${{ secrets.DOCKER_REPO }}
-          tags: latest,${{ steps.releases.outputs.release }}
diff --git a/.gitignore b/.gitignore
index 6aa7c669..efed79b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 .env
 vendor/
 gotrue
-gotrue-arm64
 
 .DS_Store
 .vscode
diff --git a/Dockerfile b/Dockerfile
index 74322c49..f5e446d8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,10 +6,14 @@ ENV GOOS=linux
 RUN apk add --no-cache make git
 
 WORKDIR /go/src/github.com/netlify/gotrue
-COPY . /go/src/github.com/netlify/gotrue
 
-RUN make deps build
+# Pulling dependencies
+COPY ./Makefile ./go.* ./
+RUN make deps
 
+# Building stuff
+COPY . /go/src/github.com/netlify/gotrue
+RUN make build
 
 FROM alpine:3.7
 RUN adduser -D -u 1000 netlify
diff --git a/Makefile b/Makefile
index f460c8ef..fc7d6191 100644
--- a/Makefile
+++ b/Makefile
@@ -1,18 +1,16 @@
 .PHONY: all build deps image lint migrate test vet
 CHECK_FILES?=$$(go list ./... | grep -v /vendor/)
-FLAGS?=-ldflags "-X github.com/netlify/gotrue/cmd.Version=`git rev-parse HEAD`"
 
 help: ## Show this help.
 	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
 
-all: lint vet build ## Run the tests and build the binary.
+all: lint vet test build ## Run the tests and build the binary.
 
 build: ## Build the binary.
-	go build $(FLAGS)
-	GOOS=linux GOARCH=arm64 go build $(FLAGS) -o gotrue-arm64
+	go build -ldflags "-X github.com/netlify/gotrue/cmd.Version=`git rev-parse HEAD`"
 
 deps: ## Install dependencies.
-	@go get -u github.com/gobuffalo/pop/soda
+	@go get -u github.com/gobuffalo/pop/v5/soda
 	@go get -u golang.org/x/lint/golint
 	@go mod download
 
diff --git a/README.md b/README.md
index 7df4caf8..ed33aec3 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ environment variables, or a combination of both. Environment variables are prefi
 
 ### Top-Level
 
-```
+```properties
 GOTRUE_SITE_URL=https://example.netlify.com/
 ```
 
@@ -36,7 +36,7 @@ Header on which to rate limit the `/token` endpoint.
 
 ### API
 
-```
+```properties
 GOTRUE_API_HOST=localhost
 PORT=9999
 ```
@@ -59,7 +59,7 @@ If you wish to inherit a request ID from the incoming request, specify the name
 
 ### Database
 
-```
+```properties
 GOTRUE_DB_DRIVER=mysql
 DATABASE_URL=root@localhost/gotrue
 ```
@@ -86,7 +86,7 @@ you've built gotrue.
 
 ### Logging
 
-```
+```properties
 LOG_LEVEL=debug # available without GOTRUE prefix (exception)
 GOTRUE_LOG_FILE=/var/log/go/gotrue.log
 ```
@@ -99,9 +99,40 @@ 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.
 
-### JSON Web Tokens (JWT)
+### Opentracing
+Currently, only the Datadog tracer is supported.
 
+```properties
+GOTRUE_TRACING_ENABLED=true
+GOTRUE_TRACING_HOST=127.0.0.1
+GOTRUE_TRACING_PORT=8126
+GOTRUE_TRACING_TAGS="tag1:value1,tag2:value2"
+GOTRUE_SERVICE_NAME="gotrue"
 ```
+
+`TRACING_ENABLED` - `bool`
+
+Whether tracing is enabled or not. Defaults to `false`.
+
+`TRACING_HOST` - `bool`
+
+The tracing destination.
+
+`TRACING_PORT` - `bool`
+
+The port for the tracing host.
+
+`TRACING_TAGS` - `string`
+
+A comma separated list of key:value pairs. These key value pairs will be added as tags to all opentracing spans.
+
+`SERVICE_NAME` - `string`
+
+The name to use for the service.
+
+### JSON Web Tokens (JWT)
+
+```properties
 GOTRUE_JWT_SECRET=supersecretvalue
 GOTRUE_JWT_EXP=3600
 GOTRUE_JWT_AUD=netlify
@@ -132,7 +163,7 @@ The default group to assign all new users to.
 We support `bitbucket`, `github`, `gitlab`, and `google` for external authentication.
 Use the names as the keys underneath `external` to configure each separately.
 
-```
+```properties
 GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=myappclientid
 GOTRUE_EXTERNAL_GITHUB_SECRET=clientsecretvaluessssh
 ```
@@ -164,7 +195,7 @@ The base URL used for constructing the URLs to request authorization and access
 Sending email is not required, but highly recommended for password recovery.
 If enabled, you must provide the required values below.
 
-```
+```properties
 GOTRUE_SMTP_HOST=smtp.mandrillapp.com
 GOTRUE_SMTP_PORT=587
 GOTRUE_SMTP_USER=smtp-delivery@example.com
@@ -229,10 +260,6 @@ Email subject to use for signup confirmation. Defaults to `Confirm Your Signup`.
 
 Email subject to use for password reset. Defaults to `Reset Your Password`.
 
-`MAILER_SUBJECTS_MAGIC_LINK` - `string`
-
-Email subject to use for magic link email. Defaults to `Your Magic Link`.
-
 `MAILER_SUBJECTS_EMAIL_CHANGE` - `string`
 
 Email subject to use for email change confirmation. Defaults to `Confirm Email Change`.
@@ -279,20 +306,6 @@ Default Content (if template is unavailable):
 <p><a href="{{ .ConfirmationURL }}">Reset Password</a></p>
 ```
 
-`MAILER_TEMPLATES_MAGIC_LINK` - `string`
-
-URL path to an email template to use when sending magic link.
-`SiteURL`, `Email`, and `ConfirmationURL` variables are available.
-
-Default Content (if template is unavailable):
-
-```html
-<h2>Magic Link</h2>
-
-<p>Follow this link to login:</p>
-<p><a href="{{ .ConfirmationURL }}">Log In</a></p>
-```
-
 `MAILER_TEMPLATES_EMAIL_CHANGE` - `string`
 
 URL path to an email template to use when confirming the change of an email address.
@@ -307,11 +320,32 @@ Default Content (if template is unavailable):
 <p><a href="{{ .ConfirmationURL }}">Change Email</a></p>
 ```
 
+`WEBHOOK_URL` - `string`
+
+Url of the webhook receiver endpoint. This will be called when events like `validate`, `signup` or `login` occur.
+
+`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.
+
+`WEBHOOK_RETRIES` - `number`
+
+How often GoTrue should try a failed hook.
+
+`WEBHOOK_TIMEOUT_SEC` - `number`
+
+Time between retries (in seconds).
+
+`WEBHOOK_EVENTS` - `list`
+
+Which events should trigger a webhook. You can provide a comma separated list.
+For example to listen to all events, provide the values `validate,signup,login`.
+
 ## Endpoints
 
 GoTrue exposes the following endpoints:
 
-### **GET /settings**
+* **GET /settings**
 
   Returns the publicly available settings for this gotrue instance.
 
@@ -328,7 +362,7 @@ GoTrue exposes the following endpoints:
   }
   ```
 
-### **POST /signup**
+* **POST /signup**
 
   Register a new user with an email and password.
 
@@ -351,17 +385,9 @@ GoTrue exposes the following endpoints:
   }
   ```
 
-### **POST /invite**
+* **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:
-
-  e.g.
-  ```json
-  headers: {
-    "Authorization" : "Bearer eyJhbGciOiJI...M3A90LCkxxtX9oNP9KZO"
-  }
-  ```
 
   ```json
   {
@@ -382,18 +408,19 @@ GoTrue exposes the following endpoints:
   }
   ```
 
-### **POST /verify**
+* **POST /verify**
 
-  Verify a registration or a password recovery. Type can be `signup` or `recovery` or `invite`
+  Verify a registration or a password recovery. Type can be `signup` or `recovery`
   and the `token` is a token returned from either `/signup` or `/recover`.
 
   ```json
   {
     "type": "signup",
-    "token": "confirmation-code-delivered-in-email"
+    "token": "confirmation-code-delivered-in-email",
+    "password": "12345abcdef"
   }
   ```
-  
+
   `password` is required for signup verification if no existing password exists.
 
   Returns:
@@ -403,62 +430,14 @@ GoTrue exposes the following endpoints:
     "access_token": "jwt-token-representing-the-user",
     "token_type": "bearer",
     "expires_in": 3600,
-    "refresh_token": "a-refresh-token",
-    "type": "signup | recovery | invite"
-  }
-  ```
-
-### **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`.
-
-  query params:
-  ```json
-  {
-    "type": "signup",
-    "token": "confirmation-code-delivered-in-email",
-  }
-  ```
-
-  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
-  ```
-
-  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
-
-### **POST /magiclink**
-
-  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
-
-  ```json
-  {
-    "email": "email@example.com"
+    "refresh_token": "a-refresh-token"
   }
   ```
 
-  Returns:
-
-  ```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)
-
-### **POST /recover**
+* **POST /recover**
 
   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
 
   ```json
   {
@@ -472,36 +451,19 @@ GoTrue exposes the following endpoints:
   {}
   ```
 
-### **POST /token**
+* **POST /token**
 
   This is an OAuth2 endpoint that currently implements
-  the password and refresh_token grant types
+  the password, refresh_token, and authorization_code grant types
 
-  query params:
-  ```
-  ?grant_type=password
   ```
-
-  body:
-  ```json
-  {
-    "email": "name@domain.com",
-    "password": "somepassword",
-  }
+  grant_type=password&username=email@example.com&password=secret
   ```
 
   or
 
-  query params:
   ```
-  grant_type=refresh_token
-  ```
-
-  body:
-  ```json
-  {
-    "refresh_token": "a-refresh-token"
-  }
+  grant_type=refresh_token&refresh_token=my-refresh-token
   ```
 
   Once you have an access token, you can access the methods requiring authentication
@@ -518,7 +480,7 @@ GoTrue exposes the following endpoints:
   }
   ```
 
-### **GET /user**
+* **GET /user**
 
   Get the JSON object for the logged in user (requires authentication)
 
@@ -534,7 +496,7 @@ GoTrue exposes the following endpoints:
   }
   ```
 
-### **PUT /user**
+* **PUT /user**
 
   Update a user (Requires authentication). Apart from changing email/password, this
   method can be used to set custom user data.
@@ -563,32 +525,13 @@ GoTrue exposes the following endpoints:
   }
   ```
 
-### **POST /logout**
+* **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.
 
-
-### **GET /authorize**
-
-  Get access_token from external oauth provider
-
-  query params:
-  ```
-  provider=google | bitbucket | github | gitlab
-  ```
- 
-  Redirects to provider and then to `/callback`
-  
-### **GET /callback**
-
-  External provider should redirect to here
- 
-  Redirects to `<GOTRUE_SITE_URL>#access_token=<access_token>&refresh_token=<refresh_token>&expires_in=3600&provider=<provider_name>`
-  
-
 ## TODO
 
-* Schema for custom user data in config file 
+* Schema for custom user data in config file
diff --git a/api/api.go b/api/api.go
index 8ed4211d..50bef826 100644
--- a/api/api.go
+++ b/api/api.go
@@ -87,6 +87,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 	r.UseBypass(xffmw.Handler)
 	r.Use(addRequestID(globalConfig))
 	r.Use(recoverer)
+	r.UseBypass(tracer)
 
 	r.Get("/health", api.HealthCheck)
 
@@ -116,18 +117,13 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 
 		r.With(api.requireEmailProvider).Post("/signup", api.Signup)
 		r.With(api.requireEmailProvider).Post("/recover", api.Recover)
-		r.With(api.requireEmailProvider).Post("/magiclink", api.MagicLink)
 		r.With(api.requireEmailProvider).With(api.limitHandler(
 			// Allow requests at a rate of 30 per 5 minutes.
 			tollbooth.NewLimiter(30.0/(60*5), &limiter.ExpirableOptions{
 				DefaultExpirationTTL: time.Hour,
 			}).SetBurst(30),
 		)).Post("/token", api.Token)
-
-		r.Route("/verify", func(r *router) {
-			r.Get("/", api.Verify)
-			r.Post("/", api.Verify)
-		})
+		r.Post("/verify", api.Verify)
 
 		r.With(api.requireAuthentication).Post("/logout", api.Logout)
 
diff --git a/api/auth.go b/api/auth.go
index bfdfb185..f3d20202 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -2,12 +2,10 @@ package api
 
 import (
 	"context"
-	"fmt"
+	"encoding/json"
 	"net/http"
-	"time"
 
 	jwt "github.com/dgrijalva/jwt-go"
-	"github.com/netlify/gotrue/models"
 )
 
 // requireAuthentication checks incoming requests for tokens presented using the Authorization header
@@ -21,23 +19,44 @@ func (a *API) requireAuthentication(w http.ResponseWriter, r *http.Request) (con
 	return a.parseJWTClaims(token, r, w)
 }
 
+type adminCheckParams struct {
+	Aud string `json:"aud"`
+}
+
 func (a *API) requireAdmin(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, error) {
 	// Find the administrative user
-	claims := getClaims(ctx)
-	if claims == nil {
-		fmt.Printf("[%s] %s %s %d %s\n", time.Now().Format("2006-01-02 15:04:05"), r.Method, r.RequestURI, http.StatusForbidden, "Invalid token")
-		return nil, unauthorizedError("Invalid token")
+	adminUser, err := getUserFromClaims(ctx, a.db)
+	if err != nil {
+		return nil, unauthorizedError("Invalid admin user").WithInternalError(err)
 	}
 
-	adminRoles := a.getConfig(ctx).JWT.AdminRoles
+	aud := a.requestAud(ctx, r)
+	if r.Body != nil && r.Body != http.NoBody {
+		c, err := addGetBody(w, r)
+		if err != nil {
+			return nil, internalServerError("Error getting body").WithInternalError(err)
+		}
+		ctx = c
 
-	if isStringInSlice(claims.Role, adminRoles) {
-		// successful authentication
-		return withAdminUser(ctx, &models.User{}), nil
+		params := adminCheckParams{}
+		bod, err := r.GetBody()
+		if err != nil {
+			return nil, internalServerError("Error getting body").WithInternalError(err)
+		}
+		err = json.NewDecoder(bod).Decode(&params)
+		if err != nil {
+			return nil, badRequestError("Could not decode admin user params: %v", err)
+		}
+		if params.Aud != "" {
+			aud = params.Aud
+		}
 	}
 
-	fmt.Printf("[%s] %s %s %d %s\n", time.Now().Format("2006-01-02 15:04:05"), r.Method, r.RequestURI, http.StatusForbidden, "this token needs role 'supabase_admin' or 'service_role'")
-	return nil, unauthorizedError("User not allowed")
+	// Make sure user is admin
+	if !a.isAdmin(ctx, adminUser, aud) {
+		return nil, unauthorizedError("User not allowed")
+	}
+	return withAdminUser(ctx, adminUser), nil
 }
 
 func (a *API) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
diff --git a/api/context.go b/api/context.go
index 22e38536..44cf364d 100644
--- a/api/context.go
+++ b/api/context.go
@@ -4,9 +4,9 @@ import (
 	"context"
 
 	jwt "github.com/dgrijalva/jwt-go"
+	"github.com/gobuffalo/uuid"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/models"
-	"github.com/gobuffalo/uuid"
 )
 
 type contextKey string
@@ -196,18 +196,18 @@ func getExternalReferrer(ctx context.Context) string {
 }
 
 // withFunctionHooks adds the provided function hooks to the context.
-func withFunctionHooks(ctx context.Context, hooks map[string]string) context.Context {
+func withFunctionHooks(ctx context.Context, hooks map[string][]string) context.Context {
 	return context.WithValue(ctx, functionHooksKey, hooks)
 }
 
 // getFunctionHooks reads the request ID from the context.
-func getFunctionHooks(ctx context.Context) map[string]string {
+func getFunctionHooks(ctx context.Context) map[string][]string {
 	obj := ctx.Value(functionHooksKey)
 	if obj == nil {
-		return map[string]string{}
+		return map[string][]string{}
 	}
 
-	return obj.(map[string]string)
+	return obj.(map[string][]string)
 }
 
 // withAdminUser adds the admin user to the context.
diff --git a/api/errors.go b/api/errors.go
index 5a6a5231..1b427a07 100644
--- a/api/errors.go
+++ b/api/errors.go
@@ -67,10 +67,6 @@ func notFoundError(fmtString string, args ...interface{}) *HTTPError {
 	return httpError(http.StatusNotFound, fmtString, args...)
 }
 
-func expiredTokenError(fmtString string, args ...interface{}) *HTTPError {
-	return httpError(http.StatusGone, fmtString, args...)
-}
-
 func unauthorizedError(fmtString string, args ...interface{}) *HTTPError {
 	return httpError(http.StatusUnauthorized, fmtString, args...)
 }
@@ -83,10 +79,6 @@ func unprocessableEntityError(fmtString string, args ...interface{}) *HTTPError
 	return httpError(http.StatusUnprocessableEntity, fmtString, args...)
 }
 
-func tooManyRequestsError(fmtString string, args ...interface{}) *HTTPError {
-	return httpError(http.StatusTooManyRequests, fmtString, args...)
-}
-
 // HTTPError is an error with a message and an HTTP status code.
 type HTTPError struct {
 	Code            int    `json:"code"`
diff --git a/api/external.go b/api/external.go
index 4666f165..b6c0aebd 100644
--- a/api/external.go
+++ b/api/external.go
@@ -177,7 +177,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 				if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil {
 					return terr
 				}
-				if terr = triggerHook(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
+				if terr = triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
 					return terr
 				}
 
@@ -189,7 +189,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 				if terr := models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil {
 					return terr
 				}
-				if terr = triggerHook(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
+				if terr = triggerEventHooks(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
 					return terr
 				}
 			}
@@ -261,7 +261,7 @@ func (a *API) processInvite(ctx context.Context, tx *storage.Connection, userDat
 	if err := models.NewAuditLogEntry(tx, instanceID, user, models.InviteAcceptedAction, nil); err != nil {
 		return nil, err
 	}
-	if err := triggerHook(ctx, tx, SignupEvent, user, instanceID, config); err != nil {
+	if err := triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); err != nil {
 		return nil, err
 	}
 
@@ -322,7 +322,7 @@ func (a *API) redirectErrors(handler apiHandler, w http.ResponseWriter, r *http.
 	err := handler(w, r)
 	if err != nil {
 		q := getErrorQueryString(err, errorID, log)
-		http.Redirect(w, r, a.getExternalRedirectURL(r)+"?"+q.Encode(), http.StatusFound)
+		http.Redirect(w, r, a.getExternalRedirectURL(r)+"#"+q.Encode(), http.StatusFound)
 	}
 }
 
diff --git a/api/helpers.go b/api/helpers.go
index 852eb1f9..b6380956 100644
--- a/api/helpers.go
+++ b/api/helpers.go
@@ -213,12 +213,3 @@ func SafeHTTPClient(client *http.Client, log logrus.FieldLogger) *http.Client {
 
 	return client
 }
-
-func isStringInSlice(checkValue string, list []string) bool {
-	for _, val := range list {
-		if val == checkValue {
-			return true
-		}
-	}
-	return false
-}
diff --git a/api/hooks.go b/api/hooks.go
index 62e052f4..ed5c07df 100644
--- a/api/hooks.go
+++ b/api/hooks.go
@@ -152,39 +152,37 @@ func closeBody(rsp *http.Response) {
 	}
 }
 
-func triggerHook(ctx context.Context, conn *storage.Connection, event HookEvent, user *models.User, instanceID uuid.UUID, config *conf.Configuration) error {
-	var hookURL *url.URL
-	secret := config.Webhook.Secret
+func triggerEventHooks(ctx context.Context, conn *storage.Connection, event HookEvent, user *models.User, instanceID uuid.UUID, config *conf.Configuration) error {
 	if config.Webhook.URL != "" {
-		var err error
-		hookURL, err = url.Parse(config.Webhook.URL)
+		hookURL, err := url.Parse(config.Webhook.URL)
 		if err != nil {
 			return errors.Wrapf(err, "Failed to parse Webhook URL")
 		}
 		if !config.Webhook.HasEvent(string(event)) {
 			return nil
 		}
+		return triggerHook(ctx, hookURL, config.Webhook.Secret, conn, event, user, instanceID, config)
 	}
 
-	if hookURL == nil {
-		fun := getFunctionHooks(ctx)
-		if fun == nil {
-			return nil
-		}
+	fun := getFunctionHooks(ctx)
+	if fun == nil {
+		return nil
+	}
 
-		if eventHookURL, ok := fun[string(event)]; ok {
-			var err error
-			hookURL, err = url.Parse(eventHookURL)
-			if err != nil {
-				return errors.Wrapf(err, "Failed to parse Event Function Hook URL")
-			}
-			secret = config.JWT.Secret
-		} else {
-			// abort hook call if there are no functions for this event
-			return nil
+	for _, eventHookURL := range fun[string(event)] {
+		hookURL, err := url.Parse(eventHookURL)
+		if err != nil {
+			return errors.Wrapf(err, "Failed to parse Event Function Hook URL")
+		}
+		err = triggerHook(ctx, hookURL, config.JWT.Secret, conn, event, user, instanceID, config)
+		if err != nil {
+			return err
 		}
 	}
+	return nil
+}
 
+func triggerHook(ctx context.Context, hookURL *url.URL, secret string, conn *storage.Connection, event HookEvent, user *models.User, instanceID uuid.UUID, config *conf.Configuration) error {
 	if !hookURL.IsAbs() {
 		siteURL, err := url.Parse(config.SiteURL)
 		if err != nil {
diff --git a/api/instance.go b/api/instance.go
index f224aa34..dda50516 100644
--- a/api/instance.go
+++ b/api/instance.go
@@ -103,15 +103,13 @@ func (a *API) GetInstance(w http.ResponseWriter, r *http.Request) error {
 func (a *API) UpdateInstance(w http.ResponseWriter, r *http.Request) error {
 	i := getInstance(r.Context())
 
-	params := InstanceRequestParams{}
+	params := InstanceRequestParams{BaseConfig: i.BaseConfig}
 	if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
 		return badRequestError("Error decoding params: %v", err)
 	}
 
-	if params.BaseConfig != nil {
-		if err := i.UpdateConfig(a.db, params.BaseConfig); err != nil {
-			return internalServerError("Database error updating instance").WithInternalError(err)
-		}
+	if err := i.UpdateConfig(a.db, params.BaseConfig); err != nil {
+		return internalServerError("Database error updating instance").WithInternalError(err)
 	}
 
 	// Hide SMTP credential from response
diff --git a/api/magic_link.go b/api/magic_link.go
deleted file mode 100644
index 309776ad..00000000
--- a/api/magic_link.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package api
-
-import (
-	"encoding/json"
-	"errors"
-	"io/ioutil"
-	"net/http"
-	"strings"
-
-	"github.com/netlify/gotrue/models"
-	"github.com/netlify/gotrue/storage"
-	"github.com/sethvargo/go-password/password"
-)
-
-// MagicLinkParams holds the parameters for a magic link request
-type MagicLinkParams struct {
-	Email string `json:"email"`
-}
-
-// MagicLink sends a recovery email
-func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error {
-	ctx := r.Context()
-	config := a.getConfig(ctx)
-	instanceID := getInstanceID(ctx)
-	params := &MagicLinkParams{}
-	jsonDecoder := json.NewDecoder(r.Body)
-	err := jsonDecoder.Decode(params)
-	if err != nil {
-		return badRequestError("Could not read verification params: %v", err)
-	}
-
-	if params.Email == "" {
-		return unprocessableEntityError("Password recovery requires an email")
-	}
-	if err := a.validateEmail(ctx, params.Email); err != nil {
-		return err
-	}
-
-	aud := a.requestAud(ctx, r)
-	user, err := models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud)
-	if err != nil {
-		if models.IsNotFoundError(err) {
-			// User doesn't exist, sign them up with temporary password
-			password, err := password.Generate(64, 10, 0, false, true)
-			if err != nil {
-				internalServerError("error creating user").WithInternalError(err)
-			}
-			newBodyContent := `{"email":"` + params.Email + `","password":"` + password + `"}`
-			r.Body = ioutil.NopCloser(strings.NewReader(newBodyContent))
-			r.ContentLength = int64(len(newBodyContent))
-
-			fakeResponse := &responseStub{}
-			if config.Mailer.Autoconfirm {
-				// signups are autoconfirmed, send magic link after signup
-				if err := a.Signup(fakeResponse, r); err != nil {
-					return err
-				}
-				newBodyContent := `{"email":"` + params.Email + `"}`
-				r.Body = ioutil.NopCloser(strings.NewReader(newBodyContent))
-				r.ContentLength = int64(len(newBodyContent))
-				return a.MagicLink(w, r)
-			}
-			// otherwise confirmation email already contains 'magic link'
-			if err := a.Signup(fakeResponse, r); err != nil {
-				return err
-			}
-
-			return sendJSON(w, http.StatusOK, make(map[string]string))
-		}
-		return internalServerError("Database error finding user").WithInternalError(err)
-	}
-
-	err = a.db.Transaction(func(tx *storage.Connection) error {
-		if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); terr != nil {
-			return terr
-		}
-
-		mailer := a.Mailer(ctx)
-		referrer := a.getReferrer(r)
-		return a.sendMagicLink(tx, user, mailer, config.SMTP.MaxFrequency, referrer)
-	})
-	if err != nil {
-		if errors.Is(err, MaxFrequencyLimitError) {
-			return tooManyRequestsError("For security purposes, you can only request this once every 60 seconds")
-		}
-		return internalServerError("Error sending magic link").WithInternalError(err)
-	}
-
-	return sendJSON(w, http.StatusOK, make(map[string]string))
-}
-
-// responseStub only implement http responsewriter for ignoring
-// incoming data from methods where it passed
-type responseStub struct {
-}
-
-func (rw *responseStub) Header() http.Header {
-	return http.Header{}
-}
-
-func (rw *responseStub) Write(data []byte) (int, error) {
-	return 1, nil
-}
-
-func (rw *responseStub) WriteHeader(statusCode int) {
-}
diff --git a/api/mail.go b/api/mail.go
index c51d935d..2ecb3a2d 100644
--- a/api/mail.go
+++ b/api/mail.go
@@ -11,13 +11,9 @@ import (
 	"github.com/pkg/errors"
 )
 
-var (
-	MaxFrequencyLimitError error = errors.New("Frequency limit reached")
-)
-
 func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error {
 	if u.ConfirmationSentAt != nil && !u.ConfirmationSentAt.Add(maxFrequency).Before(time.Now()) {
-		return MaxFrequencyLimitError
+		return nil
 	}
 
 	oldToken := u.ConfirmationToken
@@ -40,13 +36,12 @@ func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, re
 		return errors.Wrap(err, "Error sending invite email")
 	}
 	u.InvitedAt = &now
-	u.ConfirmationSentAt = &now
-	return errors.Wrap(tx.UpdateOnly(u, "confirmation_token", "confirmation_sent_at", "invited_at"), "Database error updating user for invite")
+	return errors.Wrap(tx.UpdateOnly(u, "confirmation_token", "invited_at"), "Database error updating user for invite")
 }
 
 func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error {
 	if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) {
-		return MaxFrequencyLimitError
+		return nil
 	}
 
 	oldToken := u.RecoveryToken
@@ -60,24 +55,6 @@ func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, maile
 	return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery")
 }
 
-func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error {
-	// since Magic Link is just a recovery with a different template and behaviour
-	// around new users we will reuse the recovery db timer to prevent potential abuse
-	if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) {
-		return MaxFrequencyLimitError
-	}
-
-	oldToken := u.RecoveryToken
-	u.RecoveryToken = crypto.SecureToken()
-	now := time.Now()
-	if err := mailer.MagicLinkMail(u, referrerURL); err != nil {
-		u.RecoveryToken = oldToken
-		return errors.Wrap(err, "Error sending magic link email")
-	}
-	u.RecoverySentAt = &now
-	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 {
 	oldToken := u.EmailChangeToken
 	oldEmail := u.EmailChange
diff --git a/api/middleware.go b/api/middleware.go
index 1b51c656..42166312 100644
--- a/api/middleware.go
+++ b/api/middleware.go
@@ -3,6 +3,7 @@ package api
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -18,12 +19,36 @@ const (
 	jwsSignatureHeaderName = "x-nf-sign"
 )
 
+type FunctionHooks map[string][]string
+
 type NetlifyMicroserviceClaims struct {
 	jwt.StandardClaims
-	SiteURL       string            `json:"site_url"`
-	InstanceID    string            `json:"id"`
-	NetlifyID     string            `json:"netlify_id"`
-	FunctionHooks map[string]string `json:"function_hooks"`
+	SiteURL       string        `json:"site_url"`
+	InstanceID    string        `json:"id"`
+	NetlifyID     string        `json:"netlify_id"`
+	FunctionHooks FunctionHooks `json:"function_hooks"`
+}
+
+func (f *FunctionHooks) UnmarshalJSON(b []byte) error {
+	var raw map[string][]string
+	err := json.Unmarshal(b, &raw)
+	if err == nil {
+		*f = FunctionHooks(raw)
+		return nil
+	}
+	// If unmarshaling into map[string][]string fails, try legacy format.
+	var legacy map[string]string
+	err = json.Unmarshal(b, &legacy)
+	if err != nil {
+		return err
+	}
+	if *f == nil {
+		*f = make(FunctionHooks)
+	}
+	for event, hook := range legacy {
+		(*f)[event] = []string{hook}
+	}
+	return nil
 }
 
 func addGetBody(w http.ResponseWriter, req *http.Request) (context.Context, error) {
diff --git a/api/recover.go b/api/recover.go
index 05de55d2..0f4b3c70 100644
--- a/api/recover.go
+++ b/api/recover.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"encoding/json"
-	"errors"
 	"net/http"
 
 	"github.com/netlify/gotrue/models"
@@ -49,9 +48,6 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error {
 		return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer)
 	})
 	if err != nil {
-		if errors.Is(err, MaxFrequencyLimitError) {
-			return tooManyRequestsError("For security purposes, you can only request this once every 60 seconds")
-		}
 		return internalServerError("Error recovering user").WithInternalError(err)
 	}
 
diff --git a/api/signup.go b/api/signup.go
index 9381a2a2..886b3093 100644
--- a/api/signup.go
+++ b/api/signup.go
@@ -3,10 +3,8 @@ package api
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"net/http"
 
-	"github.com/netlify/gotrue/metering"
 	"github.com/netlify/gotrue/models"
 	"github.com/netlify/gotrue/storage"
 )
@@ -24,7 +22,6 @@ type SignupParams struct {
 func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 	ctx := r.Context()
 	config := a.getConfig(ctx)
-	cookie := r.Header.Get(useCookieHeader)
 
 	if config.DisableSignup {
 		return forbiddenError("Signups not allowed for this instance")
@@ -73,7 +70,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 			if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil {
 				return terr
 			}
-			if terr = triggerHook(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
+			if terr = triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
 				return terr
 			}
 			if terr = user.Confirm(tx); terr != nil {
@@ -90,42 +87,9 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 	})
 
 	if err != nil {
-		if errors.Is(err, MaxFrequencyLimitError) {
-			return tooManyRequestsError("For security purposes, you can only request this once every minute")
-		}
 		return err
 	}
 
-	if user.IsConfirmed() {
-		var token *AccessTokenResponse
-		err = a.db.Transaction(func(tx *storage.Connection) error {
-			var terr error
-			if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil {
-				return terr
-			}
-			if terr = triggerHook(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
-				return terr
-			}
-
-			token, terr = a.issueRefreshToken(ctx, tx, user)
-			if terr != nil {
-				return terr
-			}
-
-			if cookie != "" && config.Cookie.Duration > 0 {
-				if terr = a.setCookieToken(config, token.Token, cookie == useSessionCookie, w); terr != nil {
-					return internalServerError("Failed to set JWT cookie. %s", terr)
-				}
-			}
-			return nil
-		})
-		if err != nil {
-			return err
-		}
-		metering.RecordLogin("password", user.ID, instanceID)
-		token.User = user
-		return sendJSON(w, http.StatusOK, token)
-	}
 	return sendJSON(w, http.StatusOK, user)
 }
 
@@ -153,7 +117,7 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param
 		if terr := user.SetRole(tx, config.JWT.DefaultGroupName); terr != nil {
 			return internalServerError("Database error updating user").WithInternalError(terr)
 		}
-		if terr := triggerHook(ctx, tx, ValidateEvent, user, instanceID, config); terr != nil {
+		if terr := triggerEventHooks(ctx, tx, ValidateEvent, user, instanceID, config); terr != nil {
 			return terr
 		}
 		return nil
diff --git a/api/token.go b/api/token.go
index a7e092a5..19a6593f 100644
--- a/api/token.go
+++ b/api/token.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"context"
-	"encoding/json"
 	"net/http"
 	"time"
 
@@ -19,26 +18,13 @@ type GoTrueClaims struct {
 	Email        string                 `json:"email"`
 	AppMetaData  map[string]interface{} `json:"app_metadata"`
 	UserMetaData map[string]interface{} `json:"user_metadata"`
-	Role         string                 `json:"role"`
 }
 
 // AccessTokenResponse represents an OAuth2 success response
 type AccessTokenResponse struct {
-	Token        string       `json:"access_token"`
-	TokenType    string       `json:"token_type"` // Bearer
-	ExpiresIn    int          `json:"expires_in"`
-	RefreshToken string       `json:"refresh_token"`
-	User         *models.User `json:"user"`
-}
-
-// PasswordGrantParams are the parameters the ResourceOwnerPasswordGrant method accepts
-type PasswordGrantParams struct {
-	Email    string `json:"email"`
-	Password string `json:"password"`
-}
-
-// RefreshTokenGrantParams are the parameters the RefreshTokenGrant method accepts
-type RefreshTokenGrantParams struct {
+	Token        string `json:"access_token"`
+	TokenType    string `json:"token_type"` // Bearer
+	ExpiresIn    int    `json:"expires_in"`
 	RefreshToken string `json:"refresh_token"`
 }
 
@@ -62,23 +48,18 @@ func (a *API) Token(w http.ResponseWriter, r *http.Request) error {
 
 // ResourceOwnerPasswordGrant implements the password grant type flow
 func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-	params := &PasswordGrantParams{}
-
-	jsonDecoder := json.NewDecoder(r.Body)
-	if err := jsonDecoder.Decode(params); err != nil {
-		return badRequestError("Could not read password grant params: %v", err)
-	}
-
+	username := r.FormValue("username")
+	password := r.FormValue("password")
 	cookie := r.Header.Get(useCookieHeader)
 
 	aud := a.requestAud(ctx, r)
 	instanceID := getInstanceID(ctx)
 	config := a.getConfig(ctx)
 
-	user, err := models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud)
+	user, err := models.FindUserByEmailAndAudience(a.db, instanceID, username, aud)
 	if err != nil {
 		if models.IsNotFoundError(err) {
-			return oauthError("invalid_grant", "No user found with this email")
+			return oauthError("invalid_grant", "No user found with that email, or password invalid.")
 		}
 		return internalServerError("Database error finding user").WithInternalError(err)
 	}
@@ -87,8 +68,8 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 		return oauthError("invalid_grant", "Email not confirmed")
 	}
 
-	if !user.Authenticate(params.Password) {
-		return oauthError("invalid_grant", "Invalid Password")
+	if !user.Authenticate(password) {
+		return oauthError("invalid_grant", "No user found with that email, or password invalid.")
 	}
 
 	var token *AccessTokenResponse
@@ -97,7 +78,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 		if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil {
 			return terr
 		}
-		if terr = triggerHook(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
+		if terr = triggerEventHooks(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
 			return terr
 		}
 
@@ -117,7 +98,6 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 		return err
 	}
 	metering.RecordLogin("password", user.ID, instanceID)
-	token.User = user
 	return sendJSON(w, http.StatusOK, token)
 }
 
@@ -125,21 +105,14 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 	config := a.getConfig(ctx)
 	instanceID := getInstanceID(ctx)
-
-	params := &RefreshTokenGrantParams{}
-
-	jsonDecoder := json.NewDecoder(r.Body)
-	if err := jsonDecoder.Decode(params); err != nil {
-		return badRequestError("Could not read refresh token grant params: %v", err)
-	}
-
+	tokenStr := r.FormValue("refresh_token")
 	cookie := r.Header.Get(useCookieHeader)
 
-	if params.RefreshToken == "" {
+	if tokenStr == "" {
 		return oauthError("invalid_request", "refresh_token required")
 	}
 
-	user, token, err := models.FindUserWithRefreshToken(a.db, params.RefreshToken)
+	user, token, err := models.FindUserWithRefreshToken(a.db, tokenStr)
 	if err != nil {
 		if models.IsNotFoundError(err) {
 			return oauthError("invalid_grant", "Invalid Refresh Token")
@@ -187,7 +160,6 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 		TokenType:    "bearer",
 		ExpiresIn:    config.JWT.Exp,
 		RefreshToken: newToken.Token,
-		User:         user,
 	})
 }
 
@@ -201,7 +173,6 @@ func generateAccessToken(user *models.User, expiresIn time.Duration, secret stri
 		Email:        user.Email,
 		AppMetaData:  user.AppMetaData,
 		UserMetaData: user.UserMetaData,
-		Role:         user.Role,
 	}
 
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
diff --git a/api/tracer.go b/api/tracer.go
new file mode 100644
index 00000000..64652b8a
--- /dev/null
+++ b/api/tracer.go
@@ -0,0 +1,65 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/opentracing/opentracing-go"
+	"github.com/opentracing/opentracing-go/ext"
+	ddtrace_ext "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
+	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
+)
+
+type tracingResponseWriter struct {
+	http.ResponseWriter
+	statusCode int
+}
+
+func newTracingResponseWriter(w http.ResponseWriter) *tracingResponseWriter {
+	return &tracingResponseWriter{w, http.StatusOK}
+}
+
+func (trw *tracingResponseWriter) WriteHeader(code int) {
+	trw.statusCode = code
+	trw.ResponseWriter.WriteHeader(code)
+}
+
+func tracer(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		clientContext, _ := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
+		span, traceCtx := opentracing.StartSpanFromContext(r.Context(), "http.handler",
+			ext.RPCServerOption(clientContext),
+			opentracer.SpanType(ddtrace_ext.AppTypeWeb),
+		)
+		defer span.Finish()
+
+		ext.HTTPMethod.Set(span, r.Method)
+		ext.HTTPUrl.Set(span, r.URL.Path)
+		resourceName := r.URL.Path
+		resourceName = r.Method + " " + resourceName
+		span.SetTag("resource.name", resourceName)
+
+		if reqID := getRequestID(r.Context()); reqID != "" {
+			span.SetTag("http.request_id", reqID)
+		}
+
+		trw := newTracingResponseWriter(w)
+		next.ServeHTTP(trw, r.WithContext(traceCtx))
+
+		status := trw.statusCode
+
+		// Setting the status as an int doesn't propogate for use in datadog dashboards,
+		// so we convert to a string.
+		span.SetTag(string(ext.HTTPStatusCode), strconv.Itoa(status))
+
+		if status >= 500 && status < 600 {
+			ext.Error.Set(span, true)
+			span.SetTag("error.type", fmt.Sprintf("%d: %s", status, http.StatusText(status)))
+			span.LogKV(
+				"event", "error",
+				"message", fmt.Sprintf("%d: %s", status, http.StatusText(status)),
+			)
+		}
+	})
+}
diff --git a/api/verify.go b/api/verify.go
index 49f17b2c..4e79547e 100644
--- a/api/verify.go
+++ b/api/verify.go
@@ -3,27 +3,15 @@ package api
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"net/http"
-	"net/url"
-	"strconv"
-	"time"
 
 	"github.com/netlify/gotrue/models"
 	"github.com/netlify/gotrue/storage"
-	"github.com/sethvargo/go-password/password"
-)
-
-var (
-	// used below to specify need to return answer to user via specific redirect
-	redirectWithQueryError = errors.New("need return answer with query params")
 )
 
 const (
-	signupVerification    = "signup"
-	recoveryVerification  = "recovery"
-	inviteVerification    = "invite"
-	magicLinkVerification = "magiclink"
+	signupVerification   = "signup"
+	recoveryVerification = "recovery"
 )
 
 // VerifyParams are the parameters the Verify endpoint accepts
@@ -40,20 +28,9 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error {
 
 	params := &VerifyParams{}
 	cookie := r.Header.Get(useCookieHeader)
-
-	switch r.Method {
-	// GET only supports signup type
-	case "GET":
-		params.Token = r.FormValue("token")
-		params.Password = ""
-		params.Type = r.FormValue("type")
-	case "POST":
-		jsonDecoder := json.NewDecoder(r.Body)
-		if err := jsonDecoder.Decode(params); err != nil {
-			return badRequestError("Could not read verification params: %v", err)
-		}
-	default:
-		unprocessableEntityError("Sorry, only GET and POST methods are supported.")
+	jsonDecoder := json.NewDecoder(r.Body)
+	if err := jsonDecoder.Decode(params); err != nil {
+		return badRequestError("Could not read verification params: %v", err)
 	}
 
 	if params.Token == "" {
@@ -71,23 +48,13 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error {
 		switch params.Type {
 		case signupVerification:
 			user, terr = a.signupVerify(ctx, tx, params)
-		case inviteVerification:
-			user, terr = a.signupVerify(ctx, tx, params)
-		case recoveryVerification, magicLinkVerification:
+		case recoveryVerification:
 			user, terr = a.recoverVerify(ctx, tx, params)
 		default:
 			return unprocessableEntityError("Verify requires a verification type")
 		}
 
 		if terr != nil {
-			var e *HTTPError
-			if errors.As(terr, &e) {
-				if errors.Is(e.InternalError, redirectWithQueryError) {
-					rurl := a.prepErrorRedirectURL(e, r)
-					http.Redirect(w, r, rurl, http.StatusFound)
-					return nil
-				}
-			}
 			return terr
 		}
 
@@ -107,25 +74,7 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error {
 		return err
 	}
 
-	// GET requests should return to the app site after confirmation
-	switch r.Method {
-	case "GET":
-		rurl := config.SiteURL
-		if token != nil {
-			q := url.Values{}
-			q.Set("access_token", token.Token)
-			q.Set("token_type", token.TokenType)
-			q.Set("expires_in", strconv.Itoa(token.ExpiresIn))
-			q.Set("refresh_token", token.RefreshToken)
-			q.Set("type", params.Type)
-			rurl += "#" + q.Encode()
-		}
-		http.Redirect(w, r, rurl, http.StatusSeeOther)
-	case "POST":
-		return sendJSON(w, http.StatusOK, token)
-	}
-
-	return nil
+	return sendJSON(w, http.StatusOK, token)
 }
 
 func (a *API) signupVerify(ctx context.Context, conn *storage.Connection, params *VerifyParams) (*models.User, error) {
@@ -140,22 +89,14 @@ func (a *API) signupVerify(ctx context.Context, conn *storage.Connection, params
 		return nil, internalServerError("Database error finding user").WithInternalError(err)
 	}
 
-	nextDay := user.ConfirmationSentAt.Add(24 * time.Hour)
-	if user.ConfirmationSentAt != nil && time.Now().After(nextDay) {
-		return nil, expiredTokenError("Confirmation token expired")
-	}
-
 	err = conn.Transaction(func(tx *storage.Connection) error {
 		var terr error
 		if user.EncryptedPassword == "" {
 			if user.InvitedAt != nil {
-				// sign them up with temporary password, and require application
-				// to present the user with a password set form
-				password, err := password.Generate(64, 10, 0, false, true)
-				if err != nil {
-					internalServerError("error creating user").WithInternalError(err)
+				if params.Password == "" {
+					return unprocessableEntityError("Invited users must specify a password")
 				}
-				if terr = user.UpdatePassword(tx, password); terr != nil {
+				if terr = user.UpdatePassword(tx, params.Password); terr != nil {
 					return internalServerError("Error storing password").WithInternalError(terr)
 				}
 			}
@@ -165,7 +106,7 @@ func (a *API) signupVerify(ctx context.Context, conn *storage.Connection, params
 			return terr
 		}
 
-		if terr = triggerHook(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
+		if terr = triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
 			return terr
 		}
 
@@ -186,16 +127,11 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, param
 	user, err := models.FindUserByRecoveryToken(conn, params.Token)
 	if err != nil {
 		if models.IsNotFoundError(err) {
-			return nil, notFoundError(err.Error()).WithInternalError(redirectWithQueryError)
+			return nil, notFoundError(err.Error())
 		}
 		return nil, internalServerError("Database error finding user").WithInternalError(err)
 	}
 
-	nextDay := user.RecoverySentAt.Add(24 * time.Hour)
-	if user.RecoverySentAt != nil && time.Now().After(nextDay) {
-		return nil, expiredTokenError("Recovery token expired").WithInternalError(redirectWithQueryError)
-	}
-
 	err = conn.Transaction(func(tx *storage.Connection) error {
 		var terr error
 		if terr = user.Recover(tx); terr != nil {
@@ -206,7 +142,7 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, param
 				return terr
 			}
 
-			if terr = triggerHook(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
+			if terr = triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
 				return terr
 			}
 			if terr = user.Confirm(tx); terr != nil {
@@ -221,19 +157,3 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, param
 	}
 	return user, nil
 }
-
-func (a *API) prepErrorRedirectURL(err *HTTPError, r *http.Request) string {
-	ctx := r.Context()
-	rurl := a.getConfig(ctx).SiteURL
-	q := url.Values{}
-
-	log := getLogEntry(r)
-	log.Error(err.Message)
-
-	if str, ok := oauthErrorMap[err.Code]; ok {
-		q.Set("error", str)
-	}
-	q.Set("error_code", strconv.Itoa(err.Code))
-	q.Set("error_description", err.Message)
-	return rurl + "#" + q.Encode()
-}
diff --git a/app.json b/app.json
index d82737b7..b565dbad 100644
--- a/app.json
+++ b/app.json
@@ -24,11 +24,9 @@
     "GOTRUE_MAILER_SITE_URL": {},
     "GOTRUE_MAILER_SUBJECTS_CONFIRMATION": {},
     "GOTRUE_MAILER_SUBJECTS_RECOVERY": {},
-    "GOTRUE_MAILER_SUBJECTS_MAGIC_LINK": {},
     "GOTRUE_MAILER_TEMPLATES_CONFIRMATION": {},
     "GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE": {},
     "GOTRUE_MAILER_TEMPLATES_RECOVERY": {},
-    "GOTRUE_MAILER_TEMPLATES_MAGIC_LINK": {},
     "GOTRUE_MAILER_USER": {}
   }
 }
diff --git a/cmd/migrate_cmd.go b/cmd/migrate_cmd.go
index 16b317f7..dac13435 100644
--- a/cmd/migrate_cmd.go
+++ b/cmd/migrate_cmd.go
@@ -2,8 +2,9 @@ package cmd
 
 import (
 	"net/url"
+	"os"
 
-	"github.com/gobuffalo/pop"
+	"github.com/gobuffalo/pop/v5"
 	"github.com/netlify/gotrue/conf"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
@@ -56,7 +57,7 @@ func migrate(cmd *cobra.Command, args []string) {
 		logrus.Fatalf("%+v", errors.Wrap(err, "creating db migrator"))
 	}
 	logrus.Infof("before status")
-	err = mig.Status()
+	err = mig.Status(os.Stdout)
 	if err != nil {
 		logrus.Fatalf("%+v", errors.Wrap(err, "migration status"))
 	}
@@ -69,7 +70,7 @@ func migrate(cmd *cobra.Command, args []string) {
 	}
 
 	logrus.Infof("after status")
-	err = mig.Status()
+	err = mig.Status(os.Stdout)
 	if err != nil {
 		logrus.Fatalf("%+v", errors.Wrap(err, "migration status"))
 	}
diff --git a/conf/configuration.go b/conf/configuration.go
index 8b2f3863..ac0b5fb9 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -43,12 +43,11 @@ type DBConfiguration struct {
 
 // JWTConfiguration holds all the JWT related configuration.
 type JWTConfiguration struct {
-	Secret           string   `json:"secret" required:"true"`
-	Exp              int      `json:"exp"`
-	Aud              string   `json:"aud"`
-	AdminGroupName   string   `json:"admin_group_name" split_words:"true"`
-	AdminRoles       []string `json:"admin_roles" split_words:"true"`
-	DefaultGroupName string   `json:"default_group_name" split_words:"true"`
+	Secret           string `json:"secret" required:"true"`
+	Exp              int    `json:"exp"`
+	Aud              string `json:"aud"`
+	AdminGroupName   string `json:"admin_group_name" split_words:"true"`
+	DefaultGroupName string `json:"default_group_name" split_words:"true"`
 }
 
 // GlobalConfiguration holds all the configuration that applies to all instances.
@@ -58,13 +57,13 @@ type GlobalConfiguration struct {
 		Port            int `envconfig:"PORT" default:"8081"`
 		Endpoint        string
 		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:"true"`
 	MultiInstanceMode bool
+	Tracing           TracingConfig
 	SMTP              SMTPConfiguration
 	RateLimitHeader   string `split_words:"true"`
 }
@@ -75,7 +74,6 @@ type EmailContentConfiguration struct {
 	Confirmation string `json:"confirmation"`
 	Recovery     string `json:"recovery"`
 	EmailChange  string `json:"email_change" split_words:"true"`
-	MagicLink    string `json:"magic_link" split_words:"true"`
 }
 
 type ProviderConfiguration struct {
@@ -98,17 +96,19 @@ type SMTPConfiguration struct {
 	AdminEmail   string        `json:"admin_email" split_words:"true"`
 }
 
+type MailerConfiguration struct {
+	Autoconfirm bool                      `json:"autoconfirm"`
+	Subjects    EmailContentConfiguration `json:"subjects"`
+	Templates   EmailContentConfiguration `json:"templates"`
+	URLPaths    EmailContentConfiguration `json:"url_paths"`
+}
+
 // Configuration holds all the per-instance configuration.
 type Configuration struct {
-	SiteURL string            `json:"site_url" split_words:"true" required:"true"`
-	JWT     JWTConfiguration  `json:"jwt"`
-	SMTP    SMTPConfiguration `json:"smtp"`
-	Mailer  struct {
-		Autoconfirm bool                      `json:"autoconfirm"`
-		Subjects    EmailContentConfiguration `json:"subjects"`
-		Templates   EmailContentConfiguration `json:"templates"`
-		URLPaths    EmailContentConfiguration `json:"url_paths"`
-	} `json:"mailer"`
+	SiteURL       string                `json:"site_url" split_words:"true" required:"true"`
+	JWT           JWTConfiguration      `json:"jwt"`
+	SMTP          SMTPConfiguration     `json:"smtp"`
+	Mailer        MailerConfiguration   `json:"mailer"`
 	External      ProviderConfiguration `json:"external"`
 	DisableSignup bool                  `json:"disable_signup" split_words:"true"`
 	Webhook       WebhookConfig         `json:"webhook" split_words:"true"`
@@ -164,8 +164,10 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) {
 		return nil, err
 	}
 
+	ConfigureTracing(&config.Tracing)
+
 	if config.SMTP.MaxFrequency == 0 {
-		config.SMTP.MaxFrequency = 1 * time.Minute
+		config.SMTP.MaxFrequency = 15 * time.Minute
 	}
 	return config, nil
 }
@@ -190,10 +192,6 @@ func (config *Configuration) ApplyDefaults() {
 		config.JWT.AdminGroupName = "admin"
 	}
 
-	if config.JWT.AdminRoles == nil || len(config.JWT.AdminRoles) == 0 {
-		config.JWT.AdminRoles = []string{"service_role", "supabase_admin"}
-	}
-
 	if config.JWT.Exp == 0 {
 		config.JWT.Exp = 3600
 	}
@@ -212,7 +210,7 @@ func (config *Configuration) ApplyDefaults() {
 	}
 
 	if config.SMTP.MaxFrequency == 0 {
-		config.SMTP.MaxFrequency = 1 * time.Minute
+		config.SMTP.MaxFrequency = 15 * time.Minute
 	}
 
 	if config.Cookie.Key == "" {
diff --git a/conf/tracing.go b/conf/tracing.go
new file mode 100644
index 00000000..8f51ffe9
--- /dev/null
+++ b/conf/tracing.go
@@ -0,0 +1,38 @@
+package conf
+
+import (
+	"fmt"
+
+	"github.com/opentracing/opentracing-go"
+	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
+	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+)
+
+type TracingConfig struct {
+	Enabled     bool `default:"false"`
+	Host        string
+	Port        string
+	ServiceName string `default:"gotrue" split_words:"true"`
+	Tags        map[string]string
+}
+
+func (tc *TracingConfig) tracingAddr() string {
+	return fmt.Sprintf("%s:%s", tc.Host, tc.Port)
+}
+
+func ConfigureTracing(tc *TracingConfig) {
+	var t opentracing.Tracer = opentracing.NoopTracer{}
+	if tc.Enabled {
+		tracerOps := []tracer.StartOption{
+			tracer.WithServiceName(tc.ServiceName),
+			tracer.WithAgentAddr(tc.tracingAddr()),
+		}
+
+		for k, v := range tc.Tags {
+			tracerOps = append(tracerOps, tracer.WithGlobalTag(k, v))
+		}
+
+		t = opentracer.New(tracerOps...)
+	}
+	opentracing.SetGlobalTracer(t)
+}
diff --git a/example.env b/example.env
index 0a79d259..c5618218 100644
--- a/example.env
+++ b/example.env
@@ -13,7 +13,12 @@ GOTRUE_SMTP_PASS=super-secret-password
 GOTRUE_SMTP_ADMIN_EMAIL=admin@example.com
 GOTRUE_MAILER_SUBJECTS_CONFIRMATION="Welcome to GoTrue!"
 GOTRUE_MAILER_SUBJECTS_RECOVERY="Reset your GoTrue password!"
-GOTRUE_MAILER_SUBJECTS_MAGIC_LINK="Your Magic Link!"
 GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=http://localhost:9999/callback
 GOTRUE_LOG_LEVEL=DEBUG
 GOTRUE_OPERATOR_TOKEN=super-secret-operator-token
+
+GOTRUE_WEBHOOK_URL=http://register-lambda:3000/
+GOTRUE_WEBHOOK_SECRET=test_secret
+GOTRUE_WEBHOOK_RETRIES=5
+GOTRUE_WEBHOOK_TIMEOUT_SEC=3
+GOTRUE_WEBHOOK_EVENTS=validate,signup,login
diff --git a/go.mod b/go.mod
index 133599a5..ff8a3253 100644
--- a/go.mod
+++ b/go.mod
@@ -2,67 +2,38 @@ module github.com/netlify/gotrue
 
 require (
 	github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240
-	github.com/Masterminds/semver/v3 v3.1.0 // indirect
-	github.com/PuerkitoBio/goquery v1.5.1 // indirect
 	github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62
-	github.com/beevik/etree v0.0.0-20180609182452-90dafc1e1f11
-	github.com/blang/semver v3.5.1+incompatible // indirect
-	github.com/cockroachdb/cockroach-go v2.0.1+incompatible // indirect
+	github.com/beevik/etree v1.1.0
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
-	github.com/didip/tollbooth v4.0.2+incompatible
 	github.com/didip/tollbooth/v5 v5.1.1
 	github.com/go-chi/chi v4.0.2+incompatible
 	github.com/go-sql-driver/mysql v1.5.0
-	github.com/gobuffalo/attrs v1.0.0 // indirect
-	github.com/gobuffalo/envy v1.9.0 // indirect
-	github.com/gobuffalo/fizz v1.11.0 // indirect
-	github.com/gobuffalo/genny v0.6.0 // indirect
-	github.com/gobuffalo/gogen v0.2.0 // indirect
-	github.com/gobuffalo/here v0.6.2 // indirect
-	github.com/gobuffalo/mapi v1.1.0 // indirect
-	github.com/gobuffalo/nulls v0.4.0 // indirect
-	github.com/gobuffalo/packr/v2 v2.8.0 // indirect
-	github.com/gobuffalo/plushgen v0.1.2 // indirect
-	github.com/gobuffalo/pop v4.13.1+incompatible
-	github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 // indirect
+	github.com/gobuffalo/pop/v5 v5.3.1
 	github.com/gobuffalo/uuid v2.0.5+incompatible
-	github.com/gobuffalo/validate v2.0.4+incompatible // indirect
-	github.com/gobuffalo/validate/v3 v3.3.0 // indirect
-	github.com/gofrs/uuid v3.3.0+incompatible // indirect
 	github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639
-	github.com/jackc/pgconn v1.6.2 // indirect
 	github.com/joho/godotenv v1.3.0
-	github.com/karrick/godirwalk v1.15.6 // indirect
 	github.com/kelseyhightower/envconfig v1.4.0
-	github.com/lib/pq v1.7.0 // indirect
-	github.com/mattn/go-colorable v0.1.7 // indirect
-	github.com/microcosm-cc/bluemonday v1.0.3 // indirect
 	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.6.0 // indirect
 	github.com/rs/cors v1.6.0
-	github.com/russellhaering/gosaml2 v0.3.1
-	github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7
+	github.com/russellhaering/gosaml2 v0.6.0
+	github.com/russellhaering/goxmldsig v1.1.0
 	github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
-	github.com/sethvargo/go-password v0.2.0
-	github.com/sirupsen/logrus v1.6.0
-	github.com/spf13/cobra v1.0.0
+	github.com/sirupsen/logrus v1.4.2
+	github.com/spf13/cobra v0.0.6
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/stretchr/testify v1.5.1
-	golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
-	golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
-	golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
+	github.com/stretchr/testify v1.6.1
+	golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
 	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
-	golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
+	golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect
 	golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
-	golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6 // indirect
 	google.golang.org/api v0.0.0-20170821230356-dd6bdadc5852 // indirect
-	google.golang.org/appengine v1.6.6 // indirect
-	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
-	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
+	google.golang.org/appengine v1.6.5 // indirect
+	gopkg.in/DataDog/dd-trace-go.v1 v1.12.1
+	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
 	gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
-	gopkg.in/yaml.v2 v2.3.0 // indirect
 )
 
 go 1.13
diff --git a/go.sum b/go.sum
index 535cd768..fc4eaff9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,3 @@
-cloud.google.com/go v0.0.0-20170822200954-98f5696b1026 h1:h51R+fYV+r8EoCQei90mcXqCDhDaUHiMI2nmZAl9Y8U=
-cloud.google.com/go v0.0.0-20170822200954-98f5696b1026/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
@@ -9,41 +7,27 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240
 github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
 github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
 github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
-github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
-github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
-github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62 h1:vMqcPzLT1/mbYew0gM6EJy4/sCNy9lY9rmlFO+pPwhY=
 github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62/go.mod h1:r5ZalvRl3tXevRNJkwIB6DC4DD3DMjIlY9NEU1XGoaQ=
-github.com/beevik/etree v0.0.0-20180609182452-90dafc1e1f11 h1:C2jA9xWe9gk/WFMzvR6q0tT/TNNnIJUyeowUO1Y2/VQ=
-github.com/beevik/etree v0.0.0-20180609182452-90dafc1e1f11/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
+github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
-github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
-github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
-github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
-github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051 h1:eApuUG8W2EtBVwxqLlY2wgoqDYOg3WvIHGvW4fUbbow=
-github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
-github.com/cockroachdb/cockroach-go v2.0.1+incompatible h1:rkk9T7FViadPOz28xQ68o18jBSpyShru0mayVumxqYA=
-github.com/cockroachdb/cockroach-go v2.0.1+incompatible/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -60,13 +44,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/didip/tollbooth v1.0.0 h1:nVIxVp0Jj75TVxTwUdbRzC0qO0K/LRZudetGemvTCxQ=
-github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
-github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
 github.com/didip/tollbooth/v5 v5.1.1 h1:QpKFg56jsbNuQ6FFj++Z1gn2fbBsvAc1ZPLUaDOYW5k=
 github.com/didip/tollbooth/v5 v5.1.1/go.mod h1:d9rzwOULswrD3YIrAQmP3bfjxab32Df4IaO6+D25l9g=
-github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
@@ -75,8 +54,6 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
-github.com/go-chi/chi v3.1.4+incompatible h1:LZzu7MOZa3YkL+u2j94DZSotXAIAO2KW5xRZ6SGaF+A=
-github.com/go-chi/chi v3.1.4+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
 github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
 github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -91,118 +68,44 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobuffalo/attrs v0.1.0 h1:LY6/rbhPD/hfa+AfviaMXBmZBGb0fGHF12yg7f3dPQA=
 github.com/gobuffalo/attrs v0.1.0/go.mod h1:fmNpaWyHM0tRm8gCZWKx8yY9fvaNLo2PyzBNSrBZ5Hw=
-github.com/gobuffalo/attrs v1.0.0 h1:aAK8D55L0GNVbVRlaCwvAGE2BcGdEoVoK86vgQpuUZ8=
-github.com/gobuffalo/attrs v1.0.0/go.mod h1:YWU+sjOr7V05rNzvCoVc5qQTC2IgEz9434TFW5GhNaw=
-github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
-github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
-github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
-github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
+github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs=
 github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
-github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE=
-github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
-github.com/gobuffalo/fizz v1.9.5 h1:Qh0GkP7MYtJs9RZwBkPJ0CzEXynVowdNfrjg8b+TOxA=
-github.com/gobuffalo/fizz v1.9.5/go.mod h1:v9cFl56oXm+hNNayTsIQHnq209bTDUbIM8GYWCJw3TE=
+github.com/gobuffalo/fizz v1.10.0 h1:I8vad0PnmR+CLjSnZ5L5jlhBm4S88UIGOoZZL3/3e24=
 github.com/gobuffalo/fizz v1.10.0/go.mod h1:J2XGPO0AfJ1zKw7+2BA+6FEGAkyEsdCOLvN93WCT2WI=
-github.com/gobuffalo/fizz v1.11.0 h1:kJ+9sSSZpolHz4ULV1sF76AGmo74kEi6IztIc4HmshU=
-github.com/gobuffalo/fizz v1.11.0/go.mod h1:cXLjhE5p3iuIes6AGZ/9+dfyOkehlB2Vldj0Iw2Uu38=
 github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
-github.com/gobuffalo/flect v0.1.6 h1:D7KWNRFiCknJKA495/e1BO7oxqf8tbieaLv/ehoZ/+g=
-github.com/gobuffalo/flect v0.1.6/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
 github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
 github.com/gobuffalo/flect v0.2.1 h1:GPoRjEN0QObosV4XwuoWvSd5uSiL0N3e91/xqyY4crQ=
 github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc=
-github.com/gobuffalo/genny v0.2.0/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
-github.com/gobuffalo/genny v0.3.0/go.mod h1:ywJ2CoXrTZj7rbS8HTbzv7uybnLKlsNSBhEQ+yFI3E8=
-github.com/gobuffalo/genny v0.4.1 h1:ylgRyFoVGtfq92Ziq0kyi0Sdwh//pqWEwg+vD3eK1ZA=
-github.com/gobuffalo/genny v0.4.1/go.mod h1:dpded+KBgICFciAb+6R5Lo+1VxzofjqHgKqFYIL8M7U=
-github.com/gobuffalo/genny v0.6.0 h1:d7c6d66ZrTHHty01hDX1/TcTWvAJQxRZl885KWX5kHY=
-github.com/gobuffalo/genny v0.6.0/go.mod h1:Vigx9VDiNscYpa/LwrURqGXLSIbzTfapt9+K6gF1kTA=
 github.com/gobuffalo/genny/v2 v2.0.5/go.mod h1:kRkJuAw9mdI37AiEYjV4Dl+TgkBDYf8HZVjLkqe5eBg=
-github.com/gobuffalo/github_flavored_markdown v1.0.7 h1:Vjvz4wqOnviiLEfTh5bh270b3lhpJiwwQEWOWmHMwY8=
-github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI=
 github.com/gobuffalo/github_flavored_markdown v1.1.0 h1:8Zzj4fTRl/OP2R7sGerzSf6g2nEJnaBEJe7UAOiEvbQ=
 github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic=
-github.com/gobuffalo/gogen v0.2.0 h1:Xx7NCe+/y++eII2aWAFZ09/81MhDCsZwvMzIFJoQRnU=
-github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
-github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw=
-github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk=
-github.com/gobuffalo/helpers v0.4.0 h1:DR/iYihrVCXv1cYeIGSK3EZz2CljO+DqDLQPWZAod9c=
-github.com/gobuffalo/helpers v0.4.0/go.mod h1:2q/ZnVxCehM4/y1bNz3+wXsvWvWUY+iTUr7mPC6QqGQ=
-github.com/gobuffalo/helpers v0.5.0/go.mod h1:stpgxJ2C7T99NLyAxGUnYMM2zAtBk5NKQR0SIbd05j4=
 github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8=
 github.com/gobuffalo/helpers v0.6.1 h1:LLcL4BsiyDQYtMRUUpyFdBFvFXQ6hNYOpwrcYeilVWM=
 github.com/gobuffalo/helpers v0.6.1/go.mod h1:wInbDi0vTJKZBviURTLRMFLE4+nF2uRuuL2fnlYo7w4=
-github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
-github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
-github.com/gobuffalo/here v0.6.2 h1:ZtCqC7F9ou3moLbYfHM1Tj+gwHGgWhjyRjVjsir9BE0=
-github.com/gobuffalo/here v0.6.2/go.mod h1:D75Sq0p2BVHdgQu3vCRsXbg85rx943V19urJpqAVWjI=
-github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2 h1:8thhT+kUJMTMy3HlX4+y9Da+BNJck+p109tqqKp7WDs=
-github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
-github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
-github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
-github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
 github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc=
 github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM=
-github.com/gobuffalo/mapi v1.0.2 h1:fq9WcL1BYrm36SzK6+aAnZ8hcp+SrmnDyAxhNx8dvJk=
-github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
-github.com/gobuffalo/mapi v1.1.0/go.mod h1:pqQ1XAqvpy/JYtRwoieNps2yU8MFiMxBUpAm2FBtQ50=
-github.com/gobuffalo/nulls v0.1.0 h1:pR3SDzXyFcQrzyPreZj+OzNHSxI4DphSOFaQuidxrfw=
-github.com/gobuffalo/nulls v0.1.0/go.mod h1:/HRtuDRoVoN5fABk3J6jzZaGEdcIZEMs0qczj71eKZY=
+github.com/gobuffalo/nulls v0.2.0 h1:7R0Uec6JlZI02TR29zrs3KFIuUV8Sqe/s/j3yLvs+gc=
 github.com/gobuffalo/nulls v0.2.0/go.mod h1:w4q8RoSCEt87Q0K0sRIZWYeIxkxog5mh3eN3C/n+dUc=
-github.com/gobuffalo/nulls v0.4.0 h1:xi+JHGWIetYqLmS520dSWc8Ifj1P0aNXKTVDMVsPXmw=
-github.com/gobuffalo/nulls v0.4.0/go.mod h1:2KmsoLnMrxpwPLN5LmBbm6tmttHSIZr/v/OdGsATM3M=
-github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
-github.com/gobuffalo/packd v0.2.0/go.mod h1:k2CkHP3bjbqL2GwxwhxUy1DgnlbW644hkLC9iIUvZwY=
 github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
 github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
 github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM=
 github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
-github.com/gobuffalo/packr/v2 v2.4.0/go.mod h1:ra341gygw9/61nSjAbfwcwh8IrYL4WmR4IsPkPBhQiY=
-github.com/gobuffalo/packr/v2 v2.5.2/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08=
-github.com/gobuffalo/packr/v2 v2.5.3/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08=
-github.com/gobuffalo/packr/v2 v2.6.0 h1:EMUzJIb5rof6r087PtGmgdzdLKpRBESJ/8jyL9MexfY=
-github.com/gobuffalo/packr/v2 v2.6.0/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08=
-github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
-github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
 github.com/gobuffalo/packr/v2 v2.8.0 h1:IULGd15bQL59ijXLxEvA5wlMxsmx/ZkQv9T282zNVIY=
 github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g=
-github.com/gobuffalo/plush v3.8.2+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
-github.com/gobuffalo/plush v3.8.3+incompatible h1:kzvUTnFPhwyfPEsx7U7LI05/IIslZVGnAlMA1heWub8=
-github.com/gobuffalo/plush v3.8.3+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
 github.com/gobuffalo/plush/v4 v4.0.0 h1:ZHdmfr2R7DQ77XzWZK2PGKJOXm9NRy21EZ6Rw7FhuNw=
 github.com/gobuffalo/plush/v4 v4.0.0/go.mod h1:ErFS3UxKqEb8fpFJT7lYErfN/Nw6vHGiDMTjxpk5bQ0=
-github.com/gobuffalo/plushgen v0.1.2/go.mod h1:3U71v6HWZpVER1nInTXeAwdoRNsRd4W8aeIa1Lyp+Bk=
-github.com/gobuffalo/pop v4.12.2+incompatible h1:WFHMzzHbVLulZnEium1VlYRnWkzHz39FzVLov6rZdDI=
-github.com/gobuffalo/pop v4.12.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
-github.com/gobuffalo/pop v4.13.1+incompatible h1:AhbqPxNOBN/DBb2DBaiBqzOXIBQXxEYzngHHJ+ytP4g=
-github.com/gobuffalo/pop v4.13.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
-github.com/gobuffalo/pop/v5 v5.2.0/go.mod h1:Hj586Cr7FoTFNmvzyNdUcajv3r0A+W+bkil4RIX/zKo=
-github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5/VEHync2rJGi+epHNIeRSWjzGA+4=
-github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
-github.com/gobuffalo/tags v2.1.0+incompatible h1:qQjj3n2RtHxfooqXQ4/A9SsEfZ7/7guv8cp/GdAPa+Y=
-github.com/gobuffalo/tags v2.1.0+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
-github.com/gobuffalo/tags v2.1.6+incompatible h1:xaWOM48Xz8lBh+C8l5R7vSmLAZJK4KeWcLo+0pJ516g=
-github.com/gobuffalo/tags v2.1.6+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
-github.com/gobuffalo/tags v2.1.7+incompatible h1:GUxxh34f9SI4U0Pj3ZqvopO9SlzuqSf+g4ZGSPSszt4=
-github.com/gobuffalo/tags v2.1.7+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
+github.com/gobuffalo/pop/v5 v5.3.1 h1:dJbBPy6e0G0VRjn28md3fk16wpYIBv5iYVQWd0eqmkQ=
+github.com/gobuffalo/pop/v5 v5.3.1/go.mod h1:vcEDhh6cJ3WVENqJDFt/6z7zNb7lLnlN8vj3n5G9rYA=
 github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA=
 github.com/gobuffalo/tags/v3 v3.1.0 h1:mzdCYooN2VsLRr8KIAdEZ1lh1Py7JSMsiEGCGata2AQ=
 github.com/gobuffalo/tags/v3 v3.1.0/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA=
 github.com/gobuffalo/uuid v2.0.5+incompatible h1:c5uWRuEnYggYCrT9AJm0U2v1QTG7OVDAvxhj8tIV5Gc=
 github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
-github.com/gobuffalo/validate v2.0.3+incompatible h1:6f4JCEz11Zi6iIlexMv7Jz10RBPvgI795AOaubtCwTE=
-github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
-github.com/gobuffalo/validate v2.0.4+incompatible h1:ZTxozrIw8qQ5nfhShmc4izjYPTsPhfdXTdhXOd5OS9o=
-github.com/gobuffalo/validate v2.0.4+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
 github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0=
 github.com/gobuffalo/validate/v3 v3.1.0 h1:/QQN920PciCfBs3aywtJTvDTHmBFMKoiwkshUWa/HLQ=
 github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0=
-github.com/gobuffalo/validate/v3 v3.3.0 h1:j++FFx9gtjTmIQeI9xlaIDZ0nV4x8YQZz4RJAlZNUxg=
-github.com/gobuffalo/validate/v3 v3.3.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0=
 github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
-github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -214,8 +117,6 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-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/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -230,8 +131,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639 h1:VMd01CgpBpmLpuERyY4Oibn2PpcVS1fK9sjh5UZG8+o=
 github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -240,15 +139,14 @@ github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZb
 github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
 github.com/jackc/chunkreader/v2 v2.0.0 h1:DUwgMQuuPnS0rhMXenUtZpqZqrR/30NWY+qQvTpSvEs=
 github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
 github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
 github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
 github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
 github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
-github.com/jackc/pgconn v1.1.0 h1:10i6DMVJOSko/sD3FLpFKBHONzDGKkX8pbLyHC8B92o=
-github.com/jackc/pgconn v1.1.0/go.mod h1:GgY/Lbj1VonNaVdNUHs9AwWom3yP2eymFQ1C8z9r/Lk=
 github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
+github.com/jackc/pgconn v1.6.0 h1:8FiBxMxS/Z0eQ9BeE1HhL6pzPL1R5x+ZuQ+T86WgZ4I=
 github.com/jackc/pgconn v1.6.0/go.mod h1:yeseQo4xhQbgyJs2c87RAXOH2i624N0Fh1KSPJya7qo=
-github.com/jackc/pgconn v1.6.2/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
 github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
 github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
 github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
@@ -261,20 +159,22 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod
 github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
 github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
-github.com/jackc/pgproto3/v2 v2.0.0 h1:FApgMJ/GtaXfI0s8Lvd0kaLaRwMOhs4VH92pwkwQQvU=
-github.com/jackc/pgproto3/v2 v2.0.0/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
 github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8 h1:Q3tB+ExeflWUW7AFcAhXqk40s9mnNYLk1nOkKNZ5GnU=
 github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
-github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
 github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
 github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
 github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.3.0 h1:l8JvKrby3RI7Kg3bYEeU9TA4vqC38QDpFCfcrC7KuN0=
 github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik=
+github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
 github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
 github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.6.0 h1:Fh0O9GdlG4gYpjpwOqjdEodJUQM9jzN3Hdv7PN0xmm0=
 github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@@ -283,23 +183,17 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 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.0.0-20180716110948-e7c6d408fd5c h1:P2BrTyxPK1DQLq5+Y5LQ0BWli1oO+ebRi7J6ojFhykE=
-github.com/jonboulle/clockwork v0.0.0-20180716110948-e7c6d408fd5c/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
-github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
-github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
-github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
-github.com/karrick/godirwalk v1.15.3 h1:0a2pXOgtB16CqIqXTiT7+K9L73f74n/aNQUnH6Ortew=
 github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
-github.com/karrick/godirwalk v1.15.6 h1:Yf2mmR8TJy+8Fa0SuQVto5SYap6IF7lNVX4Jdl8G1qA=
-github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
+github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
+github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
-github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
-github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
 github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -308,11 +202,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
@@ -324,51 +217,37 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
-github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA=
+github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs=
 github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 github.com/magiconair/properties v1.8.0/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/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k=
-github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
 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/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
-github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
-github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
-github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
-github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
 github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
 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.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
-github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
 github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
-github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
-github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@@ -386,27 +265,16 @@ github.com/netlify/mailme v1.1.1/go.mod h1:8g03BJmU+ps7ma5vcH+t8aMtaicQTMX3ffP7R
 github.com/netlify/netlify-commons v0.32.0 h1:IgpqedBa6aFrc+daRgGZ+SmU9eBXlDXzKSAjevWmshM=
 github.com/netlify/netlify-commons v0.32.0/go.mod h1:xZH7auZrc/N/ZKS9BRO74yNf8i9LitXq1h6JVFZ2jTc=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
-github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
-github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/patrickmn/go-cache v0.0.0-20170418232947-7ac151875ffb/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-github.com/patrickmn/go-cache v1.0.0 h1:3gD5McaYs9CxjyK5AXGcq8gdeCARtd/9gJDUvVeaZ0Y=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 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/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -430,41 +298,28 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 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.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=
-github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w=
 github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.6.0 h1:IZRgg4sfrDH7nsAD1Y/Nwj+GzIfEwpJSLjCaNC3SbsI=
-github.com/rogpeppe/go-internal v1.6.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rs/cors v0.0.0-20170608165155-8dd4211afb5d h1:573lGU02rfWK16h656qmmul1zPul8WPPCDekyq+keVs=
-github.com/rs/cors v0.0.0-20170608165155-8dd4211afb5d/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 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=
 github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
-github.com/russellhaering/gosaml2 v0.3.1 h1:s+Oz2RRS83uqocWhWdR8Gbtze4g84cWQqNUm/GqYAs0=
-github.com/russellhaering/gosaml2 v0.3.1/go.mod h1:niieRtQaw+opTVp9jzZo1nAAoksI2eNpd+weDcjZ+Mk=
-github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 h1:J4AOUcOh/t1XbQcJfkEqhzgvMJ2tDxdCVvmHxW5QXao=
-github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM=
+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/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/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
 github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
-github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516 h1:ofR1ZdrNSkiWcMsRrubK9tb2/SlZVWttAfqUjJi6QYc=
-github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
-github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
 github.com/shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:DmcHeT/UuSDXaCVb8IijmL+fHX+FK9TLy98W7mfDXXg=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -474,8 +329,6 @@ github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
 github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
@@ -487,11 +340,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
 github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.4-0.20190321000552-67fc4837d267/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
 github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
-github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
@@ -510,8 +360,10 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
 github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
@@ -519,7 +371,6 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -532,36 +383,19 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
-golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
-golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6 h1:VsmCukA2gDdC3Mu6evOIT0QjLSQWiJIwzv1Bdj4jdzU=
-golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
-golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20161007143504-f4b625ec9b21/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
@@ -575,17 +409,9 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191108063844-7e6e90b9ea88 h1:Duq71GDCs3Cs8n6cRINv6SutZHHvYwzL28LBv73RiNU=
-golang.org/x/net v0.0.0-20191108063844-7e6e90b9ea88/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200219183655-46282727080f/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/oauth2 v0.0.0-20170807180024-9a379c6b3e95 h1:RS+wSrhdVci7CsPwJaMN8exaP3UTuQU0qB34R/E/JD0=
-golang.org/x/oauth2 v0.0.0-20170807180024-9a379c6b3e95/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
@@ -596,11 +422,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEha
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -610,25 +433,16 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII=
 golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
@@ -638,22 +452,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190613204242-ed0dc450797f/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18 h1:xFbv3LvlvQAmbNJFCBKRv1Ccvnh9FVsW0FX2kTWWowE=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 h1:2AmBLzhAfXj+2HCW09VCkJtHIYgHTIPcTeYqgP7Bwt0=
-golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6 h1:nULzSsKgihxFGLnQFv2T7lE5vIhOtg8ZPpJHapEt7o0=
-golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
@@ -666,13 +468,12 @@ google.golang.org/api v0.0.0-20170821230356-dd6bdadc5852 h1:8m8eq+cjxNeY7X4z1lAx
 google.golang.org/api v0.0.0-20170821230356-dd6bdadc5852/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/DataDog/dd-trace-go.v1 v1.12.1 h1:zkyLw+Uq6BvGwy5hFeLVI1ePgOkqJswFPL1uOx6SSA4=
 gopkg.in/DataDog/dd-trace-go.v1 v1.12.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
@@ -680,17 +481,14 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 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=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
@@ -699,11 +497,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/mailer/mailer.go b/mailer/mailer.go
index eea70f69..b8e42130 100644
--- a/mailer/mailer.go
+++ b/mailer/mailer.go
@@ -16,7 +16,6 @@ type Mailer interface {
 	InviteMail(user *models.User, referrerURL string) error
 	ConfirmationMail(user *models.User, referrerURL string) error
 	RecoveryMail(user *models.User, referrerURL string) error
-	MagicLinkMail(user *models.User, referrerURL string) error
 	EmailChangeMail(user *models.User, referrerURL string) error
 	ValidateEmail(email string) error
 }
@@ -66,7 +65,7 @@ func getSiteURL(referrerURL, siteURL, filepath, fragment string) (string, error)
 		}
 		site = site.ResolveReference(path)
 	}
-	site.RawQuery = fragment
+	site.Fragment = fragment
 	return site.String(), nil
 }
 
diff --git a/mailer/noop.go b/mailer/noop.go
index 77043200..f971625a 100644
--- a/mailer/noop.go
+++ b/mailer/noop.go
@@ -21,10 +21,6 @@ func (m noopMailer) RecoveryMail(user *models.User, referrerURL string) error {
 	return nil
 }
 
-func (m noopMailer) MagicLinkMail(user *models.User, referrerURL string) error {
-	return nil
-}
-
 func (m *noopMailer) EmailChangeMail(user *models.User, referrerURL string) error {
 	return nil
 }
diff --git a/mailer/template.go b/mailer/template.go
index 625692ba..88c02fdf 100644
--- a/mailer/template.go
+++ b/mailer/template.go
@@ -14,8 +14,6 @@ type TemplateMailer struct {
 	Mailer  *mailme.Mailer
 }
 
-var configFile = ""
-
 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>
@@ -31,11 +29,6 @@ const defaultRecoveryMail = `<h2>Reset password</h2>
 <p>Follow this link to reset the password for your user:</p>
 <p><a href="{{ .ConfirmationURL }}">Reset password</a></p>`
 
-const defaultMagicLinkMail = `<h2>Magic Link</h2>
-
-<p>Follow this link to login:</p>
-<p><a href="{{ .ConfirmationURL }}">Log In</a></p>`
-
 const defaultEmailChangeMail = `<h2>Confirm email address change</h2>
 
 <p>Follow this link to confirm the update of your email address from {{ .Email }} to {{ .NewEmail }}:</p>
@@ -49,9 +42,7 @@ func (m TemplateMailer) ValidateEmail(email string) error {
 
 // InviteMail sends a invite mail to a new user
 func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error {
-	globalConfig, err := conf.LoadGlobal(configFile)
-
-	url, err := getSiteURL(referrerURL, globalConfig.API.ExternalURL, m.Config.Mailer.URLPaths.Invite, "token="+user.ConfirmationToken+"&type=invite")
+	url, err := getSiteURL(referrerURL, m.Config.SiteURL, m.Config.Mailer.URLPaths.Invite, "invite_token="+user.ConfirmationToken)
 	if err != nil {
 		return err
 	}
@@ -66,7 +57,7 @@ func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error
 	return m.Mailer.Mail(
 		user.Email,
 		string(withDefault(m.Config.Mailer.Subjects.Invite, "You have been invited")),
-		m.Config.Mailer.Templates.Invite,
+		enforceRelativeURL(m.Config.Mailer.Templates.Invite),
 		defaultInviteMail,
 		data,
 	)
@@ -74,9 +65,7 @@ func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error
 
 // ConfirmationMail sends a signup confirmation mail to a new user
 func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string) error {
-	globalConfig, err := conf.LoadGlobal(configFile)
-
-	url, err := getSiteURL(referrerURL, globalConfig.API.ExternalURL, m.Config.Mailer.URLPaths.Confirmation, "token="+user.ConfirmationToken+"&type=signup")
+	url, err := getSiteURL(referrerURL, m.Config.SiteURL, m.Config.Mailer.URLPaths.Confirmation, "confirmation_token="+user.ConfirmationToken)
 	if err != nil {
 		return err
 	}
@@ -91,7 +80,7 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string)
 	return m.Mailer.Mail(
 		user.Email,
 		string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Your Signup")),
-		m.Config.Mailer.Templates.Confirmation,
+		enforceRelativeURL(m.Config.Mailer.Templates.Confirmation),
 		defaultConfirmationMail,
 		data,
 	)
@@ -115,7 +104,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string)
 	return m.Mailer.Mail(
 		user.EmailChange,
 		string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")),
-		m.Config.Mailer.Templates.EmailChange,
+		enforceRelativeURL(m.Config.Mailer.Templates.EmailChange),
 		defaultEmailChangeMail,
 		data,
 	)
@@ -123,9 +112,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string)
 
 // RecoveryMail sends a password recovery mail
 func (m *TemplateMailer) RecoveryMail(user *models.User, referrerURL string) error {
-	globalConfig, err := conf.LoadGlobal(configFile)
-
-	url, err := getSiteURL(referrerURL, globalConfig.API.ExternalURL, m.Config.Mailer.URLPaths.Recovery, "token="+user.RecoveryToken+"&type=recovery")
+	url, err := getSiteURL(referrerURL, m.Config.SiteURL, m.Config.Mailer.URLPaths.Recovery, "recovery_token="+user.RecoveryToken)
 	if err != nil {
 		return err
 	}
@@ -140,37 +127,12 @@ func (m *TemplateMailer) RecoveryMail(user *models.User, referrerURL string) err
 	return m.Mailer.Mail(
 		user.Email,
 		string(withDefault(m.Config.Mailer.Subjects.Recovery, "Reset Your Password")),
-		m.Config.Mailer.Templates.Recovery,
+		enforceRelativeURL(m.Config.Mailer.Templates.Recovery),
 		defaultRecoveryMail,
 		data,
 	)
 }
 
-// MagicLinkMail sends a login link mail
-func (m *TemplateMailer) MagicLinkMail(user *models.User, referrerURL string) error {
-	globalConfig, err := conf.LoadGlobal(configFile)
-
-	url, err := getSiteURL(referrerURL, globalConfig.API.ExternalURL, m.Config.Mailer.URLPaths.Recovery, "token="+user.RecoveryToken+"&type=magiclink")
-	if err != nil {
-		return err
-	}
-	data := map[string]interface{}{
-		"SiteURL":         m.Config.SiteURL,
-		"ConfirmationURL": url,
-		"Email":           user.Email,
-		"Token":           user.RecoveryToken,
-		"Data":            user.UserMetaData,
-	}
-
-	return m.Mailer.Mail(
-		user.Email,
-		string(withDefault(m.Config.Mailer.Subjects.MagicLink, "Your Magic Link")),
-		m.Config.Mailer.Templates.MagicLink,
-		defaultMagicLinkMail,
-		data,
-	)
-}
-
 // Send can be used to send one-off emails to users
 func (m TemplateMailer) Send(user *models.User, subject, body string, data map[string]interface{}) error {
 	return m.Mailer.Mail(
diff --git a/models/connection.go b/models/connection.go
index 0b5bd381..89a8fd5a 100644
--- a/models/connection.go
+++ b/models/connection.go
@@ -1,7 +1,7 @@
 package models
 
 import (
-	"github.com/gobuffalo/pop"
+	"github.com/gobuffalo/pop/v5"
 	"github.com/netlify/gotrue/storage"
 )
 
diff --git a/models/instance.go b/models/instance.go
index b13351cb..40b8ed2f 100644
--- a/models/instance.go
+++ b/models/instance.go
@@ -4,7 +4,7 @@ import (
 	"database/sql"
 	"time"
 
-	"github.com/gobuffalo/pop"
+	"github.com/gobuffalo/pop/v5"
 	"github.com/gobuffalo/uuid"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/storage"
diff --git a/models/refresh_token.go b/models/refresh_token.go
index eaa33d1c..8ffea99c 100644
--- a/models/refresh_token.go
+++ b/models/refresh_token.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/netlify/gotrue/storage/namespace"
 
-	"github.com/gobuffalo/pop"
+	"github.com/gobuffalo/pop/v5"
 	"github.com/gobuffalo/uuid"
 	"github.com/netlify/gotrue/crypto"
 	"github.com/netlify/gotrue/storage"
@@ -75,9 +75,5 @@ func createRefreshToken(tx *storage.Connection, user *User) (*RefreshToken, erro
 	if err := tx.Create(token); err != nil {
 		return nil, errors.Wrap(err, "error creating refresh token")
 	}
-
-	if err := user.UpdateLastSignInAt(tx); err != nil {
-		return nil, errors.Wrap(err, "error update user`s last_sign_in field")
-	}
 	return token, nil
 }
diff --git a/models/user.go b/models/user.go
index fce3986f..1fcd1073 100644
--- a/models/user.go
+++ b/models/user.go
@@ -5,7 +5,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gobuffalo/pop"
+	"github.com/gobuffalo/pop/v5"
 	"github.com/gobuffalo/uuid"
 	"github.com/netlify/gotrue/storage"
 	"github.com/netlify/gotrue/storage/namespace"
@@ -217,11 +217,6 @@ func (u *User) Confirm(tx *storage.Connection) error {
 	return tx.UpdateOnly(u, "confirmation_token", "confirmed_at")
 }
 
-// UpdateLastSignInAt update field last_sign_in_at for user according to specified field
-func (u *User) UpdateLastSignInAt(tx *storage.Connection) error {
-	return tx.UpdateOnly(u, "last_sign_in_at")
-}
-
 // ConfirmEmailChange confirm the change of email for a user
 func (u *User) ConfirmEmailChange(tx *storage.Connection) error {
 	u.Email = u.EmailChange
diff --git a/storage/dial.go b/storage/dial.go
index 4b8c9678..499df9e8 100644
--- a/storage/dial.go
+++ b/storage/dial.go
@@ -6,8 +6,8 @@ import (
 
 	_ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql"
 	_ "github.com/go-sql-driver/mysql"
-	"github.com/gobuffalo/pop"
-	"github.com/gobuffalo/pop/columns"
+	"github.com/gobuffalo/pop/v5"
+	"github.com/gobuffalo/pop/v5/columns"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/storage/namespace"
 	"github.com/pkg/errors"


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 00000000..8de12758
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,35 @@
+on:
+  push:
+    branches: [master]
+    tags: ['*']
+  pull_request:
+    types: [opened, synchronize, reopened]
+name: Test
+jobs:
+  test:
+    strategy:
+      matrix:
+        go-version: [1.13.x, 1.14.x]
+    runs-on: ubuntu-latest
+    services:
+      mysql:
+        image: mysql:5.7
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: yes
+          MYSQL_DATABASE: gotrue_test
+        ports:
+          - 3306:3306
+        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go-version }}
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Install dependencies
+        run: make deps
+      - name: Apply database migrations
+        run: make migrate_test
+      - name: Lint and test
+        run: make all
diff --git a/api/hook_test.go b/api/hook_test.go
index 646bbb1e..6f0130c6 100644
--- a/api/hook_test.go
+++ b/api/hook_test.go
@@ -55,7 +55,7 @@ func TestSignupHookSendInstanceID(t *testing.T) {
 		},
 	}
 
-	require.NoError(t, triggerHook(context.Background(), conn, SignupEvent, user, iid, config))
+	require.NoError(t, triggerEventHooks(context.Background(), conn, SignupEvent, user, iid, config))
 
 	assert.Equal(t, 1, callCount)
 }
@@ -98,11 +98,11 @@ func TestSignupHookFromClaims(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	ctx = withFunctionHooks(ctx, map[string]string{
-		"signup": svr.URL,
+	ctx = withFunctionHooks(ctx, map[string][]string{
+		"signup": []string{svr.URL},
 	})
 
-	require.NoError(t, triggerHook(ctx, conn, SignupEvent, user, iid, config))
+	require.NoError(t, triggerEventHooks(ctx, conn, SignupEvent, user, iid, config))
 
 	assert.Equal(t, 1, callCount)
 }
diff --git a/api/instance_test.go b/api/instance_test.go
index 7f8552cf..fcbec5ef 100644
--- a/api/instance_test.go
+++ b/api/instance_test.go
@@ -174,3 +174,79 @@ func (ts *InstanceTestSuite) TestUpdate_DisableEmail() {
 	require.NoError(ts.T(), err)
 	require.True(ts.T(), i.BaseConfig.External.Email.Disabled)
 }
+
+func (ts *InstanceTestSuite) TestUpdate_PreserveSMTPConfig() {
+	instanceID := uuid.Must(uuid.NewV4())
+	err := ts.API.db.Create(&models.Instance{
+		ID:   instanceID,
+		UUID: testUUID,
+		BaseConfig: &conf.Configuration{
+			SMTP: conf.SMTPConfiguration{
+				Host: "foo.com",
+				User: "Admin",
+				Pass: "password123",
+			},
+		},
+	})
+	require.NoError(ts.T(), err)
+
+	var buffer bytes.Buffer
+	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+		"config": &conf.Configuration{
+			Mailer: conf.MailerConfiguration{
+				Subjects:  conf.EmailContentConfiguration{Invite: "foo"},
+				Templates: conf.EmailContentConfiguration{Invite: "bar"},
+			},
+		},
+	}))
+
+	req := httptest.NewRequest(http.MethodPut, "/instances/"+instanceID.String(), &buffer)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Authorization", "Bearer "+operatorToken)
+
+	w := httptest.NewRecorder()
+	ts.API.handler.ServeHTTP(w, req)
+	require.Equal(ts.T(), w.Code, http.StatusOK)
+
+	i, err := models.GetInstanceByUUID(ts.API.db, testUUID)
+	require.NoError(ts.T(), err)
+	require.Equal(ts.T(), "password123", i.BaseConfig.SMTP.Pass)
+}
+
+func (ts *InstanceTestSuite) TestUpdate_ClearPassword() {
+	instanceID := uuid.Must(uuid.NewV4())
+	err := ts.API.db.Create(&models.Instance{
+		ID:   instanceID,
+		UUID: testUUID,
+		BaseConfig: &conf.Configuration{
+			SMTP: conf.SMTPConfiguration{
+				Host: "foo.com",
+				User: "Admin",
+				Pass: "password123",
+			},
+		},
+	})
+	require.NoError(ts.T(), err)
+
+	var buffer bytes.Buffer
+	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+		"config": map[string]interface{}{
+			"smtp": map[string]interface{}{
+				"pass": "",
+			},
+		},
+	}))
+	ts.T().Log(buffer.String())
+
+	req := httptest.NewRequest(http.MethodPut, "/instances/"+instanceID.String(), &buffer)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Authorization", "Bearer "+operatorToken)
+
+	w := httptest.NewRecorder()
+	ts.API.handler.ServeHTTP(w, req)
+	require.Equal(ts.T(), w.Code, http.StatusOK)
+
+	i, err := models.GetInstanceByUUID(ts.API.db, testUUID)
+	require.NoError(ts.T(), err)
+	require.Equal(ts.T(), "", i.BaseConfig.SMTP.Pass)
+}
diff --git a/api/middleware_test.go b/api/middleware_test.go
new file mode 100644
index 00000000..28552d51
--- /dev/null
+++ b/api/middleware_test.go
@@ -0,0 +1,31 @@
+package api
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFunctionHooksUnmarshalJSON(t *testing.T) {
+	tests := []struct {
+		in string
+		ok bool
+	}{
+		{`{ "signup" : "identity-signup" }`, true},
+		{`{ "signup" : ["identity-signup"] }`, true},
+		{`{ "signup" : {"foo" : "bar"} }`, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.in, func(t *testing.T) {
+			var f FunctionHooks
+			err := json.Unmarshal([]byte(tt.in), &f)
+			if tt.ok {
+				assert.NoError(t, err)
+				assert.Equal(t, FunctionHooks{"signup": {"identity-signup"}}, f)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
diff --git a/api/tracer_test.go b/api/tracer_test.go
new file mode 100644
index 00000000..18f0385b
--- /dev/null
+++ b/api/tracer_test.go
@@ -0,0 +1,63 @@
+package api
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/gobuffalo/uuid"
+	"github.com/netlify/gotrue/conf"
+	"github.com/opentracing/opentracing-go"
+	"github.com/opentracing/opentracing-go/mocktracer"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+)
+
+type TracerTestSuite struct {
+	suite.Suite
+	API    *API
+	Config *conf.Configuration
+
+	instanceID uuid.UUID
+}
+
+func TestTracer(t *testing.T) {
+	api, config, instanceID, err := setupAPIForTestForInstance()
+	require.NoError(t, err)
+
+	ts := &TracerTestSuite{
+		API:        api,
+		Config:     config,
+		instanceID: instanceID,
+	}
+	defer api.db.Close()
+
+	suite.Run(t, ts)
+}
+
+func (ts *TracerTestSuite) TestTracer_Spans() {
+	mt := mocktracer.New()
+	opentracing.SetGlobalTracer(mt)
+
+	w := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodPost, "http://localhost/something1", nil)
+	ts.API.handler.ServeHTTP(w, req)
+	req = httptest.NewRequest(http.MethodGet, "http://localhost/something2", nil)
+	ts.API.handler.ServeHTTP(w, req)
+
+	spans := mt.FinishedSpans()
+	if assert.Equal(ts.T(), 2, len(spans)) {
+		assert.Equal(ts.T(), "POST", spans[0].Tag("http.method"))
+		assert.Equal(ts.T(), "/something1", spans[0].Tag("http.url"))
+		assert.Equal(ts.T(), "POST /something1", spans[0].Tag("resource.name"))
+		assert.Equal(ts.T(), "404", spans[0].Tag("http.status_code"))
+		assert.NotEmpty(ts.T(), spans[0].Tag("http.request_id"))
+
+		assert.Equal(ts.T(), "GET", spans[1].Tag("http.method"))
+		assert.Equal(ts.T(), "/something2", spans[1].Tag("http.url"))
+		assert.Equal(ts.T(), "GET /something2", spans[1].Tag("resource.name"))
+		assert.Equal(ts.T(), "404", spans[1].Tag("http.status_code"))
+		assert.NotEmpty(ts.T(), spans[1].Tag("http.request_id"))
+	}
+}
diff --git a/api/verify_test.go b/api/verify_test.go
index bb60358d..16e6aaad 100644
--- a/api/verify_test.go
+++ b/api/verify_test.go
@@ -8,9 +8,9 @@ import (
 	"testing"
 	"time"
 
-	"github.com/gobuffalo/uuid"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/models"
+	"github.com/gobuffalo/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
@@ -92,57 +92,3 @@ func (ts *VerifyTestSuite) TestVerify_PasswordRecovery() {
 	require.NoError(ts.T(), err)
 	assert.True(ts.T(), u.IsConfirmed())
 }
-
-func (ts *VerifyTestSuite) TestExpiredConfirmationToken() {
-	u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud)
-	require.NoError(ts.T(), err)
-	u.ConfirmationToken = "asdf3"
-	sentTime := time.Now().Add(-48 * time.Hour)
-	u.ConfirmationSentAt = &sentTime
-	require.NoError(ts.T(), ts.API.db.Update(u))
-
-	// Request body
-	var buffer bytes.Buffer
-	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
-		"type":  signupVerification,
-		"token": u.ConfirmationToken,
-	}))
-
-	// Setup request
-	req := httptest.NewRequest(http.MethodPost, "http://localhost/verify", &buffer)
-	req.Header.Set("Content-Type", "application/json")
-
-	// Setup response recorder
-	w := httptest.NewRecorder()
-
-	ts.API.handler.ServeHTTP(w, req)
-
-	assert.Equal(ts.T(), http.StatusGone, w.Code, w.Body.String())
-}
-
-func (ts *VerifyTestSuite) TestExpiredRecoveryToken() {
-	u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud)
-	require.NoError(ts.T(), err)
-	u.RecoveryToken = "asdf3"
-	sentTime := time.Now().Add(-48 * time.Hour)
-	u.RecoverySentAt = &sentTime
-	require.NoError(ts.T(), ts.API.db.Update(u))
-
-	// Request body
-	var buffer bytes.Buffer
-	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
-		"type":  recoveryVerification,
-		"token": u.RecoveryToken,
-	}))
-
-	// Setup request
-	req := httptest.NewRequest(http.MethodPost, "http://localhost/verify", &buffer)
-	req.Header.Set("Content-Type", "application/json")
-
-	// Setup response recorder
-	w := httptest.NewRecorder()
-
-	ts.API.handler.ServeHTTP(w, req)
-
-	assert.Equal(ts.T(), http.StatusGone, w.Code, w.Body.String())
-}
diff --git a/conf/configuration_test.go b/conf/configuration_test.go
index 9c76f03f..dae60605 100644
--- a/conf/configuration_test.go
+++ b/conf/configuration_test.go
@@ -4,6 +4,7 @@ import (
 	"os"
 	"testing"
 
+	"github.com/opentracing/opentracing-go"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -23,3 +24,23 @@ func TestGlobal(t *testing.T) {
 	require.NotNil(t, gc)
 	assert.Equal(t, "X-Request-ID", gc.API.RequestIDHeader)
 }
+
+func TestTracing(t *testing.T) {
+	os.Setenv("GOTRUE_DB_DRIVER", "mysql")
+	os.Setenv("GOTRUE_DB_DATABASE_URL", "fake")
+	os.Setenv("GOTRUE_OPERATOR_TOKEN", "token")
+	os.Setenv("GOTRUE_TRACING_SERVICE_NAME", "identity")
+	os.Setenv("GOTRUE_TRACING_PORT", "8126")
+	os.Setenv("GOTRUE_TRACING_HOST", "127.0.0.1")
+	os.Setenv("GOTRUE_TRACING_TAGS", "tag1:value1,tag2:value2")
+
+	gc, _ := LoadGlobal("")
+	tc := opentracing.GlobalTracer()
+
+	assert.Equal(t, opentracing.NoopTracer{}, tc)
+	assert.Equal(t, false, gc.Tracing.Enabled)
+	assert.Equal(t, "identity", gc.Tracing.ServiceName)
+	assert.Equal(t, "8126", gc.Tracing.Port)
+	assert.Equal(t, "127.0.0.1", gc.Tracing.Host)
+	assert.Equal(t, map[string]string{"tag1": "value1", "tag2": "value2"}, gc.Tracing.Tags)
+}
diff --git a/hack/test.env b/hack/test.env
index 2cafd702..f6e5a45a 100644
--- a/hack/test.env
+++ b/hack/test.env
@@ -30,3 +30,7 @@ GOTRUE_EXTERNAL_SAML_ENABLED=true
 GOTRUE_EXTERNAL_SAML_METADATA_URL=
 GOTRUE_EXTERNAL_SAML_API_BASE=http://localhost
 GOTRUE_EXTERNAL_SAML_NAME=TestSamlName
+GOTRUE_TRACING_ENABLED=false
+GOTRUE_TRACING_HOST=127.0.0.1
+GOTRUE_TRACING_PORT=8126
+GOTRUE_TRACING_TAGS="env:test"
diff --git a/models/db_test.go b/models/db_test.go
index f1ccf7fd..af9bd04e 100644
--- a/models/db_test.go
+++ b/models/db_test.go
@@ -3,7 +3,7 @@ package models_test
 import (
 	"testing"
 
-	"github.com/gobuffalo/pop"
+	"github.com/gobuffalo/pop/v5"
 	"github.com/netlify/gotrue/models"
 	"github.com/stretchr/testify/assert"
 )