Authentication as we know it leaves a lot to be desired on the web. We've come to rely on our browsers and third party applications to manage our ever mounting collection of identities. Standards such as OAuth help us consolidate these credentials–but in many ways they serve more as a form of misdirection. Do you really understand the implications of the permissions you're granting when walking the painful OAuth dance? Are you sharing too much? Very few authentication options leave you with that warm and tingly sense of security. However, there's been some intriguing projects recently that aim to do away with the authentication headache–or at least the password portion of it. In this post we'll explore how to implement a passwordless authentication flow using Go to make your next web project's authentication process slightly less unpleasant for everyone.

Your Password Is Now A Token

The idea behind passwordless authentication isn't exactly a new one. As you examine the following steps you might be reminded of account verification flows. Sign up for a new account, and you get sent an email with a link containing a unique identifier and a token. To paint a better picture, here's the typical steps involved in the email flow we'll be implementing:

  1. Registered user visits login page either directly or via a redirect from trying to access restricted content.
  2. User enters email on login page and hits submit.
  3. If given email exists in the database, generate a new token and token expiration.
  4. Send user email with a link to your authentication endpoint containing their unique identifier and token.
  5. Upon returning to the site, validate token and identifier belong to the user and that the token hasn't expired.
  6. Authenticate user by storing necessary information in their session.

Though we won't be covering it here, you could extend this type of authentication to use other delivery methods such as text messages. In that particular case, user's would submit their phone number during authentication and get sent some sort of unique 4-6 digit key. The authentication page would require they re-enter their phone along with the key.

The User

We'll define a relatively bare bones user type, and to keep things simple it'll also house a few utility methods for managing and validating the token and expiration.

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"github.com/guregu/null"
	"io"
	"time"
)

const (
	TokenLength int           = 32
	TtlDuration time.Duration = 20 * time.Minute
)

type User struct {
	Id        int64       `db:"id"`
	Email     string      `db:"email"`
	Token     string      `db:"token"`
	Ttl       time.Time   `db:"ttl"`
	OriginUrl null.String `db:"originurl"`
}

// RefreshToken refreshes Ttl and Token for the User.
func (u *User) RefreshToken() error {
	token := make([]byte, TokenLength)
	if _, err := io.ReadFull(rand.Reader, token); err != nil {
		return err
	}
	u.Token = base64.URLEncoding.EncodeToString(token)
	u.Ttl = time.Now().UTC().Add(TtlDuration)
	return nil
}

// IsValidToken returns a bool indicating that the User's current token hasn't
// expired and that the provided token is valid.
func (u *User) IsValidToken(token string) bool {
	if u.Ttl.Before(time.Now().UTC()) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(u.Token), []byte(token)) == 1
}

You might have noticed the db tags alongside the user fields, which is used by gorp–a very minimal ORM for Go–to identify the database column names each field maps to.

Auth Necessities

Before piecing together the routes and handlers, we need some basic auth middleware. I'm going to omit the details of the various auth utility code seen throughout the following samples for the sake of brevity, but you can explore them all in the project's source on github. One thing I absolutely love about the Go community is its one of tool belts and libraries, not frameworks. Frameworks are great when it comes to getting things done quickly, but they almost always come with some steep technical debt. There exists a variety of small, well documented, and feature rich libraries that can easily be combined for building web applications. My library of choice (and much of the community's for that matter) when it comes to middleware is negroni, and we'll be adhering to the middleware interface it requires for these two auth middleware types:

// UserMiddleware checks for the User in the session and adds them to the request context if they exist.
func UserMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	s := GetSession(r)
	if id, ok := s.Values[sessionUser]; ok {
		if user, err := dbmap.Get(User{}, id.(int64)); err == nil {
			SetContextUser(user.(*User), r)
		}
	}
	next(w, r)
}

// LoginRequiredMiddleware ensures a User is logged in, otherwise redirects them to the login page.
func LoginRequiredMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	if !IsLoggedIn(r) {
		http.Redirect(w, r, "/", http.StatusFound)
		return
	}
	next(w, r)
}

The order of middleware is important, so pay attention when we apply it later. Routes that require the user to be logged in rely on UserMiddleware to have populated the request context with the User value.

Routing, Gorilla Style

The gorilla toolkit includes libraries for a variety of common web-related tasks, including routing. Though the aforementioned auth utility code has been left out of this post, if you check out the full project source you'll see gorilla's context and session packages in action too. For this simple app, there's a login, login success, logout, verification, and profile page. The profile page will require a logged in user. Here's a quick look at the combined gorilla routes and negroni middleware:

import (
	"github.com/codegangsta/negroni"
	"github.com/gorilla/mux"
	"net/http"
)

// BuildRoutes returns the routes for the application.
func BuildRoutes() http.Handler {
	router := mux.NewRouter()

	router.HandleFunc("/", HomeHandler)
	router.HandleFunc("/login-success", LoginSuccessHandler)
	router.HandleFunc("/verify", VerifyHandler)
	router.HandleFunc("/logout", LogoutHandler)

	// profile routes with LoginRequiredMiddleware
	profileRouter := mux.NewRouter()
	profileRouter.HandleFunc("/profile", ProfileHandler)

	router.PathPrefix("/profile").Handler(negroni.New(
		negroni.HandlerFunc(LoginRequiredMiddleware),
		negroni.Wrap(profileRouter),
	))

	// apply the base middleware to the main router
	n := negroni.New(
		negroni.HandlerFunc(CsrfMiddleware),
		negroni.HandlerFunc(UserMiddleware),
	)
	n.UseHandler(router)

	return n
}

Here we have two routers. The base router has the routes that don't require auth, and the profileRouter contains the one profile route with the additional LoginRequiredMiddleware applied. The BuildRoutes function will get called during the main application bootstrap process.

http.Handler'n Things

Finally lets take a look at the two beefy routes in the app, the first of which is the login/signup handler. Here we get or create the user for the given email address (omitting email validation at this point), refresh their token and token expiration, then send an email with a verification link:

func HomeHandler(w http.ResponseWriter, r *http.Request) {
	// Redirect logged in users
	user := GetContextUser(r)
	if user != nil {
		http.Redirect(w, r, "/profile", http.StatusFound)
		return
	}

	if r.Method == "POST" {
		user := &User{}

		doRedirect := func() {
			http.Redirect(w, r, "/login-success", http.StatusFound)
		}

		if err := r.ParseForm(); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		email := r.PostFormValue("Email")
		if email != "" {
			// Get or create user with given email
			if err := dbmap.SelectOne(user, "SELECT * FROM users WHERE email=$1", email); err != nil {
				user.Email = email
				user.RefreshToken()

				if err := dbmap.Insert(user); err != nil {
					log.WithFields(log.Fields{
						"error": err,
						"email": email,
					}).Warn("Error creating user.")
					doRedirect()
					return
				}
			} else {
				// Update existing user's token
				user.RefreshToken()
				dbmap.Update(user)
			}

			// Update user.OriginUrl with referring page if its from this host, assuming
			// they came here via a redirect trying to access a page that requires auth
			if referrerUrl, err := url.ParseRequestURI(r.Referer()); err != nil {
				if referrerUrl.Scheme == r.URL.Scheme && referrerUrl.Host == r.URL.Host {
					if err := user.UpdateOriginUrl(referrerUrl); err != nil {
						dbmap.Update(user)
					}
				}
			}

			// Build login url
			params := url.Values{}
			params.Add("token", user.Token)
			params.Add("uid", strconv.FormatInt(user.Id, 10))

			loginUrl := url.URL{}

			if r.URL.IsAbs() {
				loginUrl.Scheme = r.URL.Scheme
				loginUrl.Host = r.URL.Host
			} else {
				loginUrl.Scheme = "http"
				loginUrl.Host = r.Host
			}

			loginUrl.Path = "/verify"

			// Send login email
			var mailContent bytes.Buffer
			ctx := struct {
				LoginUrl string
			}{
				fmt.Sprintf("%s?%s", loginUrl.String(), params.Encode()),
			}

			go func() {
				if err := loginEmailTemplate.Execute(&mailContent, ctx); err == nil {
					if err := SendMail([]string{email}, "Passwordless Login Verification", mailContent.String()); err != nil {
						log.WithFields(log.Fields{
							"error": err,
						}).Error("Error sending verification email")
					}
				}
			}()
		}

		doRedirect()
		return
	}

	renderTemplate(w, r, "home", nil)
}

It's also worth noting our handling of OriginUrl is a little naive. The handler indiscriminately uses the referring page, but if the user was coming from a page that didn't require auth then it should probably be ignored or set to some kind of default. A better approach is to have LoginRequiredMiddleware add a querystring param like ?next=<path to page that requires auth>, and if its present on the login page use it instead.

The verification handler takes the token and user's ID from the URL params, validates the token, and then logs the user in if everything checks out by calling the Login helper which stores the user ID in the session:

func VerifyHandler(w http.ResponseWriter, r *http.Request) {
	// Redirect logged in users
	user := GetContextUser(r)
	if user != nil {
		redirectToOrigin(user, w, r)
		return
	}

	// Collect URL params
	params := r.URL.Query()
	userId := params.Get("uid")
	userToken := params.Get("token")

	doResponse := func() {
		// Something failed along the way...
		renderTemplate(w, r, "verify", nil)
	}

	if userId != "" && userToken != "" {
		userId, err := strconv.ParseInt(userId, 0, 64)
		if err != nil {
			doResponse()
			return
		}

		if obj, err := dbmap.Get(User{}, userId); err == nil {
			user := obj.(*User)
			if user.IsValidToken(userToken) {
				// Valid token, log user in
				Login(user, w, r)

				// Do redirect
				redirectToOrigin(user, w, r)
				return
			}
		}
	}

	doResponse()
}

Wrapping Up

Hopefully this served as a nice bare bones example to passwordless authentication. Check out the full source on github, which includes a heroku button for instant deployment to the wonderful heroku platform for free.

Also, If you're a fan of Node, check out the Passwordless project which served as the inspiration for this post.

comments powered by Disqus