Authentication is the security layer implemented to prevent access to personal or sensitive information. It is a way to confirm identity before granting access to information. There are multiple approaches to confirming an identity, but, for the first phase of my experiment, I’ll be focusing on the popular ones used in web applications.
Features
In order to make it more practical, I’ll be building a basic web application with the following features:
- Login: Accept user name and password
- Log out: Invalidate the session ID
- Home: An endpoint that requires authentication to be accessed
How it works
With this method, the credential, typically the username and password, is provided for validation. On a successful validation of the credential, a session ID is generated, stored and added to the cookie header of the response. The client saves this session ID (in Cookies storage) and passes it along for every request. This is also known as a cookie-based authentication because of the use of cookies.
Session-based authentication is a stateful authentication method because the means of authorization (session ID) is persisted in a storage (DB and/or Cache). The session ID is persisted because the server uses it to validate the request from the client. Session IDs are invalidated and deleted either when they expire or when a logout request is initiated.
Implementation
Go is the language of choice for implementation harnessing the standard libraries with little to no third-party library. As much as possible, I'd also like to use a TDD approach to help with validating my thought before implementing the feature.
The project set up is straight forward:
Login
The /login
endpoint is a POST
request accepting a username and password. Generally, the authentication requirements are:
- Return an unauthorized access error (HTTP 401) when either the username or password isn’t provided
- Return a user not found error (HTTP 404) when the username doesn’t exist
- Return an unauthorized access error (HTTP 401) when the username and password don’t match
- Return a session ID (with a 200 OK code) on a successful validation
With a TDD approach, a test file (server_test.go
) is created to capture these requirements:
Ideally, the test for each requirement will be written, then the most minimal amount of code required to make the test pass is written. Finally, a refactor is done to tidy things up. However, that’s outside the scope of this topic.
To implement the requirements, another file, server.go
, is created:
A lot seems to be going on for the login feature but it’s straightforward:
- Packages: Keeping to the implementation constraint, we are using the standard libraries except for the UUID library (
github.com/google/uuid
) which is used for generating unique session IDs. - Credential struct: A custom type was created for the login details. The login details will be sent via JSON, so we need to export the fields to allow encoding/decoding in JSON.
- Map of users: In keeping the implementation simple, a map was used as store for existing users. Ideally, this should be read from storage.
- Map of sessions: Like users, a map is also used to store the created sessions.
- Mutex: This was added to guard the map of sessions against a race condition when writing to it.
- Constants and variables: These consists of all identifiers that are used within the package.
With the setup in place, the login function starts by decoding the JSON request into the Credential
type. By using a pointer, the decoder can write the decoded result into the memory address of the credential
variable. The major part of the function are checks from the requirements. Once all negative path have been covered, the session ID is created, using the uuid
package. The session ID is then mapped to the username
and used to create the cookie. The cookie is set on the response header and a 200 Ok
response is returned.
Home
With the login implemented, I can now add a path that requires authentication to access. Again, the requirements are:
- Return an unauthorized access (401) if a cookie is missing
- Return an unauthorized access (401) for an invalid cookie
- Return a success response if session exists and is valid
The server_test.go
file can be updated to capture these requirements and the feature can be implemented to make the test pass:
Logout
A session ID can becomes invalid when it either expires or when the user initiates a logout request. The client typically takes care of the former. Handling the latter involves removing the session ID from the store, in this case, the map:
Router
Each created function cannot be used by a client yet because they haven’t been tied to a route. The implementation of the router will be done in a new file, main.go
, inside the main()
function:
A request router (multiplexer to be precise) is created with the NewServeMux()
function in the http package. The routes are defined but specifying a string with an HTTP method, a space then the path and the handler for the route. The server can be configured with a port and the router.
Concerns
Session-based authentication benefits from an ease of implementation, however, because of the reliance on the client, it poses some concerns:
- Cross Site Request Forgery (CSRF): Cookies are on the client side which creates the room for bad actors to retrieve them through fake requests
- Privacy: With cookies usage for state managment, it can be easily used to track the activities of a user.
- Implementation: Some clients have limitations on the cookies such as: maximum number of bytes, maximum number of cookies per domain and the total number of cookies.
Lessons
Here are some things I can do better:
- Write and implement to allow for a more natural flow
- There are still rough edges with TDD, particularly around mocking and stubbing
- For the logout implementation, I didn’t consider how to invalidate expired session IDs