Building off from the session-based authentication method, the next iteration of experimentation is on the use of tokens to manage identity on web applications. Token-based authentication leverages tokens, a compact piece of information, to validate an identity. Tokens come in different forms (or types):
- Connected token: The token is generated and managed by a physical device that is connected to the computer.
- Contactless token: Unlike connected token, the device is not connected to the computer but within the range to allow for connection and communication.
- Disconnected token: This is typically a digitally generated token that is used to confirm an entity’s identity. This could also be called software or soft token.
My focus will be on JSON Web Token (JWT) because it has the least implementation dependency among others. JWT is a disconnected token that uses an open industry standard for communication between two entities.
Features
In order to make it more practical, I’ll be building a web application with the following features:
- Login: Accepts a user name and password. It returns a JSON response containing the JWT.
- Base path: An endpoint that requires authentication to be accessed
Log out?
JWT is stateless by default, so there’s nothing to do on logout. Except state is introduced, then the log out feature isn’t necessary.
How JWT works
JWT tokens provide a means of authorization for an entity. On a successful authentication, it is generated and sent back to the client. A JWT consists of three parts:
Header: This contains the token type and the signing algorithm:
Payload: This is a set of information (claims) about the entity and token. The key for each item in the payload (claim name) could either be: registered, public and private. Registered claims are managed in the IANA JWT Claims registry. Public claims are custom name that are either added to the IANA JWT Claims registry or unique name not existing in the registry. Private claims are those that are agreed upon by the entities interacting with each other with a possibility of being non-unique from the ones on the registry.
Signature: This is a Message Authentication Code (MAC) or digital signing used to verify the authenticity of the token. The signature generation is a 3-step process:
- Generate a
Base64URL
string of the Header - Generate a
Base64URL
string of the Payload - Encrypt the generated
Base64URLs
with a secret key
The
Base64URL
of the Header and Payload are concatenated with a dot (.
) before encryption. The encryption algorithm can be done with either a symmetric or asymmetric key such as: HMAC256, RSA and ECDSA.- Generate a
The Base64URL string of each part is joined together with dots(.) to produce the token:
The client passes this token for subsequent request while the server validates the authenticity of the token before responding to the request. For this reason, it is a stateless flow as tokens aren’t persisted in the DB.
Implementation
Go is the language of choice for implementation, harnessing the standard libraries only. I’ll be using the TDD approach to help with validating my thought before implementing the feature.
The project set up is straight forward:
Generate JWT
The central part of the POST /login
endpoint is the token generation, so I’ll be getting that out of the way first. The requirements include:
- The ability to generate the
Base64URL
of the Header - The ability to generate the
Base64URL
of the Payload - The ability to generate a token
These requirements will serve as the test cases. A token_test.go
file can be created to capture them as follow:
Ideally, when a test case is written, the code required to make it pass is written immediately, but this isn’t a TDD post and the test cases are straightforward. The type (struct) for each part is created and passed to the function that creates a Base64URL
string. The Base64URL
string is decoded and compared with the expected string.
A token.go
file can be created to implement the requirements:
Some things to take note of:
- The secret key should be managed outside the application
RawURLEncoding
is used to avoid the padding character (add reference)- The expiry time could be shorter because of the risk of key hijacking
Login
The /login
endpoint is a POST
request accepting a username and password. The authentication requirements are:
- Return a bad request (HTTP 400) when either the username or password isn’t provided
- Return not found (HTTP 404) if user doesn’t exist
- Return an unauthorized access error (HTTP 401) when the username and password don’t match
- Return a JSON response containing a token (200 OK code) on a successful validation
A new test file, server_test.go
, is created to capture these requirements:
On running the test, I get the expected error that the Login
function is undefined
:
To make the test pass, I created the server.go
file and added the Login
function:
Before fleshing out the Login
function, I need to add the set up code that includes:
- Credential struct: This is a custom type that will be used to capture the payload of the login request.
- Constants: These are the fixed and reusable identifier needed across the package
- Dummy users: The users map represent existing users that are being validated. This is to keep the implementation simple by not using any form of storage.
This will be added just above the Login
function:
With the set up in place, the first requirement for the login request can be implemented:
On running the test again, it should return a successful outcome:
Following the TDD approach, the test cases and feature can be implemented for the remaining requirements:
Validate JWT
To validate the token, I will:
- Get and decode the Header
Base64URL
string - Get and decode the Payload
Base64URL
string - Generate a new token using the same secret key
- Compare the previous and newly generated token
While the test cases didn’t capture these requirements, they simply call the ValidateJWT
method in the token.go
file to perform the required checks:
Home
With the token validation in place, I can now add the path that requires authentication to access. Here, are the requirements:
- Return an unauthorized access (401) if the token is missing
- Return an unauthorized access (401) for an invalid token
- Return a success response if token exists and is valid
The server_test.go
and server.go
files can be updated to capture these requirements:
Router
To tie everything together, a router is introduced to allow clients communicate with the server. The implementation of the router is done in a new file, main.go
, inside the main()
function:
And that’s a wrap! Obviously, the implementation would be more complex in libraries, but this was a fun ride.
Lessons
Here are some takeaways from this experiment:
- I mixed up casing for the method names which isn’t right. Method names with
PascalCasing
is for public method while those withcamelCasing
is for private methods. In my defence, I’m not using packages yet 🌚 - Errors weren’t handled properly, particularly in the
token.go
file. I should be returning the error message along with the boolean from each method in thetoken.go
file - Order matters when validating credentials. I initially wrote the test for credentials mismatch before the nonexistent user, making the nonexistent user check fall into the credentials check
- I didn’t break down the flow of the token validation enough before implementation. Luckily, I caught it before going off track.
- The validation flow isn’t the best in class as described in RFC 8725. The goal of the experiment is to understand the concept of JWT and how libraries that manage it works.
References
- What Is An Authentication Token? | Fortinet
- RFC 7515: JSON Web Signature (JWS)
- RFC 7516: JSON Web Encryption (JWE)
- RFC 7518: JSON Web Algorithm (JWA)
- RFC 7519: JSON Web Token (JWT)
- JWT.IO - JSON Web Tokens Introduction
- Why is JWT popular?
- JSON Web Token (JWT) | IANA
- RFC 8725 JSON Web Token Best Current Practices
- Encryption choices
- Base64 Encoding | Go Packages