Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/oidc-initial-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@exortek/oidc': major
---

Initial release of `@exortek/oidc` — OpenID Connect Core 1.0 on top of `@exortek/oauth2`.

- **Relying party** (`@exortek/oidc/client`) — a discovery-first `createClient` that enforces the `openid` scope and reuses oauth2's verified authorization-code flow: `authorize()` (with OIDC auth-request params), `handleCallback()` returning `{ idToken, claims, userinfo }` with the id_token verified end to end (`iss`/`aud`/`nonce`/`exp`, `azp`, `at_hash`), plus `endSessionUrl()` (RP-Initiated Logout 1.0), `refresh` and `revoke`.
- **OpenID Provider** (`@exortek/oidc/provider`) — add-ons to mount beside an `@exortek/oauth2/server` authorization server: `discoveryHandler` (a full `/.well-known/openid-configuration`), `userinfoHandler` (OIDC Core §5.3 with scope→claims release and a `claims.userinfo` policy), `jwksHandler`, `endSessionHandler` (validates `post_logout_redirect_uri`), `checkSessionHandler` + `sessionState()` (Session Management 1.0), and an `idTokenSigner` reusing oauth2's `createIdTokenSigner`.
- **Framework adapters** — `@exortek/oidc/client/express`, `/client/fastify`, `/provider/express`, `/provider/fastify`, with `express` / `fastify` as optional peers.

Server-only, built on `node:crypto`; runtime deps are `@exortek/oauth2`, `@exortek/jwt`, `@exortek/jwks`.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ body:
- '@exortek/passkey'
- '@exortek/paseto'
- '@exortek/oauth2'
- '@exortek/oidc'
- 'Repo tooling (build, tests, CI, docs site)'
- "Other / I don't know"
validations:
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ body:
- '@exortek/passkey'
- '@exortek/paseto'
- '@exortek/oauth2'
- '@exortek/oidc'
- 'A new package (please describe below)'
- 'Repo tooling / docs site'
validations:
Expand Down
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ doesn't apply — a "🟢 N/A" is fine, empty checkbox lists are noise.
- [ ] `@exortek/passkey`
- [ ] `@exortek/paseto`
- [ ] `@exortek/oauth2`
- [ ] `@exortek/oidc`
- [ ] Repo tooling (build, CI, docs site, lint/format)
- [ ] Docs only (no source code changed)

Expand Down
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Status legend: ✅ shipped to npm · 🛠 on disk, pre-release · ⏳ planned.
| 16 | `@exortek/opaque` | ✅ | Opaque reference tokens, RFC 7662 introspection + RFC 7009 revocation |
| 17 | `@exortek/paseto` | ✅ | PASETO v4 — `v4.local` (XChaCha20 + keyed BLAKE2b) + `v4.public` (Ed25519), no `alg` header |
| 18 | `@exortek/oauth2` | ✅ | OAuth 2.1 — RP flow + provider presets + full authorization server (DPoP · PAR · JAR/JARM · device · token-exchange · dynamic registration · opt-in OIDC id_token · FAPI) |
| 19 | `@exortek/oidc` | ⏳ | OpenID Connect on top of `@exortek/oauth2` |
| 19 | `@exortek/oidc` | ✅ | OpenID Connect Core 1.0 on `@exortek/oauth2` — discovery-first RP + OP add-ons (discovery · UserInfo · JWKS · RP-Initiated Logout · Session Management) |
| 20 | `@exortek/auth` | ⏳ | Umbrella — re-exports every package above |

Not versioned in this table: `@exortek/shared` — internal consolidation
Expand Down Expand Up @@ -303,7 +303,7 @@ Where each protocol is anchored:
| `session` | OWASP ASVS 4.0.3 V3, RFC 6265 (Cookies) |
| `security` | OWASP ASVS 4.0.3 V13 / V14, RFC 6749 §10 (OAuth2 threats), RFC 7231 §5 (HTTP) |
| `oauth2` | OAuth 2.1 / RFC 9700 (BCP), RFC 6749, RFC 7636 (PKCE), RFC 9207 (iss), RFC 8414 (metadata), RFC 9449 (DPoP), RFC 9126 (PAR), RFC 8707 (resource), RFC 9396 (RAR), RFC 9101 (JAR/JARM), RFC 7523 / 8705 (client auth), RFC 8693 (exchange), RFC 8628 (device), RFC 7591 (dynamic registration), RFC 9068 (JWT profile), OpenID Connect Core (id_token, opt-in OP mode), FAPI 2.0 |
| `oidc` | _(planned)_ OpenID Connect Core 1.0, OpenID Connect Discovery |
| `oidc` | OpenID Connect Core 1.0, Discovery 1.0, RP-Initiated Logout 1.0, Session Management 1.0 (on `@exortek/oauth2`) |
| `passkey` | W3C WebAuthn Level 3, FIDO2 CTAP2 |

For deeper per-package interface tables (JSDoc typedefs, worked API
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ packages under one scope. Every package is built on `node:crypto`, ships secure-
[![license](https://img.shields.io/github/license/ExorTek/auth?style=flat-square&color=blue)](./LICENSE)
[![docs](https://img.shields.io/badge/docs-auth.memet.dev-cb3837?style=flat-square&logo=readthedocs&logoColor=white)](https://auth.memet.dev)

**18 of 20 packages published** · [Documentation](https://auth.memet.dev) · [Guides](https://auth.memet.dev/guides) · [Comparison](https://auth.memet.dev/comparison)
**19 of 20 packages published** · [Documentation](https://auth.memet.dev) · [Guides](https://auth.memet.dev/guides) · [Comparison](https://auth.memet.dev/comparison)

## Why

Expand Down Expand Up @@ -53,7 +53,7 @@ Some packages pull in **optional peers** only when you use a feature that needs
| Peer | Needed for | Packages |
|------|------------|----------|
| `ioredis` **or** `redis` | multi-process stores | apikey · magic-link · opaque · passkey · paseto · session · jwt · oauth2 · security |
| `express` **or** `fastify` | middleware adapters | apikey · opaque · passkey · ua · security · session · oauth2 |
| `express` **or** `fastify` | middleware adapters | apikey · opaque · passkey · ua · security · session · oauth2 · oidc |
| `argon2` / `bcrypt` | those hash algorithms | password |

## Packages
Expand Down Expand Up @@ -81,7 +81,7 @@ time. Linked names are **published on npm**; the rest are planned.
| 16 | [`@exortek/opaque`](https://auth.memet.dev/opaque) | [![v](https://img.shields.io/npm/v/@exortek/opaque?style=flat-square&color=07d600&label=)](https://www.npmjs.com/package/@exortek/opaque) | opaque reference tokens, RFC 7662 introspection + RFC 7009 revocation handlers |
| 17 | [`@exortek/paseto`](https://auth.memet.dev/paseto) | [![v](https://img.shields.io/npm/v/@exortek/paseto?style=flat-square&color=07d600&label=)](https://www.npmjs.com/package/@exortek/paseto) | PASETO v4 — `v4.local` (XChaCha20 + BLAKE2b) · `v4.public` (Ed25519), `tokenPair` reuse detection |
| 18 | [`@exortek/oauth2`](https://auth.memet.dev/oauth2) | [![v](https://img.shields.io/npm/v/@exortek/oauth2?style=flat-square&color=07d600&label=)](https://www.npmjs.com/package/@exortek/oauth2) | OAuth 2.1 — `createOAuth` RP flow + 18 provider presets, login middleware, full authorization server (DPoP · PAR · PKCE · JAR/JARM · device · token-exchange · FAPI) |
| 19 | `@exortek/oidc` | _planned_ | OpenID Connect on top of `oauth2` |
| 19 | [`@exortek/oidc`](https://auth.memet.dev/oidc) | [![v](https://img.shields.io/npm/v/@exortek/oidc?style=flat-square&color=07d600&label=)](https://www.npmjs.com/package/@exortek/oidc) | OpenID Connect Core 1.0 on top of `oauth2` — discovery-first RP + OpenID Provider add-ons (discovery · UserInfo · JWKS · RP-Initiated Logout · Session Management) |
| 20 | `@exortek/auth` | _planned_ | umbrella — re-exports every package above |

## Documentation
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ lines are not patched unless the project has an explicit LTS commitment (none do
| `@exortek/passkey` | `1.x` — current |
| `@exortek/paseto` | `1.x` — current |
| `@exortek/oauth2` | `1.x` — current |
| `@exortek/oidc` | `1.x` — current |

Everything else in the roadmap is **not yet published** — file bug reports through the usual template once a version is
out.
Expand Down
15 changes: 15 additions & 0 deletions docs/compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ never transmit plaintext passwords over the network.
| RFC 7009 / 7662 | Revocation + introspection, no cross-client oracle | ✅ | Revoke is idempotent; introspection denies cross-client by default (`allowCrossClient` opt-in) |
| FAPI 2.0 | PAR + PKCE + DPoP/mTLS + `iss` in one profile | ✅ | `security: { fapi: true }` tightens the defaults together |

## OpenID Connect (`@exortek/oidc`)

| § | Requirement | Status | How |
|------------------|--------------------------------------------------------------------|:------:|--------------------------------------------------------------------------------------------------|
| OIDC Core §3.1.3.7 | RP `id_token` validation (`iss`/`aud`/`nonce`/`exp`, `azp`, `at_hash`) | ✅ | `createClient` reuses oauth2's verified flow; `openid` scope is non-optional |
| OIDC Core §2 | OP `id_token` issuance — signed JWS with `nonce` / `auth_time` / `at_hash` | ✅ | `provider.idTokenSigner` (oauth2 `createIdTokenSigner`), asymmetric alg only, never `none`/HS* |
| OIDC Core §5.3 / §5.4 | UserInfo endpoint — `sub` always, claims released per granted scope | ✅ | `userinfoHandler` — Bearer resolve, scope→claims map + `claims.userinfo` policy, 401 on bad token |
| OIDC Discovery 1.0 | `/.well-known/openid-configuration` metadata | ✅ | `discoveryHandler` — superset of RFC 8414, advertises only endpoints the OP actually serves |
| RP-Initiated Logout 1.0 | `end_session_endpoint`, validated `post_logout_redirect_uri` | ✅ | `endSessionHandler` exact-matches registered URIs before redirect (open-redirect lever); `endSessionUrl` on the RP |
| Session Management 1.0 | `session_state` + `check_session_iframe` | ✅ | `sessionState()` (§4.2 hash) + `checkSessionHandler` serving the OP iframe |

## Summary — what we ship today

- ✅ **NIST SP 800-63B AAL2** — memorized secret + OOB OTP paths
Expand All @@ -200,6 +211,10 @@ never transmit plaintext passwords over the network.
`@exortek/oauth2` (mandatory PKCE, `iss`, DPoP incl. nonce, PAR,
RAR, JAR/JARM, resource indicators, token exchange, device grant,
FAPI 2.0; RP flow + authorization server; see table above)
- ✅ **OpenID Connect Core 1.0 + Discovery / RP-Initiated Logout /
Session Management** — `@exortek/oidc` (discovery-first RP with full
`id_token` validation; OpenID Provider add-ons: discovery, UserInfo,
JWKS, logout, session; see table above)
- ✅ **ASVS V2.9 cryptographic authenticators** — `@exortek/passkey`
(WebAuthn L3 / FIDO2 CTAP2 server verification, all seven
attestation formats)
Expand Down
1 change: 1 addition & 0 deletions packages/oidc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @exortek/oidc
143 changes: 143 additions & 0 deletions packages/oidc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# @exortek/oidc

> OpenID Connect Core 1.0 for Node.js 22+ — the identity layer on top of **[`@exortek/oauth2`](../oauth2)**. Relying party + OpenID Provider, discovery, UserInfo, RP-Initiated Logout, Session Management. Server-only, built on `node:crypto`.

[![npm](https://img.shields.io/npm/v/@exortek/oidc.svg?color=cb3837)](https://www.npmjs.com/package/@exortek/oidc)
[![tests](https://github.com/ExorTek/auth/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ExorTek/auth/actions/workflows/ci.yml)
[![node](https://img.shields.io/node/v/@exortek/oidc.svg?color=339933)](https://nodejs.org)
[![install size](https://packagephobia.com/badge?p=@exortek/oidc)](https://packagephobia.com/result?p=@exortek/oidc)
[![types](https://img.shields.io/badge/types-included-3178C6)](./dist/index.d.ts)
[![license](https://img.shields.io/npm/l/@exortek/oidc.svg?color=blue)](https://github.com/ExorTek/auth/blob/master/LICENSE)

`@exortek/oauth2` already speaks OAuth 2.1 and can issue an `id_token`.
`@exortek/oidc` makes **OpenID Connect** the first-class shape on both
sides of the exchange: a discovery-first relying-party client that enforces
the `openid` scope and validates the `id_token` end to end, and an OpenID
Provider add-on that serves discovery, UserInfo, logout and session
management beside your `@exortek/oauth2/server` authorization server.

📖 **Docs:** [**auth.memet.dev/oidc**](https://auth.memet.dev/oidc)

## Why

- **`@exortek/oauth2`** gives you the OAuth 2.1 machinery — PKCE, `state`,
`nonce`, DPoP, PAR, the authorization server. OIDC is the identity
contract layered on it: *who the user is*, proven by a signed `id_token`
and a UserInfo endpoint.
- Bolting OIDC onto a generic OAuth client by hand is where the subtle bugs
live — a skipped `nonce` check, an unvalidated `aud`, trusting UserInfo
`sub` over the `id_token` `sub`, a `post_logout_redirect_uri` open
redirect. This package refuses to let the caller skip them, and reuses
oauth2's verified flow rather than reimplementing it.

## Modules

| Import | Purpose |
|--------|---------|
| `@exortek/oidc` | Barrel — `createClient`, `createProvider`, `ErrorCode`, `OidcError`. |
| `@exortek/oidc/client` | Relying-party (SSO) client. |
| `@exortek/oidc/client/express` · `/client/fastify` | Browser-login route adapters. |
| `@exortek/oidc/provider` | OpenID Provider add-ons (discovery / UserInfo / JWKS / logout / session). |
| `@exortek/oidc/provider/express` · `/provider/fastify` | Mount the provider endpoints. |

## Install

```bash
npm install @exortek/oidc
```

## Relying party (SSO)

```js
import { createClient } from '@exortek/oidc/client';

const client = createClient({
issuer: 'https://accounts.google.com',
clientId: '...',
clientSecret: '...',
redirectUri: 'https://myapp.com/callback',
scope: ['openid', 'email', 'profile'],
});

// 1. Start — redirect the user to `url`, keep `session` (cookie / store).
const { url, session } = await client.authorize({ prompt: 'login' });

// 2. Callback — id_token signature / iss / aud / nonce are verified inside.
const { idToken, claims, userinfo } = await client.handleCallback(req.query, { session });

// 3. Logout (RP-Initiated Logout 1.0)
const logoutUrl = await client.endSessionUrl({
idTokenHint: idToken,
postLogoutRedirectUri: 'https://myapp.com/',
state: 'xyz',
});
```

With Express, the browser flow is two routes:

```js
import { mountOidcLogin } from '@exortek/oidc/client/express';

mountOidcLogin(app, {
client,
onSuccess: ({ res, claims }) => {
req.session.user = claims.sub;
res.redirect('/');
},
});
```

## OpenID Provider

`createProvider` supplies the OIDC endpoints to mount **beside** your
`@exortek/oauth2/server` `createServer` (which already issues the
`id_token` off the `openid` scope):

```js
import { createProvider } from '@exortek/oidc/provider';
import { mountOidcProvider } from '@exortek/oidc/provider/express';

const provider = createProvider({
issuer: 'https://auth.myapp.com',
signing: { key: signingPrivateJwk, alg: 'ES256', kid: 'key-1' },
jwks: [publicJwk], // published at jwks_uri
endpoints: { authorization: '/authorize', token: '/token' },
claims: {
supported: ['sub', 'email', 'email_verified', 'name', 'picture'],
userinfo: ['email', 'name', 'picture'],
},
userinfo: {
// turn a Bearer access token into { sub, scope, claims }
resolve: accessToken => introspect(accessToken),
},
logout: { postLogoutRedirectUris: ['https://myapp.com/'] },
session: { cookieName: 'op_browser_state' },
});

mountOidcProvider(app, provider);
// → /.well-known/openid-configuration, /.well-known/jwks.json,
// /userinfo, /end_session, /check_session
```

Handlers are framework-agnostic (`{ method, url, headers, query } →
{ status, headers, body }`) — mount them by hand, or use the express /
fastify adapters. `provider.idTokenSigner` is the same signer your
`createServer` should use, so both sign with one key.

## Why not just `@exortek/oauth2`?

Use `@exortek/oauth2` on its own when you need access-token authorization
and nothing more. Reach for `@exortek/oidc` the moment you need
*authentication* — a verified end-user identity — with the OIDC Core
guarantees (nonce, `aud`, `sub` binding, logout, session) enforced rather
than reassembled per app.

## Specifications

- OpenID Connect Core 1.0 · Discovery 1.0 · RP-Initiated Logout 1.0 ·
Session Management 1.0
- Builds on OAuth 2.1 (`@exortek/oauth2`), RFC 7519 (JWT), RFC 7517 (JWK).

## License

MIT © [ExorTek](https://github.com/ExorTek)
115 changes: 115 additions & 0 deletions packages/oidc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"name": "@exortek/oidc",
"version": "0.0.0",
"description": "OpenID Connect Core 1.0 for Node.js 22+ — the identity layer on top of @exortek/oauth2. A relying-party client (id_token + UserInfo, discovery, nonce/state) and an OpenID Provider (discovery metadata, UserInfo endpoint, id_token issuance). Server-only, built on node:crypto.",
"type": "module",
"sideEffects": false,
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.mjs",
"require": "./dist/client/index.cjs"
},
"./client/express": {
"types": "./dist/client/express.d.ts",
"import": "./dist/client/express.mjs",
"require": "./dist/client/express.cjs"
},
"./client/fastify": {
"types": "./dist/client/fastify.d.ts",
"import": "./dist/client/fastify.mjs",
"require": "./dist/client/fastify.cjs"
},
"./provider": {
"types": "./dist/provider/index.d.ts",
"import": "./dist/provider/index.mjs",
"require": "./dist/provider/index.cjs"
},
"./provider/express": {
"types": "./dist/provider/express.d.ts",
"import": "./dist/provider/express.mjs",
"require": "./dist/provider/express.cjs"
},
"./provider/fastify": {
"types": "./dist/provider/fastify.d.ts",
"import": "./dist/provider/fastify.mjs",
"require": "./dist/provider/fastify.cjs"
}
},
"files": [
"dist",
"/README.md",
"/CHANGELOG.md",
"/LICENSE"
],
"scripts": {
"build": "rm -rf dist tsconfig.tsbuildinfo && rollup -c rollup.config.js && tsc -p tsconfig.json && rollup -c ../../rollup.dts.config.mjs",
"build:watch": "rollup -c rollup.config.js --watch",
"build:types": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --test 'tests/**/*.test.js'",
"test:coverage": "node --test --experimental-test-coverage 'tests/**/*.test.js'",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"prepack": "cp ../../LICENSE ./LICENSE"
},
"keywords": [
"backend",
"security",
"oidc",
"openid",
"openid-connect",
"id-token",
"userinfo",
"discovery",
"oauth2",
"sso",
"single-sign-on",
"authentication",
"identity",
"node-crypto"
],
"license": "MIT",
"homepage": "https://github.com/ExorTek/auth/tree/master/packages/oidc#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ExorTek/auth.git",
"directory": "packages/oidc"
},
"bugs": {
"url": "https://github.com/ExorTek/auth/issues"
},
"dependencies": {
"@exortek/jwks": "workspace:^",
"@exortek/jwt": "workspace:^",
"@exortek/oauth2": "workspace:^"
Comment on lines +89 to +92
},
"devDependencies": {
"@exortek/jwk": "workspace:^"
},
"peerDependencies": {
"express": ">=4.0.0",
"fastify": ">=4.0.0"
},
"peerDependenciesMeta": {
"express": {
"optional": true
},
"fastify": {
"optional": true
}
},
"engines": {
"node": ">=22.0.0"
},
"publishConfig": {
"access": "public"
}
}
Loading