An API with Authentication using JWT (with and without refresh token) and cookie based
This repository contains a small HTTP API that manages users backed by PostgreSQL. It was built using Go 1.25 and assumes a PostgreSQL instance is available, if not... I've provided a Docker command to run one locally.
This is an area of study aimed at testing different ways to create an authentication system.
- Go 1.25
- PostgreSQL (run with Docker)
- chi (router)
- jackc/pgx driver
- gorilla/schema for query decoding
- godotenv for environment variables
- Go 1.25 or later installed on your machine
- Docker (for running PostgreSQL locally) or an accessible Postgres instance
- Make sure the
DATABASE_URLenvironment variable points to a reachable Postgres database
Create a .env file (example provided in the project). The service expects at least:
JWT_PORT = "localhost:8001"
COOKIE_PORT = "localhost:8000"
JWT_REFRESH_PORT = "localhost:8002"
OAUTH2_PORT = "localhost:8003"
#-------------------------------------
DATABASE_URL="postgres://root:example@localhost:5432/postgres"
#-------------------------------------
ISSUER="golang"
JWT_SECRET_KEY="<your-secret-key-for-jwt-token>"
TOKEN_DURATION="15" # In minutes...
#-------------------------------------
GOOGLE_KEY="<your-secret-key-from-google-oauth2>"
GOOGLE_CLIENT_ID="<your-client-id-from-google-oauth2>"
GOOGLE_CLIENT_SECRET="random secret... omg!!"
URL_CALLBACK="http://localhost:8000/auth/google/callback" # change this if you're running the application in another port...
#-------------------------------------
SECRET_CURSOR_KEY="<your-secret-key-for-cursor-hash>"
SIGNATURE_LENGTH="32" # Default length for sha256Use the schema below to initialize the database:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
session_token VARCHAR(255),
csrf_token VARCHAR(255),
age INT not null
);
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(255) PRIMARY KEY NOT NULL,
username VARCHAR(50) NOT NULL,
is_revoked BOOL NOT null default false,
refresh_token VARCHAR(512) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
expires_at TIMESTAMP
);-
Clone the repo:
git clone <repo-url> cd fauthless-go go mod tidy
-
Create a
.envfile (or use the provided.env.example) and setDATABASE_URL,JWT_PORT,COOKIE_PORTandJWT_REFRESH_PORTas needed. -
Start PostgreSQL with Docker:
docker run --name postgres -e POSTGRES_PASSWORD=example -e POSTGRES_USER=root -e POSTGRES_DB=postgres -p 5432:5432 -d postgres:15 # OR docker-compose up -dAdjust user/password/db name to match your
DATABASE_URLif necessary. -
Apply the database schema (run the SQL above) using
psqlor a GUI tool. -
Run the service:
# from the project root if you dont want to use Docker. If so, change the env variable to point to localhost instead of postgres. go run cmd/cookie-based/main.go # This one for the Cookie token with csrf protection. go run cmd/jwt-based/main.go # This one for the JWT but without the refresh token, only the expiration time go run cmd/jwt-refresh-based/main.go # This one for the JWT with refresh token. go run cmd/oauth/main.go # This one for the Login using Google, don't worry... the application stores no data... feel free to check
- POST /register
- POST /login (auth-type dependent: Cookie, JWT, JWT+Refresh)
Base path: /api/v1
- GET |
/users/{id} - GET |
/users/hashed-cursor-pagination - GET |
/users/cursor-pagination?size=$&cursor=$ - GET |
/users/offset-pagination?page=$&size=$ - PATCH |
/users/{username} - DELETE |
/users/{username}
Body
{
"username": "alice",
"password": "plainPassword123",
"age": 30
}Example
curl -i -X POST http://localhost:8000/register -H "Content-Type: application/json" -d '{"username":"alice","password":"plainPassword123","age":30}'Your server can run in one of four modes:
-
Cookie-based |
COOKIE_PORT=8000 -
JWT-based |
JWT_PORT=8001 -
JWT + Refresh |
JWT_REFRESH_PORT=8002 -
OAuth2 |
OAUTH2_PORT=8003
POST /login
Creates: - session_token cookie (HttpOnly) - crsf_token cookie
Body
{
"username": "alice",
"password": "plainPassword123"
}Example
curl -i -c cookies.txt -X POST http://localhost:8000/login -H "Content-Type: application/json" -d '{"username":"alice","password":"plainPassword123"}'After the log in, you need to get the CSRF token on the cookies response.
Also add into the Header a "X-CSRF-Token" with the token value.
curl -b cookies.txt http://localhost:8000/api/v1/userscurl -b cookies.txt -X PATCH http://localhost:8000/api/v1/users -H "Content-Type: application/json" -H "X-CSRF-Token: $CRSF" -d '{"age": 31}'POST /login
Response:
{
"token": "<JWT>"
}Example:
TOKEN=$(curl -s -X POST http://localhost:8001/login -H "Content-Type: application/json" -d '{"username":"alice","password":"plainPassword123"}' | jq -r '.token')curl -H "Authorization: Bearer $TOKEN" http://localhost:8001/api/v1/usersThis authentication mode issues two tokens on login: a short-lived access token (JWT) used for API requests, and a longer-lived refresh token used to obtain new access tokens without re-authenticating. Refresh tokens are stored server-side (sessions table) so they can be revoked.
- POST /login — returns
session_id,access_token,refresh_token, and their expiration times. - POST /renew — accepts a refresh token and returns a new access token and its expiration.
- POST /revoke/{id} — revoke a session by
id(sets session as revoked). Returns204 No Contenton success.
Request:
curl -s -X POST http://localhost:8002/login -H "Content-Type: application/json" -d '{"username":"alice","password":"plainPassword123"}'Successful response (201 Created):
{
"session_id": "8a4f2d9e-1a3b-4c2a-9b8f-0a1b2c3d4e5f",
"access_token": "<JWT_ACCESS_TOKEN>",
"refresh_token": "<JWT_REFRESH_TOKEN>",
"access_token_expires_at": "2025-11-30T12:34:56Z",
"refresh_token_expires_at": "2025-12-01T12:34:56Z"
}Request:
curl -s -X POST http://localhost:8002/renew -H "Content-Type: application/json" -d '{"refresh_token":"<JWT_REFRESH_TOKEN>"}'Successful response (200 OK):
{
"access_token": "<NEW_JWT_ACCESS_TOKEN>",
"access_token_expires_at": "2025-11-30T12:44:56Z"
}curl -X POST http://localhost:8002/revoke/8a4f2d9e-1a3b-4c2a-9b8f-0a1b2c3d4e5fOAuth2 support is implemented using the goth and gothic packages. The oauth server flow in this project exposes a simple UI and three main endpoints used by the Google provider: start auth, callback and logout.
- GET /auth/{provider} — Redirects to the provider's auth page (e.g.
/auth/google). - GET /auth/{provider}/callback — Provider redirects back to this URL after the user grants permission.
- GET /logout/{provider} — Logs the user out from the local session and redirects to
/.
Example handler behavior (simplified)
GetAuthOAuth2will callgothic.BeginAuthHandler(w, r)to start the provider flow. If an existing goth session is present, it will display the user.GetAuthCallbackOAuth2will callgothic.CompleteUserAuth(w, r)to complete the flow and receive provider user data.LogoutOAuth2callsgothic.Logout(w, r)and redirects to/.
- Go to Google Cloud Console > APIs & Services > Credentials.
- Create an OAuth 2.0 Client ID. Set the Authorized redirect URI to the
URL_CALLBACKvalue from your.env.example(e.g.http://localhost:8000/auth/google/callback). - Fill
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin your.env.examplefile. - Run
go run cmd/oauth/main.goand openhttp://localhost:8000/in your browser to start the flow.
Note: The project does not persist provider user information by default — it only demonstrates the OAuth flow and prints/stores session info. Adjust as needed to map provider users to your local user table.
Paginated list
Supports: - ?page=1 - ?size=25
Paginated list with a hash to ensure security. Example body:
{
"cursor": ""
}OR
{
"cursor": "eyJzaXplIjoxMywibmV4dF9jdXJzb3..."
}Paginated list
Supports: - ?cursor=0 - ?size=25
Returns a single user object by numeric id.
Updates user info (must be account owner). Example body:
{ "age": 31 }Deletes account (must be account owner).
# Register
curl -X POST http://localhost:8000/register -H "Content-Type: application/json" -d '{"username":"alice","hashed_password":"pwd","age":30}'
# Cookie Login
curl -i -c cookies.txt -X POST http://localhost:8000/login -H "Content-Type: application/json" -d '{"username":"alice","password":"pwd"}'
# JWT Login
TOKEN=$(curl -s -X POST http://localhost:8001/login -H "Content-Type: application/json" -d '{"username":"alice","password":"pwd"}' | jq -r '.token')There are some unit tests in the repository. To run them execute: (WIP)
go test ./...Add more tests and edge case coverage as needed.
- If the server cannot connect to Postgres, verify
DATABASE_URLand that the Docker container is running. - If OAuth callback fails, confirm the Google Cloud Console redirect URI exactly matches
URL_CALLBACKin.env.example. - When switching ports make sure the
.env.exampleports and any URLs (callback) are consistent across commands and provider settings.
If you need help or are having issues with the project, contact: [email protected].