README ¶
Go Authentication Service (Golang + PostgreSQL + Redis)
This project is an authentication service written in Go that handles the following responsibilities:
- User registration, user logins, and storing user credentials.
- Issuing JSON Web Tokens (JWTs) for session-management purposes. Tokens are passed to the client-side as cookies.
- Generating and storing API Keys.
- Verifies on behalf of other services if JWTs and API Keys are valid. Service will return the user ID if valid.
User Authentication
The primary function of the authentication service is to handle the storing of user credentials, verifying user credentials, and issuing JSON Web Tokens (as cookies) to users who provide correct credentials. All of the user information is stored in a PostgreSQL database.
Authentication Flow
- Login by submitting a POST request to "/auth/login" with a valid Email and Password combination.
- Successful user login generates two JSON Web Tokens:
- Access Token: Valid for 15 minutes, the Access Token should be used to provide privileged access to other services.
- Refresh Token: Valid for 24 hours, the Refresh Token's only purpose is to generate new Access tokens.
- The signed tokens are returned to the client as values in the cookies "access_token" and "refresh_token".
- New access tokens can be generated as needed by submitting a POST request to "/auth/refresh":
- The service will check the Refresh Token included in the "refresh_token" cookie. If it is valid, a new Access Token will be generated and passed to the client-side in the "access_token" cookie.
- If the Refresh Token is no longer valid (ex: 24 hours have elapsed), the client will need to re-authenticate to "/auth/login".
- Logout by submitting a POST request to "/auth/logout".
JSON Web Token Blocklist
By design, it can be difficult to invalidate JSON Web Tokens from the server-side without queries to the database or maintaining individualized JWT Secrets. The service implements a JSON Web Token Blocklist stored in Redis in an attempt to overcome these limitations.
Any time that a user logs out of the service, the service will extract the signed JWT strings from the "access_token" and "refresh_token" cookies (if available) and then add them as keys to the Blocklist. And, any time that a user refreshes their Access Token, it will add the signed JWT string from "access_token" to the Blocklist.
The Blocklist is consulted whenever the authentication service checks if a JWT is valid. Access Token strings are kept in the Blocklist for 15 minutes. Refresh Token strings are kept for 24 hours.
API Keys
If needed, the authentication service can be used to generate and store API Keys for users. By design, it will only store a single API Key per user in the database.
Generating API Keys
- Generate a new API Key by submitting a POST request to "/auth/apikey".
- Only requests with a valid Access Token stored in the access_token cookie will be allowed to generate an API Key.
- If the associated user already has an API Key in the database, it will prompt the client to delete the current API Key before proceeding.
- The new API Key is returned to the client, and then hashed (HMAC-SHA256) and stored in the PostgreSQL database.
- Because we only store the hash of the key, it is not possible to retrieve an API Key after it has been generated.
- Users can delete their API Key by submitting a DELETE request to "/auth/apikey".
JWT and API Key Verification
The authentication service exposes two API Endpoints that can be used by other services to verify that a provided API Key or "access_token" cookie is valid. If valid, it will return the user's ID to the services.
Verifying the Access Token
It is important that you pass any Access Token received from the client-side to the "/auth/claims" API endpoint before making note of the User ID or passing the client request off to a protected service.
- Issue a GET request to
/auth/claims?token=123456789
, where token is the signed JWT string extracted from the "access_token" cookie. - The service will verify the signature and consult the JWT Blocklist. If valid, it will extract the Claims from the token and return the data as JSON. Returned claims will include the "userId", which contains the User ID associated with the Access Token.
Verifying API Keys
Similarly, API Keys should be checked against the "/auth/verify" API endpoint before passing the client request to any protected services.
- Issue a GET request to
/auth/verify?key=123456789
, where key is the API Key extracted from the request's HTTP Headers. - The service will hash the provided API Key and look for any matches in the database. If it finds a match, it will return the User ID associated with the API Key as JSON ("userId").
API Endpoints
POST /auth/register
Register a new user account. The service only expects an email and password. Implementing additional fields (DisplayName, FirstName, LastName, etc.) should be done on a separate service that can map to the user ID.
curl --request POST 'http://localhost:4000/auth/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "fakeman@gmail.com",
"password": "teS123x!!4^4"
}'
HTTP/1.1 201 Created
{
"email": "fakeman@gmail.com"
}
POST /auth/login
User login. If successful, returns access_token and refresh_token cookies.
curl --request POST 'http://localhost:4000/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "user@email.com",
"password": "!Test!1234!"
}'
HTTP/1.1 200 OK
POST /auth/logout
User logout. Returns expired access_token and refresh_token cookies to overwrite any pre-existing cookies.
curl --request POST 'http://localhost:4000/auth/logout' \
--header 'Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE3NDE5MjUsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.LcEve-kpwXKlsOAO5V-6_dHSqRiObyCcEnfBm1u1YgI; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE2NTY0MjUsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.Fwq95sARHAuX-tDGoCXdjvMMLOG9l_w9SaZGc7HWU5M'
HTTP/1.1 204 No Content
POST /auth/refresh
Refreshes the Access Token and returns the new token in an access_token cookie. Client request only requires a valid refresh_token cookie in order to be successful. Including the access_token cookie is not mandatory.
curl --request POST 'http://localhost:4000/auth/refresh' \
--header 'Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE3NDI0NzEsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.omM7ycOxUHC_etWlTrfbyBASicPlYnPtQZrY-6jBQ-A; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE2NTY5NzEsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.lFDce-4o6gh2jnKemAnf8HoWena-a6OcQk4vdczpZLo'
HTTP/1.1 200 OK
GET /auth/claims
Extracts the JSON Web Token string from the ?token=
query parameter. If the token is valid (not modified, etc.) then service will return the claims embedded within the token as JSON.
curl --request GET 'http://localhost:4000/auth/claims?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE2NTU2OTYsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.vFFeYSUYxoOYF8aozuStnwC2qqgx8cW3nnX6SXth01s'
HTTP/1.1 200 OK
{
"userId": "02e8c9d6-66c6-4d61-9934-84fe8b9a18a0",
"exp": "1.631655696e+09"
}
POST /auth/apikey
Generates a new API Key for the user if one does not already exist. Client request only requires a valid access_token cookie in order to be successful. Including the refresh_token cookie is not mandatory.
curl --request POST 'http://localhost:4000/auth/apikey' \
--header 'Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE3NDExOTYsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.9tW7SFQgxYImJLkFLrLuFiHg3rr2OihLcnfNSXETmk8; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE2NTU2OTYsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.vFFeYSUYxoOYF8aozuStnwC2qqgx8cW3nnX6SXth01s'
HTTP/1.1 201 Created
{
"apiKey": "NaLft36C4EfgcjMMKCQP0udJAZvEspu_pOFIG96rCfQ",
"msg": "Your new API key has been generated. Please save this key. It is not possible to recover this key."
}
DELETE /auth/apikey
Deletes the API Key associated with the requesting user. Client request only requires a valid access_token cookie in order to be successful. Including the refresh_token cookie is not mandatory.
curl --request DELETE 'http://localhost:4000/auth/apikey' \
--header 'Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE3NDExOTYsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.9tW7SFQgxYImJLkFLrLuFiHg3rr2OihLcnfNSXETmk8; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzE2NTU2OTYsInVzZXJJZCI6IjAyZThjOWQ2LTU1YzYtNGQ2MS04ODM0LTg0ZmU4YjlhMThhMCJ9.vFFeYSUYxoOYF8aozuStnwC2qqgx8cW3nnX6SXth01s'
HTTP/1.1 204 No Content
GET /auth/verify
Extracts the API Key string from the ?key=
query parameter. If the API Key exists in the database, then service will return the user ID associated with the API Key.
curl --request GET 'http://localhost:4000/auth/verify?key=NaLft36C4EfgcjMMKCQP0udJAZvEspu_pOFIG96rCfQ'
HTTP/1.1 200 OK
{
"userId": "02e8c9d6-66c6-4d61-9934-84fe8b9a18a0"
}