Skip to content

OAuth2 & OpenID Connect

Muljax ID provides an edge-native OpenID Connect Core 1.0 and OAuth 2.0 (RFC 6749) authorization server engineered for Cloudflare Workers. It implements cryptographic verification at rest, zero plaintext token persistence, timing-safe PKCE validation, and ECDSA P-256 (ES256) JSON Web Token (JWT) signing.


The OAuth2 / OIDC subsystem complies with the following RFCs and OpenID Foundation standards:

  • RFC 6749: The OAuth 2.0 Authorization Framework (Authorization Code, Refresh Token, and Client Credentials grants).
  • RFC 7636: Proof Key for Code Exchange by OAuth Public Clients (PKCE) (mandating code_challenge_method=S256).
  • OpenID Connect Core 1.0: ID Tokens, Userinfo claims, and authentication contexts.
  • RFC 7515: JSON Web Signature (JWS) (ECDSA P-256 with SHA-256).
  • RFC 7517: JSON Web Key (JWK) and JWKS endpoint specifications.
  • RFC 7519: JSON Web Token (JWT) claims and structure.
  • RFC 7009: OAuth 2.0 Token Revocation.
  • RFC 7662: OAuth 2.0 Token Introspection.
  • RFC 8414: OAuth 2.0 Authorization Server Metadata (/.well-known/openid-configuration).

Tokens and secrets are never persisted in plaintext. Every credential stored in Cloudflare D1 is transformed into a SHA-256 cryptographic digest before writing to the database:

flowchart LR
    Client["Client / Browser"]
    Worker["API Worker"]
    D1[("Cloudflare D1")]

    Client -->|"Plaintext Token (at_... / rt_...)"| Worker
    Worker -->|"SHA-256(Token)"| D1
    Worker -->|"Set-Cookie / JSON Body"| Client

Database Entities (apps/api/src/db/schema/)

Section titled “Database Entities (apps/api/src/db/schema/)”
Database Table Primary Identifier Security Transform at Rest Expiration / TTL
oauth_clients Muljax ID (client_id) Client secrets hashed with SHA-256 (client_secret_hash) Permanent until deleted
oauth_authorization_codes code_hash (PK) Ephemeral codes hashed with SHA-256 10 minutes (single-use enforced)
oauth_access_tokens token_hash (PK) Access tokens hashed with SHA-256 1 hour (3600 seconds)
oauth_refresh_tokens token_hash (PK) Refresh tokens hashed with SHA-256 30 days (revocable)
oauth_grants (user_id, client_id) JSON-encoded approved scopes Persisted until revoked by user

In compliance with RFC 7636 and current OAuth 2.1 recommendations, PKCE is enforced with code_challenge_method=S256. The legacy plain transform is rejected.

  1. The client generates an unguessable high-entropy cryptographic string V (code_verifier) containing between 43 and 128 characters from the unreserved set [A-Z, a-z, 0-9, -, ., _, ~].
  2. The client calculates the code challenge C:
C = BASE64URL(SHA256(ASCII(V)))
  1. The server validates the code verifier upon exchange:
apps/api/src/lib/oauth/pkce.ts
export async function createCodeChallenge(codeVerifier: string): Promise<string> {
const data = new TextEncoder().encode(codeVerifier);
const hash = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(hash));
}
export async function verifyCodeChallenge(
codeVerifier: string,
codeChallenge: string,
): Promise<boolean> {
const expected = await createCodeChallenge(codeVerifier);
return timingSafeEqual(expected, codeChallenge);
}

Validation uses constant-time comparison (timingSafeEqual) to eliminate side-channel timing attacks.


OpenID Connect ID Tokens are issued as compact JSON Web Signatures (JWS) signed using the server’s extractable ECDSA P-256 private key stored in terraform.tfvars.

  • Algorithm: ES256 (ECDSA using P-256 curve and SHA-256 hash).
  • Key Identifier (kid): "Muljax-id-1".
  • Key Type (kty): "EC".
  • Curve (crv): "P-256".
{
"alg": "ES256",
"typ": "JWT",
"kid": "Muljax-id-1"
}
{
"iss": "https://id-api.example.com",
"sub": "usr_01HXYZ123456789ABCDEF",
"aud": "client_abc123",
"iat": 1773752400,
"exp": 1773756000,
"nonce": "n-0S6_WzA2Mj",
"auth_time": 1773752390,
"acr": "urn:mace:incommon:iap:bronze"
}

In apps/api/src/lib/oauth/id-token.ts:

const encodedHeader = encodeJson(header);
const encodedPayload = encodeJson(payload);
const signingInput = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`);
const privateKey = await importPrivateKey(input.privateKey);
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
privateKey,
signingInput,
);
const encodedSignature = base64UrlEncode(new Uint8Array(signature));
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;

The resulting token is a triple of Base64URL strings separated by dots:

id_token = Base64Url(Header) . Base64Url(Payload) . Base64Url(Signature)

sequenceDiagram
    autonumber
    actor User as User (Browser)
    participant Client as Relying Party (Client)
    participant API as ID API Worker
    participant DB as Cloudflare D1

    Client->>User: 1. Redirect to /oauth/authorize (client_id, redirect_uri, code_challenge, S256, scope, state, nonce)
    User->>API: 2. GET /oauth/authorize?...
    API->>API: 3. Verify active session cookie & client registration
    API-->>User: 4. Display Scope Consent Modal (if not previously granted)
    User->>API: 5. Submit consent approval
    API->>DB: 6. INSERT into oauth_authorization_codes (code_hash, code_challenge, expires_at)
    API-->>Client: 7. 302 Found: redirect_uri?code=ac_...&state=...
    Client->>API: 8. POST /oauth/token (code, code_verifier, client_id, client_secret)
    API->>API: 9. verifyCodeChallenge(code_verifier, code_challenge)
    API->>DB: 10. Atomically consume code (DELETE/UPDATE) & INSERT tokens
    API-->>Client: 11. 200 OK: { access_token, id_token, refresh_token, token_type, expires_in }
    Client->>API: 12. GET /oauth/userinfo (Authorization: Bearer <access_token>)
    API->>DB: 13. Verify SHA-256(access_token) in oauth_access_tokens
    API-->>Client: 14. 200 OK: { sub, email, name, preferred_username }

Phase 1: Authorization Request (/oauth/authorize)

Section titled “Phase 1: Authorization Request (/oauth/authorize)”

The client redirects the user to the authorization endpoint:

GET /oauth/authorize?
response_type=code&
client_id=client_018f92&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
scope=openid%20profile%20email%20offline_access&
state=c29tZXN0YXRl&
code_challenge=E9Melhoa2OwvFrGMTJguCH5rtx64ZnPUq36127NWq2Y&
code_challenge_method=S256&
nonce=n-0S6_WzA2Mj
HTTP/1.1
Host: id-api.example.com

Validation Checks:

  1. client_id exists in oauth_clients.
  2. redirect_uri matches one of the registered URLs in oauth_clients.redirect_uris.
  3. code_challenge_method is strictly S256.
  4. Requested scope is a subset of oauth_clients.scopes.

The client exchanges the authorization code using an HTTP POST:

POST /oauth/token HTTP/1.1
Host: id-api.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id=client_018f92&
client_secret=sec_abcdef123456&
code=ac_e82f1b409c&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Response (200 OK):

{
"access_token": "at_9b2e04f128c701d4",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_81f09c88210e4a7b",
"id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ik11bGpheC1pZC0xIn0...",
"scope": "openid profile email offline_access"
}

6. Public Metadata & Cryptographic Discovery

Section titled “6. Public Metadata & Cryptographic Discovery”

Discovery Document (/.well-known/openid-configuration)

Section titled “Discovery Document (/.well-known/openid-configuration)”

Provides standard RFC 8414 server metadata:

{
"issuer": "https://id-api.example.com",
"authorization_endpoint": "https://id-api.example.com/oauth/authorize",
"token_endpoint": "https://id-api.example.com/oauth/token",
"userinfo_endpoint": "https://id-api.example.com/oauth/userinfo",
"jwks_uri": "https://id-api.example.com/.well-known/jwks.json",
"revocation_endpoint": "https://id-api.example.com/oauth/revoke",
"introspection_endpoint": "https://id-api.example.com/oauth/introspect",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["ES256"],
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "none"],
"code_challenge_methods_supported": ["S256"]
}

Exposes the public key used to verify id_token signatures:

{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"alg": "ES256",
"use": "sig",
"kid": "Muljax-id-1",
"x": "W4o3L8yv4N0jA8m-sR9Q7x1z2...",
"y": "K9p2M1n0B7v6C5x4Z3a2S1d0..."
}
]
}

When an OAuth 2.0 validation error occurs, the server returns structured JSON responses per RFC 6749 § 5.2:

Error Code HTTP Status Trigger Condition
invalid_request 400 Bad Request Missing required parameters (client_id, redirect_uri, or code_verifier).
invalid_client 401 Unauthorized Client authentication failed (invalid client_secret or unrecognized client_id).
invalid_grant 400 Bad Request Authorization code expired, already used, or PKCE verifier mismatch.
unauthorized_client 400 Bad Request Client not authorized to use the requested grant type.
unsupported_grant_type 400 Bad Request Grant type other than authorization_code, refresh_token, or client_credentials.
invalid_scope 400 Bad Request Requested scopes exceed client configuration or user approval.