Skip to content

Repository files navigation

OAuth 2.0 Authorization Server

A fully functional OAuth 2.0 authorization server built from scratch with Node.js, TypeScript, Express, and PostgreSQL. Implements the Authorization Code flow, Refresh Token flow with rotation and reuse detection, Client Credentials flow, token revocation, and a Bearer-token validation middleware — verified end to end with a real client application integration.


What This Is

When you click "Sign in with Google," an OAuth server is doing the work behind the scenes — letting a third-party app access your data without ever seeing your password. This project implements that server. It issues scoped, revocable, time-limited access tokens to registered client applications on behalf of users, without exposing user credentials to those applications.

The whole system was validated with a separate test client app that performs the complete login dance across two servers, proving it works as a real integration and not just isolated endpoints.


Core Concept: Delegated Authorization

OAuth solves delegated authorization — granting an application limited access to resources without sharing credentials. Instead of handing an app your password, the app receives an access token that:

  • Grants only specific permissions (scope)
  • Can be revoked without changing your password
  • Expires automatically

OAuth is an authorization protocol (what you can do), not authentication (who you are). The login layer on top of it is a separate spec called OpenID Connect, which this project does not implement.


Grant Types Implemented

Authorization Code Flow

The full flow for when a user grants an app access to their resources.

  1. App redirects the user to /authorize with its client_id, redirect_uri, and scope
  2. Server validates the client and shows a consent screen
  3. User approves; server issues a short-lived, single-use authorization code via browser redirect
  4. App's backend exchanges the code (plus its client_secret) at /token for an access token and refresh token

The intermediate code exists for security: it travels through the browser (a leaky channel — history, logs, Referer headers), but it's useless to an attacker because it's single-use, expires in 60 seconds, and can't be exchanged without the client_secret, which only the app's backend holds. The token itself is exchanged through a secure server-to-server back-channel.

Refresh Token Flow (with Rotation)

Access tokens expire in 15 minutes. Rather than forcing the user to re-authorize, the app uses a long-lived refresh token to mint a new access token.

This implementation uses refresh token rotation — each refresh token works exactly once. When used, the old token is revoked and a new pair is issued. If a revoked refresh token is ever presented again, it signals possible theft (two parties holding the same token), and the request is rejected. This turns silent token theft into a detectable event.

Client Credentials Flow

For machine-to-machine communication where no user is involved — one backend service authenticating to another. The service authenticates with its own client_id and client_secret and receives an access token directly. No user, no consent, no authorization code, and no refresh token (the service can simply re-authenticate when needed).

Token Revocation

A /revoke endpoint lets a client explicitly invalidate a token, such as on logout. Because the access and refresh tokens share a database row with a single revocation flag, revoking either one invalidates both — so logout fully cuts access and the refresh token can't be used to mint new tokens.


Security Properties

  • Client secrets are hashed with bcrypt before storage; the plaintext is shown only once at registration and never recoverable.
  • Exact redirect_uri matching — registered redirect URIs are matched exactly, preventing attackers from redirecting authorization codes to their own servers.
  • Single-use authorization codes with 60-second expiry, preventing replay.
  • Bearer token validation via middleware that verifies the JWT signature and checks the token hasn't been revoked in the database.
  • No secrets in JWT payloads — JWT payloads are only base64-encoded (not encrypted), so client secrets never go inside them.
  • Refresh token rotation with reuse detection.

Note: This server runs over HTTP for local development. In production, OAuth requires HTTPS/TLS — TLS encrypts the entire request including the body, which is why sending the client_secret in the request body is safe. Without HTTPS, credentials would be exposed in transit.


Architecture

Client App                OAuth Server                    Database
    |                          |                              |
    |  GET /authorize ───────► validate client + redirect_uri |
    |                          show consent screen            |
    |  ◄─── redirect w/ code   issue single-use code ───────► auth_codes
    |                          |                              |
    |  POST /token ──────────► verify secret + code           |
    |  (code + secret)         issue tokens ────────────────► tokens
    |  ◄─── access + refresh   |                              |
    |                          |                              |
    |  GET /userinfo ────────► requireAuth: verify JWT        |
    |  (Bearer token)          + check not revoked ─────────► tokens
    |  ◄─── user data          |                              |

Endpoints

Method Endpoint Description
POST /api/v1/register Register a new client app; returns client_id and plaintext client_secret (shown once)
GET /api/v1/authorize Validates client and renders the consent screen
POST /api/v1/authorize Handles user approval; issues an authorization code and redirects
POST /api/v1/token Token endpoint for all three grant types (authorization_code, refresh_token, client_credentials)
GET /api/v1/userinfo Protected endpoint; returns user data for a valid Bearer token
POST /api/v1/revoke Revokes a token (access or refresh)

Database Schema

Four tables: clients, users, auth_codes, and tokens. Foreign keys cascade on delete, secrets and passwords are stored hashed, and token columns use TEXT to accommodate JWT length.

CREATE TABLE clients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    client_id UUID NOT NULL UNIQUE,
    client_secret VARCHAR(255) NOT NULL,   -- bcrypt hash
    redirect_uri VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,        -- bcrypt hash
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE auth_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    code VARCHAR(255) NOT NULL UNIQUE,
    scope VARCHAR(255) NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    is_used BOOLEAN DEFAULT false
);

CREATE TABLE tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    access_token TEXT NOT NULL,
    refresh_token TEXT NOT NULL,
    client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    scope VARCHAR(255) NOT NULL,
    access_token_expires_at TIMESTAMP NOT NULL,
    refresh_token_expires_at TIMESTAMP NOT NULL,
    is_revoked BOOLEAN DEFAULT false
);

Tech Stack

  • Node.js + TypeScript
  • Express
  • PostgreSQL (via pg)
  • jsonwebtoken for signing/verifying access and refresh tokens
  • bcrypt for hashing client secrets and passwords

Quick Start

# Clone and install
git clone https://github.com/littlegod20/oauth-server.git
cd oauth-server
npm install

# Set up environment
cp .env.example .env   # add DATABASE_URL and JWT_SECRET

# Create the database and tables
psql -U postgres -c "CREATE DATABASE oauth_server;"
psql -U postgres -d oauth_server -f schema.sql

# Run
npm run dev

Environment Variables

DATABASE_URL=postgresql://user:password@localhost:5432/oauth_server
JWT_SECRET=your_long_random_secret

Testing the Full Flow

A separate test client application (in oauth-test-client/) demonstrates the complete integration. With both servers running, visiting http://localhost:4000/oauth triggers the entire Authorization Code flow: redirect to consent, approval, code exchange, and a protected /userinfo call — ending with the user's data rendered in the browser.


Future Enhancements

  • PKCE for public clients (mobile apps, SPAs) that cannot safely store a client secret
  • Token family revocation — tracking refresh token lineage with a family_id to revoke an entire chain on reuse detection, rather than a single token
  • OpenID Connect layer for authentication (ID tokens)
  • Scope enforcement on protected endpoints, not just scope storage
  • HTTPS/TLS for production deployment

About

A fully functional OAuth 2.0 authorization server built from scratch with Node.js, TypeScript, Express, and PostgreSQL.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages