Skip to main content

Overview

The Auth Service is a Node.js/Express microservice that provides JWT-based authentication using RSA-256 signing. It supports user registration, login, token verification, and exposes a JWKS endpoint for public key distribution.
Source code: node-services/auth-service/app.jsEntry point: node-services/auth-service/server.js

Technology Stack

  • Language: Node.js
  • Framework: Express
  • Database: PostgreSQL via pg driver
  • Authentication: JWT with RSA-256 signing
  • Password Hashing: bcrypt
  • Observability: OpenTelemetry

Configuration

Environment Variables

PORT
string
default:"8090"
HTTP server port for the auth service
DATABASE_URL
string
PostgreSQL connection stringExample: postgresql://devuser:devpass@postgres:5432/auth_dbIf not provided, the service operates in legacy mode (JWT-only without database)
JWT_SECRET
string
default:"dev-secret"
Secret used for JWT signing (deprecated in favor of RSA keys)This is kept for backward compatibility but not used with RSA signing
OTEL_EXPORTER_OTLP_ENDPOINT
string
OpenTelemetry collector endpoint
OTEL_SERVICE_NAME
string
default:"auth-service"
Service name for distributed tracing

Docker Compose Configuration

auth-service:
  build:
    context: .
    dockerfile: deploy/docker/auth-service/Dockerfile
  environment:
    PORT: "8090"
    JWT_SECRET: "dev-secret"
    DATABASE_URL: "postgresql://devuser:devpass@postgres:5432/auth_db"
    OTEL_SERVICE_NAME: "auth-service"
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.auth.rule=PathPrefix(`/auth`)"
    - "traefik.http.services.auth.loadbalancer.server.port=8090"
  depends_on:
    - postgres

API Reference

POST /auth/register

Register a new user with email and password.
email
string
required
User’s email address (must be unique)
password
string
required
User’s password (will be hashed with bcrypt)
id
integer
User ID assigned by the database
email
string
Registered email address
role
string
User role (default: “user”)

Example Request

curl -X POST http://localhost:8090/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "secretpassword"
  }'

Example Response

{
  "id": 1,
  "email": "[email protected]",
  "role": "user"
}

Error Responses

  • 400 Bad Request: Missing email or password
  • 409 Conflict: Email already exists
  • 500 Internal Server Error: Database error
  • 503 Service Unavailable: Database not configured

POST /auth/login

Authenticate a user and receive a JWT token.
email
string
required
User’s email address
password
string
required
User’s password
token
string
JWT access token (RS256 signed, 24-hour expiry)

Example Request

curl -X POST http://localhost:8090/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "secretpassword"
  }'

Example Response

{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1Njc4OTAifQ..."
}

JWT Claims

The JWT token contains:
{
  "sub": "1",                    // User ID (or email in legacy mode)
  "role": "user",                // User role
  "iss": "auth-service",         // Issuer
  "exp": 1234567890,             // Expiry (24 hours)
  "iat": 1234567890              // Issued at
}

Error Responses

  • 400 Bad Request: Missing email or password
  • 401 Unauthorized: Invalid email or password
  • 500 Internal Server Error: Database error

GET /verify

Verify a JWT token and extract user information.
Authorization
string
required
Bearer token to verifyFormat: Bearer <token>
ok
boolean
Always true for successful verification
userId
string
User ID extracted from the token

Example Request

curl http://localhost:8090/verify \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1Njc4OTAifQ..."

Example Response

{
  "ok": true,
  "userId": "1"
}
The response also includes an X-User-Id header with the user ID.

Error Responses

  • 401 Unauthorized: Missing bearer token or invalid token

GET /.well-known/jwks.json

Expose public keys for JWT verification (JWKS endpoint).
keys
array
Array of JWK (JSON Web Key) objects

Example Request

curl http://localhost:8090/.well-known/jwks.json

Example Response

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "1234567890abcdef",
      "n": "xGOFj9YiW5...",
      "e": "AQAB"
    }
  ]
}
The JWKS endpoint allows other services to verify JWT tokens without shared secrets. Services can fetch the public key and validate tokens independently.

GET /healthz

Health check endpoint.

Example Request

curl http://localhost:8090/healthz

Example Response

ok

Error Responses

  • 503 Service Unavailable: Database health check failed

Implementation Details

RSA Key Generation

The service generates an RSA key pair at startup: From node-services/auth-service/app.js:12-17:
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
In production, keys should be loaded from Kubernetes Secrets or a secure key management service, not generated at runtime. Generating keys at startup means:
  • Keys change on every deployment
  • Existing tokens become invalid after restart
  • Multi-instance deployments have different keys

JWT Signing

From node-services/auth-service/app.js:102-109:
const token = jwt.sign(
  {
    sub: user.id,
    role: user.role,
  },
  privateKey,
  { algorithm: 'RS256', issuer: 'auth-service', expiresIn: '24h', keyid: kid }
);

Password Hashing

From node-services/auth-service/app.js:62-67:
const passwordHash = await bcrypt.hash(password, 10);
const result = await pool.query(
  'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email, role',
  [email, passwordHash]
);
Passwords are hashed using bcrypt with a cost factor of 10 (2^10 rounds).

Legacy Mode

If no database is configured, the service operates in legacy mode: From node-services/auth-service/app.js:84-92:
if (!pool) {
  const token = jwt.sign({ sub: email, role: 'user' }, privateKey, {
    algorithm: 'RS256',
    issuer: 'auth-service',
    expiresIn: '24h',
    keyid: kid,
  });
  return res.status(200).json({ token });
}
In legacy mode:
  • No user validation
  • Tokens are issued for any email/password combination
  • Useful for development without database

Database Schema

The service expects a users table (must be created manually or via migration):
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    role VARCHAR(50) NOT NULL DEFAULT 'user',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Unlike the Go services, the Node.js services don’t include automatic migrations. You need to create the table manually or add a migration system.

Integration with Traefik

The auth service is used as a middleware by Traefik for protecting other services. Traefik middleware configuration in deploy/traefik/dynamic/middlewares.yml:
http:
  middlewares:
    auth:
      forwardAuth:
        address: "http://auth-service:8090/verify"
        authResponseHeaders:
          - "X-User-Id"
Services that require authentication add the auth@file middleware:
labels:
  - "traefik.http.routers.greeter.middlewares=cors@file,auth@file,rate-limit@file"
When a request arrives:
  1. Traefik forwards the request to /verify with the Authorization header
  2. Auth service validates the JWT
  3. If valid, Traefik forwards the original request with X-User-Id header
  4. If invalid, Traefik returns 401 to the client

Security Considerations

Password Storage

  • Passwords are hashed with bcrypt (cost factor 10)
  • Never stored in plain text
  • Salt is automatically generated by bcrypt

JWT Security

  • Tokens signed with RSA-256 (asymmetric)
  • 24-hour expiration
  • Contains minimal claims (sub, role)
  • Public key available via JWKS endpoint

Production Recommendations

For production deployments:
  1. Load RSA keys from secrets: Don’t generate at runtime
  2. Use HTTPS: Never send tokens over HTTP
  3. Implement refresh tokens: 24-hour access tokens are too long
  4. Add rate limiting: Prevent brute-force attacks
  5. Implement account lockout: After N failed attempts
  6. Add email verification: Confirm email ownership
  7. Use strong password requirements: Minimum length, complexity
  8. Implement token revocation: Logout functionality
  9. Add CORS configuration: Restrict allowed origins
  10. Monitor failed login attempts: Detect attacks

Service Dependencies

Upstream Dependencies

  • PostgreSQL: Optional, service runs in legacy mode without DB

Downstream Consumers

  • Traefik: ForwardAuth middleware
  • Frontend: Login/register flows
  • All Protected Services: Via Traefik middleware

Testing

Test the service using curl:
# Health check
curl http://localhost:8090/healthz

# Register a user
curl -X POST http://localhost:8090/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "password123"}'

# Login
TOKEN=$(curl -X POST http://localhost:8090/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "password123"}' \
  | jq -r '.token')

echo "Token: $TOKEN"

# Verify token
curl http://localhost:8090/verify \
  -H "Authorization: Bearer $TOKEN"

# Get JWKS
curl http://localhost:8090/.well-known/jwks.json

# Test protected service (via Traefik)
curl http://localhost:30081/greeter.v1.GreeterService/Greet \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice"}'

Observability

Logging

The service logs errors using console.error:
console.error('register error:', err);
console.error('login error:', err);

Distributed Tracing

OpenTelemetry instrumentation captures:
  • HTTP requests
  • Database queries
  • JWT operations
  • Error tracking
Tracing is configured in node-services/auth-service/tracing.js.

Performance Characteristics

  • Register Latency: ~50-100ms (bcrypt hashing)
  • Login Latency: ~50-100ms (bcrypt comparison)
  • Verify Latency: ~1-5ms (JWT verification only)
  • JWKS Latency: Less than 1ms (returns cached key)
  • Database Pool: 10 connections

Common Issues

Database Not Configured

Log: Service runs but operates in legacy mode Solution: Set DATABASE_URL environment variable

Tokens Invalid After Restart

Cause: RSA keys regenerated on startup Solution: Load keys from persistent storage (Kubernetes Secrets)

Email Already Exists

Error: 409 Conflict: email already exists Cause: Duplicate registration attempt Solution: Return error to user or implement login flow

Invalid Token

Error: 401 Unauthorized: invalid token Causes:
  • Token expired (>24 hours old)
  • Token signed with different key (after restart)
  • Token format invalid
  • Token signature invalid

Services Overview

Learn about service architecture

Deployment

Deploy with authentication

Build docs developers (and LLMs) love