Authentication & Authorization
Implement JWT-based auth, refresh tokens, RBAC middleware, and secure password hashing from scratch.
Authentication vs Authorization
Authentication answers “who are you?” — verifying identity via passwords, tokens, or OAuth. Authorization answers “what can you do?” — checking permissions to access resources.
Real-World Analogy
Like a gym membership card — you verify your identity once at the front desk (login), get a card (token), and swipe it for access without showing your ID again. Your membership tier (basic/premium) determines what areas you can use (authorization).
email + password
Verify credentials
access + refresh
Bearer token
Verify + RBAC
Why JWTs?
Traditional session-based auth stores session data on the server. This requires sticky sessions or shared session storage as you scale. JWTs are stateless — the token itself contains the user’s identity and permissions. Any server can validate it without hitting a database.
The tradeoff: you can’t revoke a JWT before it expires without maintaining a blacklist (which re-introduces server state). The solution: short-lived access tokens (15 min) + long-lived refresh tokens (7 days).
Complete Auth System
import http from "node:http";
import crypto from "node:crypto";
// --- Password Hashing (using scrypt — built into Node, no deps) ---
async function hashPassword(password: string): Promise<string> {
const salt = crypto.randomBytes(16).toString("hex");
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, (err, derived) => {
if (err) reject(err);
resolve(`${salt}:${derived.toString("hex")}`);
});
});
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
const [salt, key] = hash.split(":");
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, (err, derived) => {
if (err) reject(err);
resolve(crypto.timingSafeEqual(Buffer.from(key, "hex"), derived));
});
});
}
// --- JWT Implementation (no external deps) ---
interface JWTPayload {
sub: string; // user ID
email: string;
role: string;
iat: number; // issued at
exp: number; // expires at
}
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
const ACCESS_TOKEN_TTL = 15 * 60; // 15 minutes
const REFRESH_TOKEN_TTL = 7 * 24 * 3600; // 7 days
function base64url(data: string | Buffer): string {
const buf = typeof data === "string" ? Buffer.from(data) : data;
return buf.toString("base64url");
}
function signJWT(payload: Omit<JWTPayload, "iat" | "exp">, ttl: number): string {
const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const now = Math.floor(Date.now() / 1000);
const body = base64url(
JSON.stringify({ ...payload, iat: now, exp: now + ttl })
);
const signature = crypto
.createHmac("sha256", JWT_SECRET)
.update(`${header}.${body}`)
.digest("base64url");
return `${header}.${body}.${signature}`;
}
function verifyJWT(token: string): JWTPayload | null {
const parts = token.split(".");
if (parts.length !== 3) return null;
const [header, body, signature] = parts;
const expected = crypto
.createHmac("sha256", JWT_SECRET)
.update(`${header}.${body}`)
.digest("base64url");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return null; // invalid signature
}
const payload: JWTPayload = JSON.parse(
Buffer.from(body, "base64url").toString()
);
if (payload.exp < Math.floor(Date.now() / 1000)) {
return null; // expired
}
return payload;
}
// --- User Store (replace with database) ---
interface User {
id: string;
email: string;
passwordHash: string;
role: "user" | "admin" | "editor";
name: string;
}
const users = new Map<string, User>();
const refreshTokens = new Map<string, { userId: string; expiresAt: number }>();
// --- RBAC (Role-Based Access Control) ---
type Permission = "read:posts" | "write:posts" | "delete:posts" | "manage:users";
const rolePermissions: Record<string, Permission[]> = {
user: ["read:posts"],
editor: ["read:posts", "write:posts"],
admin: ["read:posts", "write:posts", "delete:posts", "manage:users"],
};
function hasPermission(role: string, permission: Permission): boolean {
return rolePermissions[role]?.includes(permission) ?? false;
}
// --- Auth Middleware ---
interface AuthenticatedRequest extends http.IncomingMessage {
user?: JWTPayload;
}
function requireAuth(
handler: (req: AuthenticatedRequest, res: http.ServerResponse) => void | Promise<void>
) {
return async (req: AuthenticatedRequest, res: http.ServerResponse) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
json(res, 401, { error: "Missing or invalid Authorization header" });
return;
}
const token = authHeader.slice(7);
const payload = verifyJWT(token);
if (!payload) {
json(res, 401, { error: "Invalid or expired token" });
return;
}
req.user = payload;
await handler(req, res);
};
}
function requirePermission(permission: Permission) {
return (
handler: (req: AuthenticatedRequest, res: http.ServerResponse) => void | Promise<void>
) => {
return requireAuth(async (req, res) => {
if (!req.user || !hasPermission(req.user.role, permission)) {
json(res, 403, { error: "Insufficient permissions" });
return;
}
await handler(req, res);
});
};
}
// --- Helpers ---
function json(res: http.ServerResponse, status: number, data: unknown): void {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
function parseBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
catch { reject(new Error("Invalid JSON")); }
});
});
}
// --- Route Handlers ---
async function handleRegister(req: http.IncomingMessage, res: http.ServerResponse) {
const body = await parseBody(req);
const email = body.email as string;
const password = body.password as string;
const name = body.name as string;
if (!email || !password || password.length < 8) {
json(res, 400, { error: "Email and password (min 8 chars) are required" });
return;
}
// Check existing user
for (const u of users.values()) {
if (u.email === email) {
json(res, 409, { error: "Email already registered" });
return;
}
}
const id = crypto.randomUUID();
const passwordHash = await hashPassword(password);
const user: User = { id, email, passwordHash, role: "user", name: name || "" };
users.set(id, user);
const accessToken = signJWT({ sub: id, email, role: user.role }, ACCESS_TOKEN_TTL);
const refreshToken = crypto.randomBytes(32).toString("hex");
refreshTokens.set(refreshToken, {
userId: id,
expiresAt: Date.now() + REFRESH_TOKEN_TTL * 1000,
});
json(res, 201, {
user: { id, email, name, role: user.role },
accessToken,
refreshToken,
expiresIn: ACCESS_TOKEN_TTL,
});
}
async function handleLogin(req: http.IncomingMessage, res: http.ServerResponse) {
const body = await parseBody(req);
const email = body.email as string;
const password = body.password as string;
let user: User | undefined;
for (const u of users.values()) {
if (u.email === email) { user = u; break; }
}
if (!user || !(await verifyPassword(password, user.passwordHash))) {
// Use same error message to prevent user enumeration
json(res, 401, { error: "Invalid email or password" });
return;
}
const accessToken = signJWT(
{ sub: user.id, email: user.email, role: user.role },
ACCESS_TOKEN_TTL
);
const refreshToken = crypto.randomBytes(32).toString("hex");
refreshTokens.set(refreshToken, {
userId: user.id,
expiresAt: Date.now() + REFRESH_TOKEN_TTL * 1000,
});
json(res, 200, {
user: { id: user.id, email: user.email, name: user.name, role: user.role },
accessToken,
refreshToken,
expiresIn: ACCESS_TOKEN_TTL,
});
}
async function handleRefresh(req: http.IncomingMessage, res: http.ServerResponse) {
const body = await parseBody(req);
const token = body.refreshToken as string;
const stored = refreshTokens.get(token);
if (!stored || stored.expiresAt < Date.now()) {
json(res, 401, { error: "Invalid or expired refresh token" });
return;
}
// Rotate refresh token (invalidate old one)
refreshTokens.delete(token);
const user = users.get(stored.userId);
if (!user) {
json(res, 401, { error: "User not found" });
return;
}
const accessToken = signJWT(
{ sub: user.id, email: user.email, role: user.role },
ACCESS_TOKEN_TTL
);
const newRefreshToken = crypto.randomBytes(32).toString("hex");
refreshTokens.set(newRefreshToken, {
userId: user.id,
expiresAt: Date.now() + REFRESH_TOKEN_TTL * 1000,
});
json(res, 200, {
accessToken,
refreshToken: newRefreshToken,
expiresIn: ACCESS_TOKEN_TTL,
});
}
// --- Protected route examples ---
const handleGetProfile = requireAuth(async (req, res) => {
const user = users.get(req.user!.sub);
if (!user) {
json(res, 404, { error: "User not found" });
return;
}
json(res, 200, {
id: user.id, email: user.email, name: user.name, role: user.role,
});
});
const handleAdminUsers = requirePermission("manage:users")(async (_req, res) => {
const allUsers = Array.from(users.values()).map((u) => ({
id: u.id, email: u.email, name: u.name, role: u.role,
}));
json(res, 200, { users: allUsers });
});
// --- Router ---
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
const method = req.method!;
try {
if (url.pathname === "/auth/register" && method === "POST") return await handleRegister(req, res);
if (url.pathname === "/auth/login" && method === "POST") return await handleLogin(req, res);
if (url.pathname === "/auth/refresh" && method === "POST") return await handleRefresh(req, res);
if (url.pathname === "/api/profile" && method === "GET") return await handleGetProfile(req, res);
if (url.pathname === "/api/admin/users" && method === "GET") return await handleAdminUsers(req, res);
json(res, 404, { error: "Not found" });
} catch (err) {
console.error(err);
json(res, 500, { error: "Internal server error" });
}
});
server.listen(3000, () => console.log("Auth server on http://localhost:3000"));package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"golang.org/x/crypto/bcrypt"
)
// --- Types ---
type User struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
Role string `json:"role"`
Name string `json:"name"`
}
type JWTPayload struct {
Sub string `json:"sub"`
Email string `json:"email"`
Role string `json:"role"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
}
type RefreshTokenData struct {
UserID string
ExpiresAt time.Time
}
// --- Config ---
var (
jwtSecret = []byte("your-secret-key-change-in-production")
accessTokenTTL = 15 * time.Minute
refreshTokenTTL = 7 * 24 * time.Hour
)
// --- Store ---
type AuthStore struct {
mu sync.RWMutex
users map[string]*User
refreshTokens map[string]RefreshTokenData
}
var store = &AuthStore{
users: make(map[string]*User),
refreshTokens: make(map[string]RefreshTokenData),
}
// --- JWT ---
func base64URLEncode(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data)
}
func signJWT(sub, email, role string, ttl time.Duration) string {
header := base64URLEncode([]byte(`{"alg":"HS256","typ":"JWT"}`))
now := time.Now().Unix()
payload, _ := json.Marshal(JWTPayload{
Sub: sub, Email: email, Role: role,
Iat: now, Exp: now + int64(ttl.Seconds()),
})
body := base64URLEncode(payload)
mac := hmac.New(sha256.New, jwtSecret)
mac.Write([]byte(header + "." + body))
sig := base64URLEncode(mac.Sum(nil))
return header + "." + body + "." + sig
}
func verifyJWT(token string) (*JWTPayload, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
mac := hmac.New(sha256.New, jwtSecret)
mac.Write([]byte(parts[0] + "." + parts[1]))
expected := base64URLEncode(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(parts[2]), []byte(expected)) != 1 {
return nil, fmt.Errorf("invalid signature")
}
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid payload")
}
var payload JWTPayload
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return nil, fmt.Errorf("invalid payload JSON")
}
if payload.Exp < time.Now().Unix() {
return nil, fmt.Errorf("token expired")
}
return &payload, nil
}
// --- RBAC ---
type Permission string
const (
ReadPosts Permission = "read:posts"
WritePosts Permission = "write:posts"
DeletePosts Permission = "delete:posts"
ManageUsers Permission = "manage:users"
)
var rolePermissions = map[string][]Permission{
"user": {ReadPosts},
"editor": {ReadPosts, WritePosts},
"admin": {ReadPosts, WritePosts, DeletePosts, ManageUsers},
}
func hasPermission(role string, perm Permission) bool {
perms := rolePermissions[role]
for _, p := range perms {
if p == perm {
return true
}
}
return false
}
// --- Middleware ---
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
writeJSON(w, 401, map[string]string{"error": "Missing Authorization header"})
return
}
payload, err := verifyJWT(strings.TrimPrefix(auth, "Bearer "))
if err != nil {
writeJSON(w, 401, map[string]string{"error": "Invalid or expired token"})
return
}
// Store user info in header for downstream handlers
r.Header.Set("X-User-ID", payload.Sub)
r.Header.Set("X-User-Email", payload.Email)
r.Header.Set("X-User-Role", payload.Role)
next(w, r)
}
}
func requirePerm(perm Permission, next http.HandlerFunc) http.HandlerFunc {
return requireAuth(func(w http.ResponseWriter, r *http.Request) {
role := r.Header.Get("X-User-Role")
if !hasPermission(role, perm) {
writeJSON(w, 403, map[string]string{"error": "Insufficient permissions"})
return
}
next(w, r)
})
}
// --- Handlers ---
func handleRegister(w http.ResponseWriter, r *http.Request) {
var input struct {
Email string `json:"email"`
Password string `json:"password"`
Name string `json:"name"`
}
json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&input)
if input.Email == "" || len(input.Password) < 8 {
writeJSON(w, 400, map[string]string{"error": "Email and password (min 8 chars) required"})
return
}
store.mu.Lock()
defer store.mu.Unlock()
for _, u := range store.users {
if u.Email == input.Email {
writeJSON(w, 409, map[string]string{"error": "Email already registered"})
return
}
}
hash, _ := bcrypt.GenerateFromPassword([]byte(input.Password), 12)
id := generateID()
user := &User{ID: id, Email: input.Email, PasswordHash: string(hash), Role: "user", Name: input.Name}
store.users[id] = user
accessToken := signJWT(id, input.Email, "user", accessTokenTTL)
refreshToken := generateRefreshToken()
store.refreshTokens[refreshToken] = RefreshTokenData{
UserID: id, ExpiresAt: time.Now().Add(refreshTokenTTL),
}
writeJSON(w, 201, map[string]interface{}{
"user": user, "accessToken": accessToken,
"refreshToken": refreshToken, "expiresIn": int(accessTokenTTL.Seconds()),
})
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
var input struct {
Email string `json:"email"`
Password string `json:"password"`
}
json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&input)
store.mu.RLock()
var user *User
for _, u := range store.users {
if u.Email == input.Email {
user = u
break
}
}
store.mu.RUnlock()
if user == nil || bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)) != nil {
writeJSON(w, 401, map[string]string{"error": "Invalid email or password"})
return
}
accessToken := signJWT(user.ID, user.Email, user.Role, accessTokenTTL)
refreshToken := generateRefreshToken()
store.mu.Lock()
store.refreshTokens[refreshToken] = RefreshTokenData{
UserID: user.ID, ExpiresAt: time.Now().Add(refreshTokenTTL),
}
store.mu.Unlock()
writeJSON(w, 200, map[string]interface{}{
"user": user, "accessToken": accessToken,
"refreshToken": refreshToken, "expiresIn": int(accessTokenTTL.Seconds()),
})
}
func handleRefresh(w http.ResponseWriter, r *http.Request) {
var input struct {
RefreshToken string `json:"refreshToken"`
}
json.NewDecoder(r.Body).Decode(&input)
store.mu.Lock()
defer store.mu.Unlock()
data, ok := store.refreshTokens[input.RefreshToken]
if !ok || time.Now().After(data.ExpiresAt) {
writeJSON(w, 401, map[string]string{"error": "Invalid or expired refresh token"})
return
}
delete(store.refreshTokens, input.RefreshToken) // rotate
user := store.users[data.UserID]
if user == nil {
writeJSON(w, 401, map[string]string{"error": "User not found"})
return
}
accessToken := signJWT(user.ID, user.Email, user.Role, accessTokenTTL)
newRefresh := generateRefreshToken()
store.refreshTokens[newRefresh] = RefreshTokenData{
UserID: user.ID, ExpiresAt: time.Now().Add(refreshTokenTTL),
}
writeJSON(w, 200, map[string]interface{}{
"accessToken": accessToken, "refreshToken": newRefresh,
"expiresIn": int(accessTokenTTL.Seconds()),
})
}
// --- Helpers ---
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func generateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func generateRefreshToken() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/auth/register", handleRegister)
mux.HandleFunc("/auth/login", handleLogin)
mux.HandleFunc("/auth/refresh", handleRefresh)
mux.HandleFunc("/api/profile", requireAuth(func(w http.ResponseWriter, r *http.Request) {
store.mu.RLock()
user := store.users[r.Header.Get("X-User-ID")]
store.mu.RUnlock()
if user == nil {
writeJSON(w, 404, map[string]string{"error": "User not found"})
return
}
writeJSON(w, 200, user)
}))
mux.HandleFunc("/api/admin/users", requirePerm(ManageUsers, func(w http.ResponseWriter, _ *http.Request) {
store.mu.RLock()
defer store.mu.RUnlock()
var list []User
for _, u := range store.users {
list = append(list, *u)
}
writeJSON(w, 200, map[string]interface{}{"users": list})
}))
log.Println("Auth server on http://localhost:3000")
log.Fatal(http.ListenAndServe(":3000", mux))
}Key Takeaways
- Use timing-safe comparison for signature verification — prevents timing attacks
- Rotate refresh tokens on every use — if one is stolen, it can only be used once
- Return the same error for “user not found” and “wrong password” — prevents user enumeration
- RBAC is simpler than ABAC (attribute-based) and sufficient for most applications
- Hash passwords with bcrypt (Go) or scrypt (Node.js) — never SHA-256 or MD5
Real-World Usage
- Auth0 and Firebase Auth implement exactly this JWT + refresh token flow
- GitHub uses short-lived tokens with refresh for their OAuth apps
- Stripe uses API keys (simpler than JWT) because they don’t need user sessions
- For most apps, use a managed auth service (Auth0, Clerk, Supabase Auth). Build custom only if you have specific compliance needs.