diff --git a/author/author.go b/author/author.go
index e2e9508..0114905 100644
--- a/author/author.go
+++ b/author/author.go
@@ -1,128 +1,128 @@
 /*
- * Copyright © 2018 A Bunch Tell LLC.
+ * Copyright © 2018-2020 A Bunch Tell LLC.
  *
  * This file is part of WriteFreely.
  *
  * WriteFreely is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License, included
  * in the LICENSE file in this source code package.
  */
 
 package author
 
 import (
 	"github.com/writeas/writefreely/config"
 	"os"
 	"path/filepath"
 	"regexp"
 )
 
 // Regex pattern for valid usernames
 var validUsernameReg = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]*$")
 
 // List of reserved usernames
 var reservedUsernames = map[string]bool{
 	"a":                true,
 	"about":            true,
 	"add":              true,
 	"admin":            true,
 	"administrator":    true,
 	"adminzone":        true,
 	"api":              true,
 	"article":          true,
 	"articles":         true,
 	"auth":             true,
 	"authenticate":     true,
 	"browse":           true,
 	"c":                true,
 	"categories":       true,
 	"category":         true,
 	"changes":          true,
 	"community":        true,
 	"create":           true,
 	"css":              true,
 	"data":             true,
 	"dev":              true,
 	"developers":       true,
 	"draft":            true,
 	"drafts":           true,
 	"edit":             true,
 	"edits":            true,
 	"faq":              true,
 	"feed":             true,
 	"feedback":         true,
 	"guide":            true,
 	"guides":           true,
 	"help":             true,
 	"index":            true,
 	"invite":           true,
 	"js":               true,
 	"login":            true,
 	"logout":           true,
 	"me":               true,
 	"media":            true,
 	"meta":             true,
 	"metadata":         true,
 	"new":              true,
 	"news":             true,
 	"oauth":            true,
 	"post":             true,
 	"posts":            true,
 	"privacy":          true,
 	"publication":      true,
 	"publications":     true,
 	"publish":          true,
 	"random":           true,
 	"read":             true,
 	"reader":           true,
 	"register":         true,
 	"remove":           true,
 	"signin":           true,
 	"signout":          true,
 	"signup":           true,
 	"start":            true,
 	"status":           true,
 	"summary":          true,
 	"support":          true,
 	"tag":              true,
 	"tags":             true,
 	"team":             true,
 	"template":         true,
 	"templates":        true,
 	"terms":            true,
 	"terms-of-service": true,
 	"termsofservice":   true,
 	"theme":            true,
 	"themes":           true,
 	"tips":             true,
 	"tos":              true,
 	"update":           true,
 	"updates":          true,
 	"user":             true,
 	"users":            true,
 	"yourname":         true,
 }
 
 // IsValidUsername returns true if a given username is neither reserved nor
 // of the correct format.
 func IsValidUsername(cfg *config.Config, username string) bool {
 	// Username has to be above a character limit
 	if len(username) < cfg.App.MinUsernameLen {
 		return false
 	}
 	// Username is invalid if page with the same name exists. So traverse
 	// available pages, adding them to reservedUsernames map that'll be checked
 	// later.
 	filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error {
 		reservedUsernames[i.Name()] = true
 		return nil
 	})
 
 	// Username is invalid if it is reserved!
 	if _, reserved := reservedUsernames[username]; reserved {
 		return false
 	}
 
 	// TODO: use correct regexp function here
 	return len(validUsernameReg.FindStringSubmatch(username)) > 0
 }
diff --git a/oauth_signup.go b/oauth_signup.go
index 10d2306..220afbd 100644
--- a/oauth_signup.go
+++ b/oauth_signup.go
@@ -1,208 +1,218 @@
+/*
+ * Copyright © 2020 A Bunch Tell LLC.
+ *
+ * This file is part of WriteFreely.
+ *
+ * WriteFreely is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, included
+ * in the LICENSE file in this source code package.
+ */
+
 package writefreely
 
 import (
 	"crypto/sha256"
 	"encoding/hex"
 	"fmt"
 	"github.com/writeas/impart"
 	"github.com/writeas/web-core/auth"
 	"github.com/writeas/web-core/log"
 	"github.com/writeas/writefreely/page"
 	"html/template"
 	"net/http"
 	"strings"
 	"time"
 )
 
 type viewOauthSignupVars struct {
 	page.StaticPage
 	To      string
 	Message template.HTML
 	Flashes []template.HTML
 
 	AccessToken     string
 	TokenUsername   string
 	TokenAlias      string // TODO: rename this to match the data it represents: the collection title
 	TokenEmail      string
 	TokenRemoteUser string
 	Provider        string
 	ClientID        string
 	TokenHash       string
 
 	LoginUsername string
 	Alias         string // TODO: rename this to match the data it represents: the collection title
 	Email         string
 }
 
 const (
 	oauthParamAccessToken       = "access_token"
 	oauthParamTokenUsername     = "token_username"
 	oauthParamTokenAlias        = "token_alias"
 	oauthParamTokenEmail        = "token_email"
 	oauthParamTokenRemoteUserID = "token_remote_user"
 	oauthParamClientID          = "client_id"
 	oauthParamProvider          = "provider"
 	oauthParamHash              = "signature"
 	oauthParamUsername          = "username"
 	oauthParamAlias             = "alias"
 	oauthParamEmail             = "email"
 	oauthParamPassword          = "password"
 )
 
 type oauthSignupPageParams struct {
 	AccessToken     string
 	TokenUsername   string
 	TokenAlias      string // TODO: rename this to match the data it represents: the collection title
 	TokenEmail      string
 	TokenRemoteUser string
 	ClientID        string
 	Provider        string
 	TokenHash       string
 }
 
 func (p oauthSignupPageParams) HashTokenParams(key string) string {
 	hasher := sha256.New()
 	hasher.Write([]byte(key))
 	hasher.Write([]byte(p.AccessToken))
 	hasher.Write([]byte(p.TokenUsername))
 	hasher.Write([]byte(p.TokenAlias))
 	hasher.Write([]byte(p.TokenEmail))
 	hasher.Write([]byte(p.TokenRemoteUser))
 	hasher.Write([]byte(p.ClientID))
 	hasher.Write([]byte(p.Provider))
 	return hex.EncodeToString(hasher.Sum(nil))
 }
 
 func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.Request) error {
 	tp := &oauthSignupPageParams{
 		AccessToken:     r.FormValue(oauthParamAccessToken),
 		TokenUsername:   r.FormValue(oauthParamTokenUsername),
 		TokenAlias:      r.FormValue(oauthParamTokenAlias),
 		TokenEmail:      r.FormValue(oauthParamTokenEmail),
 		TokenRemoteUser: r.FormValue(oauthParamTokenRemoteUserID),
 		ClientID:        r.FormValue(oauthParamClientID),
 		Provider:        r.FormValue(oauthParamProvider),
 	}
 	if tp.HashTokenParams(h.Config.Server.HashSeed) != r.FormValue(oauthParamHash) {
 		return impart.HTTPError{Status: http.StatusBadRequest, Message: "Request has been tampered with."}
 	}
 	tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed)
 	if err := h.validateOauthSignup(r); err != nil {
 		return h.showOauthSignupPage(app, w, r, tp, err)
 	}
 
 	var err error
 	hashedPass := []byte{}
 	clearPass := r.FormValue(oauthParamPassword)
 	hasPass := clearPass != ""
 	if hasPass {
 		hashedPass, err = auth.HashPass([]byte(clearPass))
 		if err != nil {
 			return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password"))
 		}
 	}
 	newUser := &User{
 		Username:   r.FormValue(oauthParamUsername),
 		HashedPass: hashedPass,
 		HasPass:    hasPass,
 		Email:      prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey),
 		Created:    time.Now().Truncate(time.Second).UTC(),
 	}
 	displayName := r.FormValue(oauthParamAlias)
 	if len(displayName) == 0 {
 		displayName = r.FormValue(oauthParamUsername)
 	}
 
 	err = h.DB.CreateUser(h.Config, newUser, displayName)
 	if err != nil {
 		return h.showOauthSignupPage(app, w, r, tp, err)
 	}
 
 	err = h.DB.RecordRemoteUserID(r.Context(), newUser.ID, r.FormValue(oauthParamTokenRemoteUserID), r.FormValue(oauthParamProvider), r.FormValue(oauthParamClientID), r.FormValue(oauthParamAccessToken))
 	if err != nil {
 		return h.showOauthSignupPage(app, w, r, tp, err)
 	}
 
 	if err := loginOrFail(h.Store, w, r, newUser); err != nil {
 		return h.showOauthSignupPage(app, w, r, tp, err)
 	}
 	return nil
 }
 
 func (h oauthHandler) validateOauthSignup(r *http.Request) error {
 	username := r.FormValue(oauthParamUsername)
 	if len(username) < h.Config.App.MinUsernameLen {
 		return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too short."}
 	}
 	if len(username) > 100 {
 		return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."}
 	}
 	collTitle := r.FormValue(oauthParamAlias)
 	if len(collTitle) == 0 {
 		collTitle = username
 	}
 	email := r.FormValue(oauthParamEmail)
 	if len(email) > 0 {
 		parts := strings.Split(email, "@")
 		if len(parts) != 2 || (len(parts[0]) < 1 || len(parts[1]) < 1) {
 			return impart.HTTPError{Status: http.StatusBadRequest, Message: "Invalid email address"}
 		}
 	}
 	return nil
 }
 
 func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error {
 	username := tp.TokenUsername
 	collTitle := tp.TokenAlias
 	email := tp.TokenEmail
 
 	session, err := app.sessionStore.Get(r, cookieName)
 	if err != nil {
 		// Ignore this
 		log.Error("Unable to get session; ignoring: %v", err)
 	}
 
 	if tmpValue := r.FormValue(oauthParamUsername); len(tmpValue) > 0 {
 		username = tmpValue
 	}
 	if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 {
 		collTitle = tmpValue
 	}
 	if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 {
 		email = tmpValue
 	}
 
 	p := &viewOauthSignupVars{
 		StaticPage: pageForReq(app, r),
 		To:         r.FormValue("to"),
 		Flashes:    []template.HTML{},
 
 		AccessToken:     tp.AccessToken,
 		TokenUsername:   tp.TokenUsername,
 		TokenAlias:      tp.TokenAlias,
 		TokenEmail:      tp.TokenEmail,
 		TokenRemoteUser: tp.TokenRemoteUser,
 		Provider:        tp.Provider,
 		ClientID:        tp.ClientID,
 		TokenHash:       tp.TokenHash,
 
 		LoginUsername: username,
 		Alias:         collTitle,
 		Email:         email,
 	}
 
 	// Display any error messages
 	flashes, _ := getSessionFlashes(app, w, r, session)
 	for _, flash := range flashes {
 		p.Flashes = append(p.Flashes, template.HTML(flash))
 	}
 	if errMsg != nil {
 		p.Flashes = append(p.Flashes, template.HTML(errMsg.Error()))
 	}
 	err = pages["signup-oauth.tmpl"].ExecuteTemplate(w, "base", p)
 	if err != nil {
 		log.Error("Unable to render signup-oauth: %v", err)
 		return err
 	}
 	return nil
 }
diff --git a/oauth_slack.go b/oauth_slack.go
index f700c2c..1db3613 100644
--- a/oauth_slack.go
+++ b/oauth_slack.go
@@ -1,170 +1,180 @@
+/*
+ * Copyright © 2020 A Bunch Tell LLC.
+ *
+ * This file is part of WriteFreely.
+ *
+ * WriteFreely is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, included
+ * in the LICENSE file in this source code package.
+ */
+
 package writefreely
 
 import (
 	"context"
 	"errors"
 	"fmt"
 	"github.com/writeas/nerds/store"
 	"github.com/writeas/slug"
 	"net/http"
 	"net/url"
 	"strings"
 )
 
 type slackOauthClient struct {
 	ClientID         string
 	ClientSecret     string
 	TeamID           string
 	CallbackLocation string
 	HttpClient       HttpClient
 }
 
 type slackExchangeResponse struct {
 	OK          bool   `json:"ok"`
 	AccessToken string `json:"access_token"`
 	Scope       string `json:"scope"`
 	TeamName    string `json:"team_name"`
 	TeamID      string `json:"team_id"`
 	Error       string `json:"error"`
 }
 
 type slackIdentity struct {
 	Name  string `json:"name"`
 	ID    string `json:"id"`
 	Email string `json:"email"`
 }
 
 type slackTeam struct {
 	Name string `json:"name"`
 	ID   string `json:"id"`
 }
 
 type slackUserIdentityResponse struct {
 	OK    bool          `json:"ok"`
 	User  slackIdentity `json:"user"`
 	Team  slackTeam     `json:"team"`
 	Error string        `json:"error"`
 }
 
 const (
 	slackAuthLocation     = "https://slack.com/oauth/authorize"
 	slackExchangeLocation = "https://slack.com/api/oauth.access"
 	slackIdentityLocation = "https://slack.com/api/users.identity"
 )
 
 var _ oauthClient = slackOauthClient{}
 
 func (c slackOauthClient) GetProvider() string {
 	return "slack"
 }
 
 func (c slackOauthClient) GetClientID() string {
 	return c.ClientID
 }
 
 func (c slackOauthClient) GetCallbackLocation() string {
 	return c.CallbackLocation
 }
 
 func (c slackOauthClient) buildLoginURL(state string) (string, error) {
 	u, err := url.Parse(slackAuthLocation)
 	if err != nil {
 		return "", err
 	}
 	q := u.Query()
 	q.Set("client_id", c.ClientID)
 	q.Set("scope", "identity.basic identity.email identity.team")
 	q.Set("redirect_uri", c.CallbackLocation)
 	q.Set("state", state)
 
 	// If this param is not set, the user can select which team they
 	// authenticate through and then we'd have to match the configured team
 	// against the profile get. That is extra work in the post-auth phase
 	// that we don't want to do.
 	q.Set("team", c.TeamID)
 
 	// The Slack OAuth docs don't explicitly list this one, but it is part of
 	// the spec, so we include it anyway.
 	q.Set("response_type", "code")
 	u.RawQuery = q.Encode()
 	return u.String(), nil
 }
 
 func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
 	form := url.Values{}
 	// The oauth.access documentation doesn't explicitly mention this
 	// parameter, but it is part of the spec, so we include it anyway.
 	// https://api.slack.com/methods/oauth.access
 	form.Add("grant_type", "authorization_code")
 	form.Add("redirect_uri", c.CallbackLocation)
 	form.Add("code", code)
 	req, err := http.NewRequest("POST", slackExchangeLocation, strings.NewReader(form.Encode()))
 	if err != nil {
 		return nil, err
 	}
 	req.WithContext(ctx)
 	req.Header.Set("User-Agent", "writefreely")
 	req.Header.Set("Accept", "application/json")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.SetBasicAuth(c.ClientID, c.ClientSecret)
 
 	resp, err := c.HttpClient.Do(req)
 	if err != nil {
 		return nil, err
 	}
 	if resp.StatusCode != http.StatusOK {
 		return nil, errors.New("unable to exchange code for access token")
 	}
 
 	var tokenResponse slackExchangeResponse
 	if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
 		return nil, err
 	}
 	if !tokenResponse.OK {
 		return nil, errors.New(tokenResponse.Error)
 	}
 	return tokenResponse.TokenResponse(), nil
 }
 
 func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
 	req, err := http.NewRequest("GET", slackIdentityLocation, nil)
 	if err != nil {
 		return nil, err
 	}
 	req.WithContext(ctx)
 	req.Header.Set("User-Agent", "writefreely")
 	req.Header.Set("Accept", "application/json")
 	req.Header.Set("Authorization", "Bearer "+accessToken)
 
 	resp, err := c.HttpClient.Do(req)
 	if err != nil {
 		return nil, err
 	}
 	if resp.StatusCode != http.StatusOK {
 		return nil, errors.New("unable to inspect access token")
 	}
 
 	var inspectResponse slackUserIdentityResponse
 	if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
 		return nil, err
 	}
 	if !inspectResponse.OK {
 		return nil, errors.New(inspectResponse.Error)
 	}
 	return inspectResponse.InspectResponse(), nil
 }
 
 func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
 	return &InspectResponse{
 		UserID:      resp.User.ID,
 		Username:    fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.GenerateRandomString("0123456789bcdfghjklmnpqrstvwxyz", 5)),
 		DisplayName: resp.User.Name,
 		Email:       resp.User.Email,
 	}
 }
 
 func (resp slackExchangeResponse) TokenResponse() *TokenResponse {
 	return &TokenResponse{
 		AccessToken: resp.AccessToken,
 	}
 }