Integrating JWT Authentication with Go and chi jwtauth Middleware

Accessing an e-mail account anywhere in the world on any device requires authenticating yourself to prove the data associated with the account (e.g., e-mail address and inbox messages) actually belongs to you. Often, you must fill out a login form with credentials, such as an e-mail address and password, that uniquely identify your account. When you first create an account, you provide this information in a sign-up form. In some cases, the service sends either a confirmation e-mail or an SMS text message to ensure that you own the supplied e-mail address or phone number. Because it is highly likely that only you know the credentials to your account, authentication prevents unwanted actors from accessing your account and its data. Each time you log into your e-mail account and read your most recent unread messages, you, and like many other end users, don't think about how the service implements authentication to protect/secure your data and hide your activity history. You're busy, and you only want to spend a few minutes in your e-mail inbox before closing it out and resuming your day. For developers, the difficulty in implementing authentication comes from striking a balance between the user experience and the strength of the authentication. For example, a sign up form may prompt the user to enter a password that contains not only alphanumeric characters, but also must meet other requirements such as a minimum password length and containing punctuation marks. Asking for a stronger password decreases the likelihood of a malicious user correctly guessing it, but simultaneously, this password is increasingly more difficult for the user to remember. Keep in mind that poorly designed authentication can easily be bypassed and introduce more vulnerabilities into your application. In most cases, applications implement either session-based or token-based authentication to reliably verify a user's identity and persist authentication for subsequent page visits. Since Go is a popular choice for building server-side applications, Go's ecosystem offers many third-party packages for implementing these solutions into your applications. Below, I'm going to show you how to integrate JWT authentication within a Go and chi application with the chi jwtauth middleware. Let's imagine the following scenario. Within your e-mail inbox, you are asked to re-enter your e-mail address and password on every single action you take (e.g., opening an unread e-mail or switching to a different inbox tab) to continuously verify your identity. This implementation could be useful in the context of accidentally leaving your e-mail inbox open on a publicly-shared library computer when you have to step out to take a phone call. However, if the login credentials are sent over a non-HTTPS connection, then the login credentials are susceptible to a MITM (man-in-the-middle) attack and can be hijacked. Plus, it would result in a frustrating user experience and immediately drive users away to a different service. Traditionally, to persist authentication, an application establishes a session and saves an http-only cookie with this session's ID inside the user's browser. Usually, this session ID maps to the user's ID, which can then be used to fetch the user's information. If you have ever built an Express.js application with the authentication middleware library Passport and session middleware library express-session , then you are probably familiar with the connect.sid http-only cookie, which is a session ID cookie, and managing sessions with Redis . In Redis, the connect.sid cookie's corresponding key is the session ID (the substring proceeding s%3A and preceding the first dot of this cookie's value) prefixed with sess: , and its value contains information about the cookie and user authenticated by Passport. When a user sends an authentication request (via the standard username/password combination or an OAuth 2.0 provider such as Google / Facebook / Twitter ), Passport determines which of these authentication mechanisms ("strategies") to use to process the request. For example, if the user chooses to authenticate via Google, then Passport uses GoogleStrategy , like so: The done function supplies Passport with the authenticated user. To avoid exposing credentials in subsequent requests, the browser uses a unique cookie that identifies the user's session. Passport serializes the least amount of information that's required to map the user to the session. Often, the user's ID gets serialized. By serializing as little information as needed, this means there is less data stored in the user's session. Upon receiving a subsequent requests, Passport deserializes the user's ID (serialized via serializeUser ) into an object with the user's information, which allows it to be up to date with any recent changes. Whenever an Express.js route needs to access this information, it can via the req.user object. With session-based authentication, authentication is stateful because the server persists/tracks the session (either within the server's internal memory or an in-memory data store like Redis or Memcached). With token-based authentication, authentication is stateless . With tokens, nothing needs to be persisted on the server-side, and the server doesn't need to fetch the user's information on every subsequent request. One of the most popular token standards is JSON Web Token (JWT). JWTs are used for authorization, information exchange and verifying the user's authentication. Instead of creating a session, the server creates a cryptographically-signed JWT and saves an http-only cookie with this token inside of the user's browser, which allows the JWT to automatically be sent on every subsequent request. If the JWT is saved in plain memory, then it should be sent in the Authorization header using the Bearer authentication scheme ( Bearer <token> ). A JWT consists of three strings encoded in Base64URL : These strings are concatenated together (separated by dots) to form a token. Example : The following is a simple JWT, which follows the format <BASE64_URL_HEADER>.<BASE64_URL_PAYLOAD>.<BASE64_URL_SIGNATURE> : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c Constructing JWTs is relatively straight-forward. Decoding a JWT is also relatively straight-forward. Try out different signing algorithms, adding scope: [ "admin", "user" ] to the payload or modifying the secret in the JWT debugger . Note : Since a JWT is digitally signed, its content is protected from tampering. Tampering invalidates the token. Having sensitive data in the payload or header requires the JWT to be encrypted. It is recommended to first sign the JWT and then encrypt it . Referring back to the previous Express.js and Passport example, we can remove both Redis, the session middleware and the serialization/deserialization logic (relies on sessions), and then add the Passport JWT strategy passport-jwt for authenticating with a JWT. We no longer have to devote any backend infrastructure/resources to managing sessions with the introduction of token-based authentication via JWT. This will significantly reduce the number of times we need to query the database for the user's information. Like any other authentication method, token-based authentication comes with its own set of unique problems. For example, when we store the token in a cookie, this cookie is sent on every request (bound to a single domain), even those that don't require the user to be authenticated. Although this cookie is stored with the HttpOnly attribute (inaccessible to JavaScript), it is still susceptible to a Cross-Site Request Forgery attack, which happens when a third-party website successfully sends a request to a service without the user's explicit consent due to cookies (those set by the service's server) being sent on all requests to that service's server. If you're running an online banking service, and one of your users is authenticated and visits a malicious website that sends the request POST when they click on a harmless-looking button, then money will be transferred from the user's bank account since their valid token is sent with the request. To mitigate this vulnerability, set the token's cookie SameSite attribute ( sameSite: "lax" or sameSite: "strict" depending on your needs) and include a CSRF token specific to each user of your service in case of malicious subdomains. It should be set as a hidden form field in forms that send requests to protected endpoints upon being submitted, and your service should regenerate a new CSRF token for the user upon them logging in. This way, malicious websites cannot send requests to protected endpoints unless they also know that specific user's CSRF token. Note : By default, the latest versions of some modern browsers already treat cookies without the SameSite attribute as if this attribute was set to Lax . Setting the SameSite attribute of a cookie to Strict restricts a cookie to its originating website only and prevents cookies from being sent on any cross-site request or iframe. Setting the SameSite attribute of a cookie to Lax causes the same behavior as Strict , but relaxes the cross-site request restriction to target only POST requests. The alternative is to store the token in localStorage , but this is not recommended because localStorage is accessible by any JavaScript code running on your website. Therefore, it is susceptible to a Cross-Site Scripting attack, which allows unwanted JavaScript code to be injected into and executed within your website. Common attack vectors for XSS are passing unsanitized user input directly to eval and appending unsanitized HTML (contains a <script /> tag with malicious code). Unlike sessions, an individual JWT cannot be forcefully invalidated when security concerns arise. Rather, there are approaches that can be taken to invalidate a JWT, such as... Fortunately, supporting JWT authentication in a Go and chi application is made easy with the third-party jwtauth library. Similar to the Express.js and Passport example, jwtauth validates and extracts payload information from a JWT for route handlers via several pre-defined middleware handlers ( jwtauth.Verifier and jwtauth.Authenticator ) and context respectively. To demonstrate this, let's walkthrough a simple login flow: Inside of a Go file, scaffold out the routes using the chi router library. This application involves only four routes: ( main.go ) Let's think about these routes in-depth. When the user logs in, the navigation bar should no longer display a "Log In" link. Instead, the navigation bar should display the user's username as a link, which opens the user's "Profile" page when clicked, and a "Log Out" link. This means that all pages that display the navigation bar should be aware of whether or not the user is logged in, as well as the identity of the user. Let's group the GET / , GET /login and GET /profile endpoints together via the r.Group method, and then execute the middleware handler jwtauth.Verifier to seek, verify and validate the user's JWT token. This handler accepts a pointer to a JWTAuth struct, which is returned by the jwtauth.New method. Essentially, this method creates a factory for generating JWT tokens using a specified algorithm and secret (an additional key must be provided for RSA and ECDSA algorithms). The POST /login and POST /logout endpoints can be grouped together to establish them as routes that don't require a JWT token. Behind-the-scenes, jwtauth.Verifier automatically searches for a JWT token in an incoming request in the following order: Once the JWT token is verified, it is decoded and then set on the request context. This allows subsequent handlers to have direct access to the payload claims and the token itself. When the user submits a login form, their credentials are sent to the endpoint POST /login . It's corresponding route handler checks if the credentials are valid, and when they are, the handler generates a token that encodes parts of the user's information (i.e., their username) as payload claims via a MakeToken function and stores the token cookie within the user's browser, all before redirecting the user to their "Profile" page. Note : Underscores indicate placeholders for unused variables. For this simplicity's sake, we're going to accept any username and password combination as long as each is at least one character long. When the user logs out, this token cookie needs to be deleted. To delete this cookie, set its MaxAge to a value less than zero. After this cookie is deleted, redirect the user back to the homepage. Although the GET / , GET /login and GET /profile endpoints rely on the jwtauth.Verifier middleware, they each need to be grouped individually (not together) to add custom middleware to account for these scenarios: When rendering the webpages via data-driven templates , we need to extract the user's username from the JWT token's payload, which we encoded via the MakeToken function, to display it within the navigation bar. The payload's claims can be accessed from the request's context. Once the templates are parsed and prepared via the template.ParseFiles and template.Must methods respectively, apply these templates ( tmpl ) to the page data data via the ExecuteTemplate method. The second argument of ExecuteTemplate method names the root template that contains the other parsed templates (partials). The output is written to the ResponseWriter , which will render the result as a webpage. Note : If building a service, such as a RESTful API, that requires a 401 response to be returned on protected routes that can only be accessed by an authenticated user, use the jwtauth.Authenticator middleware. Finally, spin up the server by running the go run . command. Within a browser, visit the application at http://localhost:8080 and open the browser's developer console. Observe how the browser sets and unsets the cookie when you log in and out of the application, and watch as the user's username gets extracted from the JWT and displayed in the navigation bar. If you find yourself stuck at any point while working through this tutorial, then feel free to visit the main branch of this GitHub repository here for the code. Explore and try out other token standards for authentication. If you want to learn more advanced back-end web development techniques with Go, then check out the Reliable Webservers with Go course by Nat Welch, a site reliability engineer at Time by Ping (and formerly a site reliability engineer at Google), and Steve McCarthy, a senior software engineer at Etsy.

Thumbnail Image of Tutorial Integrating JWT Authentication with Go and chi jwtauth Middleware