logo
0
0
WeChat Login

OIDC Provider

Lightweight OpenID Connect provider on EdgeOne Pages (Next.js 16 App Router) with EdgeOne Blob storage. Implements the Authorization Code + PKCE flow with RS256-signed ID/Access tokens and rotating refresh tokens.

CNB Issue: Bring/ToDoList#37 — Phase 1 (OIDC core + Blob storage).

Architecture

app/
  .well-known/openid-configuration   discovery metadata
  .well-known/jwks.json              public signing keys (current + archive)
  oauth2/authorize                   authorization endpoint (PKCE S256 enforced)
  oauth2/token                       authorization_code + refresh_token grants
  oauth2/userinfo                    bearer-protected claims
  login + login/callback/[provider]  placeholder login; reserved federated callback
  api/admin/clients[/:id]            client CRUD (ADMIN_TOKEN)
  api/admin/seed                     bootstrap demo client/user + signing key
lib/
  storage/   storage-agnostic repository interfaces + EdgeOne Blob adapter
  oidc/      keys, tokens, PKCE, grants, discovery, scopes
  auth/      Authenticator registry, password provider, session, admin guard
  notify/    Notifier interface (phase-2 placeholder)

Storage model. Each data domain has its own Blob namespace (<BLOB_STORE_NAME>-users, -clients, -sessions, -authcodes, -refresh, -jwks, -bindings, -media) for independent quota/migration/access — see lib/storage/namespaces.ts. EdgeOne Blob is object storage with no native TTL, so every value is wrapped in an envelope with an optional expiresAt; reads validate it and lazily delete expired entries. Authorization codes use strong-consistency read + immediate delete (single-use). The OIDC core depends only on the interfaces in lib/storage/types.ts, so the backend is swappable.

Runtime. The EdgeOne Blob SDK is Node-only, so every route runs on the Next.js nodejs runtime (Edge runtime is intentionally not used).

Local development

Run via the EdgeOne CLI so the Pages runtime injects the Blob deploy credential — no token needed. (Plain next dev is not a Pages runtime; only then set EDGEONE_PROJECT_ID + EDGEONE_BLOB_TOKEN in .env.local for token mode. Production on EdgeOne Pages injects the credential automatically.)

corepack enable
pnpm install
cp .env.example .env.local        # set ADMIN_TOKEN / SESSION_SECRET; leave
                                  # EDGEONE_* empty; OIDC_ISSUER=http://localhost:8088
pnpm exec edgeone pages dev       # http://localhost:8088 (Pages runtime)
ADMIN_TOKEN=dev-admin-token SEED_BASE_URL=http://localhost:8088 pnpm seed

pnpm seed POSTs to /api/admin/seed (default http://localhost:8088; set SEED_BASE_URL to override). It prints the demo client/user credentials.

Verify the flow

# CLIENT_ID / CLIENT_SECRET are printed by `pnpm seed` (randomly generated).
CV=$(openssl rand -base64 60 | tr -d '\n+/=' | cut -c1-43)
CC=$(printf %s "$CV" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=')

open "http://localhost:8088/oauth2/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http://localhost:8080/callback&scope=openid%20profile%20email&state=xyz&code_challenge=$CC&code_challenge_method=S256"
# log in as demo / demo-password, copy `code` from the callback URL

curl -s -X POST http://localhost:8088/oauth2/token \
  -d grant_type=authorization_code -d code=$CODE \
  -d redirect_uri=http://localhost:8080/callback \
  -d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET -d code_verifier=$CV | jq

curl -s http://localhost:8088/oauth2/userinfo -H "Authorization: Bearer $ACCESS_TOKEN" | jq
curl -s http://localhost:8088/.well-known/openid-configuration | jq

Automated end-to-end coverage: pnpm test (drives the full code+PKCE flow, single-use code, refresh rotation, PKCE failure — Blob mocked in-memory for tests only; production keeps no local fallback).

Extending (later phases)

Login methods are pluggable via Authenticator (lib/auth/authenticator.ts). To add SMS / Email OTP / third-party OAuth / Passkey:

  1. Implement Authenticator (begin, optional complete).
  2. Register it in lib/auth/register.ts.
  3. For OTP/alerts, implement a Notifier (lib/notify/notifier.ts) and register it; OTP authenticators depend on the interface only.
  4. Federated providers use the reserved app/login/callback/[providerId]/route.ts endpoint.

The OIDC core, token issuance and storage layer require no changes.

Endpoints

EndpointMethodNotes
/.well-known/openid-configurationGETdiscovery
/.well-known/jwks.jsonGETpublic keys
/oauth2/authorizeGETPKCE S256 required
/oauth2/tokenPOSTauthorization_code, refresh_token
/oauth2/userinfoGET/POSTBearer access token
/oauth2/logoutGET/POSTRP-initiated logout (end_session_endpoint)
/consentGETconsent screen (shown by /authorize when no covering grant)
/oauth/error · /logged-outGETstyled OAuth error / logged-out pages
/login · /accountGETEmail/SMS OTP login (first login auto-creates the account) · account center
/api/otp/sendPOSTissue + dispatch a login OTP
/admin/clients · /admin/users · /admin/providersGETadmin console — applications / users / login methods. Auth = OIDC session with role: admin (the first-ever user bootstraps as admin); non-admins get 403
/api/admin/clientsGET/POSTAuthorization: Bearer $ADMIN_TOKEN (machine API)
/api/admin/clients/:idGET/PUT/DELETEsame
/api/admin/rotate-keysPOSTrotate signing key (Bearer ADMIN_TOKEN)
/api/admin/seedPOSTbootstrap (dev, needs ALLOW_SEED=1)

OTP & notification providers

Email/SMS login uses a 6-digit OTP (5-min TTL, 60s resend cooldown, 5-attempt cap, single-use) stored hashed in the *-otp namespace. Delivery providers are configured at /admin/providers (stored in *-settings, per-channel enable/disable):

  • Email: smtp (nodemailer) · tencent-ses (SES SendEmail) · aliyun-dm (DirectMail SingleSendMail)
  • SMS: aliyun (Dysmsapi SendSms) · aliyun-nac (Dypnsapi SendSmsVerifyCode) · tencent (SMS SendSms)

Disabled / console → the code is logged to the server console (dev). Cloud adapters are credential-driven (entered in the admin UI, stored in Blob); signing is implemented per each vendor's docs but untested without live credentials.

Operations

Key rotation. POST /api/admin/rotate-keys (Bearer ADMIN_TOKEN) generates a new RS256 key and archives the old public key so in-flight tokens still verify. Run it periodically — e.g. a CNB pipeline crontab trigger that curls the endpoint, or any external scheduler.

RP-initiated logout. Clients send the user to /oauth2/logout with optional id_token_hint, post_logout_redirect_uri (must be registered in the client's postLogoutRedirectUris) and state. The session is destroyed; with a valid return URI the user is redirected back, otherwise shown /logged-out.

UI / design system

The pages use a shared dark-navy + violet design system from a Claude Design handoff: tokens and reusable classes in app/globals.css (.btn, .input, .panel, .chip, .scope-row, the .oidc-page frame, ...) plus components/oidc/ (PageFrame, Icon). Restyle by editing tokens, not per-page. The Authorization Code flow now includes a real /consent step; approved grants are stored (ConsentGrant) and re-skipped, and are revocable from the account center.

About

No description, topics, or website provided.
Language
TypeScript95.6%
CSS4.2%
JavaScript0.2%