Look Ma, no JWT!

  • January 27, 2021
  • Pete Whiting
  • 6 min read

by Nick Maloney


  • JWT is a complicated standard and in many cases you probably don't need it
  • Instead, use a randomly generated bearer token that gets verified on the server

Why JWT?

When building an API there are a number of important decisions to be made. Authentication and security are generally high on the list of features to implement. For many scenarios, it is common to reach for a JWT-based auth workflow. It is easy to implement, most frameworks have a JWT library and let’s face it, there are countless examples on the internet for implementing it. Given its popularity, JWT must be the correct approach! … or is it?

A typical web API authentication flow follows the following steps:

  1. User authenticates via username and password
  2. API returns a token (often JWT)
  3. Client stores token
  4. Subsequent API requests are made using the token

While appearing simple, the steps listed above take a lot for granted. There are a number of implementation details that impact the overall security of your application. For example, step 1 implies you are securely hashing the password and not storing it in plain text. Step 2 implies all communications over the wire use https. Step 3 implies you are securely storing the bearer token. Step 4 implies that the token is correctly being verified.

One of the many reasons JWT has become ubiquitous is due to the volume of how-to’s/guides/etc, many of which have been cargo-culted, perpetuating its use, and occassionally are using insecure or incorrect implementations. I would argue that in many cases your application doesn't need JWT and is better off with a simpler solution.

If you are verifying the token on the server (as you likley should be), what does JWT offer that a bearer token doesn't? Below I’ll outline a simple POC that relies on a bearer token auth scheme.

Side Note

No self-respecting blog post offering alternatives to JWT would be complete without a tptacek quote from Hacker News. The following post from 2017 is the driving force behind this example:

“For almost every use I've seen in the real world, JWT is drastic overkill; often it's just an gussied-up means of expressing a trivial bearer token, the kind that could be expressed securely with virtually no risk of implementation flaws simply by hexifying 20 bytes of urandom.”

Demo Time

This basic application provides two endpoints written in Golang: one for authenticating and one for responding to authenticated requests. It uses go-cache as the "database" for storing the users and the auth tokens purely as a means to make it self-contained.

Data Structures

// An application context that contains anything required by the handlers
type AppContext struct {
	DB *cache.Cache

//Auth - Contains the auth token
type AuthContext struct {
	Token string `json:"token"`

//User - Basic user model
type User struct {
	Username     string `json:"username"`
	PasswordHash string `json:"-"`

The main function sets up a pseudo-db and adds a single user (User management is an exercise left for the reader). Two routes are also declared within the function:

  • /login Handles the authentication request and returns the bearer token
  • /user Returns information about the user to authenticated requests

App Setup

func main() {
  // We'd use an actual database IRL
	db := cache.New(cache.NoExpiration, cache.NoExpiration)

	// Here we'll populate the database with a sample user
	username := "nimalo"
	password := []byte("Secret Password!")
	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
	if err != nil {
	u := User{Username: username, PasswordHash: string(hash)}
	db.Set(u.Username, u, cache.NoExpiration)

	a := &AppContext{
		DB: db,

	http.HandleFunc("/login", a.loginHandler)
	http.HandleFunc("/user", a.userHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))

The login handler is responsible for receiving the authentication payload (username and password), authenticating it, and either returning an error OR a bearer token. It searches the database for the submitted user, and then compares the clear-text password against the hashed password using bcrypt's CompareHashAndPassword function. If the credentials are valid, it creates a pseudo-random token and persists it.

One of the big arguments for JWT is it is stateless; however, most implementations verify server side, which is the entire crux of this post. Verifying a pseudo-random token is easier and (IMHO) no less secure than a JWT.

Login Handler that creates the auth token

func (a *AppContext) loginHandler(w http.ResponseWriter, r *http.Request) {
	username := r.FormValue("username")
	password := r.FormValue("password")

	var user User
	if u, found := a.DB.Get(username); found {
		user = u.(User)

	// Compare the plain-text password with the stored hash
	err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))

	// If a user isn't found OR passwords don't match return an error
	if user.Username != username || err != nil {
		http.Error(w, "Access Denied", http.StatusForbidden)

	// Generate an auth token user CSPRNG (cryptographically secure pseudorandom number generator)
	bytes := make([]byte, 128)
	_, err = rand.Read(bytes)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	token := hex.EncodeToString(bytes)
	auth := Auth{Token: token}

	// Store the auth token and associate it with the user
	// Note - Use a db or Redis for storing this IRL. A cache
	// invalidation strategy will also need to be considered
	a.DB.Set(auth.Token, user.Username, cache.DefaultExpiration)

	js, err := json.Marshal(auth)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	w.Header().Set("Content-Type", "application/json")

Once a client has received a token, they'll then use it for subsequent requests. How that gets implemented is an entirely seperate blog post (or book). In this example, it is looking for a bearer token in the Authorization header but it could just as easily use a cookie (or both?). If a valid token is received it returns the user name, otherwise access will be denied. This post does not dive into revocation strategies. Presumably the token will expire at some point.

User Handler that verifies a bearer token

func (a *AppContext) userHandler(w http.ResponseWriter, r *http.Request) {
	authorization := r.Header.Get("Authorization")
	idToken := strings.TrimSpace(strings.Replace(authorization, "Bearer", "", 1))

	var username string
	if u, found := a.DB.Get(idToken); found {
		username = u.(string)

	if username == "" {
		http.Error(w, "Access Denied", http.StatusForbidden)

	user := User{Username: username}
	js, err := json.Marshal(user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	w.Header().Set("Content-Type", "application/json")

In Summary

JWT and stateless tokens do have their place in the security toolbox, particularly when multiple provider API's are communicating. For a typical web/mobile application, just use a bearer token. They are simple to implement, easily revokable, and just as secure on both the client and server (if using TLS) without the complexity of needing to correctly implement JWT. The full example file can be found here.

Further Reading

Interested in building with us?