@lpshanley/oidc-start-core 0.1.0-next.0 → 0.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +93 -9
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,25 +1,109 @@
1
1
  # @lpshanley/oidc-start-core
2
2
 
3
- OpenID Connect for a Node.js server. Handles discovery, code exchange with PKCE, sealed browser sessions, refresh, revocation and back-channel logout. Use the separate `@lpshanley/oidc-start-react` package for TanStack Start.
3
+ OpenID Connect sign-in for Node.js servers. Give the core a standard `Request` and it handles provider discovery, the authorization code exchange with PKCE, and sealed session cookies. Your app decides who can sign in and where to record revoked sessions.
4
+
5
+ Building a TanStack Start app? The React adapter connects the core to Start’s request middleware, routes, and server functions. [TanStack Start quickstart](https://www.npmjs.com/package/@lpshanley/oidc-start-react)
6
+
7
+ ## Install and register your app
8
+
9
+ Use Node.js 22.12 or newer with ES modules. Install the current prerelease from npm.
10
+
11
+ ```sh
12
+ npm install @lpshanley/oidc-start-core@next
13
+ ```
14
+
15
+ Register a **confidential web client** with your OIDC provider. For the example below, register `http://localhost:5173/callback` as the sign-in redirect URI and `http://localhost:5173/` as the post-logout redirect URI. In production, use your app’s public HTTPS origin for both.
16
+
17
+ | Server setting | What to supply |
18
+ | --- | --- |
19
+ | `APP_ORIGIN` | The public origin of your app, such as `http://localhost:5173` in development. |
20
+ | `OIDC_ISSUER` | The provider’s issuer URL, including its tenant path if it has one. |
21
+ | `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` | The credentials issued to your web client. |
22
+ | `SESSION_SECRET` | Generate once with `openssl rand -hex 32`. Keep the same secret across restarts and server instances. |
23
+ | `OIDC_SCOPES` | Optional. Defaults to `openid profile email offline_access`. The provider must supply an email in the ID token or through UserInfo. |
24
+ | `OIDC_ROLES_CLAIM` | Optional. Defaults to `roles`. Set this to the claim your provider uses if your policy checks roles. |
25
+
26
+ ## Create the core
27
+
28
+ Create one instance in a server module. This example requires the `members` role, so give your test user that role at the provider. The core checks the policy at sign-in, when reading a session, and after refreshing it.
4
29
 
5
30
  ```ts
31
+ // auth.server.ts
6
32
  import { createOidcCore } from '@lpshanley/oidc-start-core'
7
33
  import { hasRole } from '@lpshanley/oidc-start-core/models'
34
+ import { revocations } from './revocations.js'
8
35
 
9
- const auth = createOidcCore({
36
+ export const auth = createOidcCore({
10
37
  cookiePrefix: 'myapp',
11
- callbackPath: '/auth/callback',
12
- postLogoutPath: '/signed-out',
38
+ callbackPath: '/callback',
39
+ postLogoutPath: '/',
13
40
  allowSession: user => hasRole(user, 'members'),
14
- revocations: mySessionRevocationStore,
41
+ revocations,
15
42
  })
43
+
16
44
  auth.assertConfigured()
17
45
  ```
18
46
 
19
- Provide `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and a `SESSION_SECRET` of at least 32 characters. Set `APP_ORIGIN` to the public app origin and `OIDC_ROLES_CLAIM` to the provider's role claim (default `roles`). Register the callback and post-logout URLs. Each app chooses its own role policy and supplies a `SessionRevocations` implementation shared by its server instances. The core checks that policy at sign-in, on session access and after refresh.
47
+ `./revocations.js` is an app module you supply. For a local trial, copy the three maps and the `revocations` object from the example into `revocations.ts` and export the object. A deployed app needs durable storage shared by every instance. [Local revocation store](https://github.com/lpshanley/oidc-start/blob/main/examples/tanstack-start/src/auth.server.ts)
48
+
49
+ ## Wire sign-in and callback
50
+
51
+ Mount these handlers at `GET /sign-in` and `GET /callback` in your server. The first sends the browser to the provider. The second checks the code and browser verifier, sets the session cookie, and returns the user to `/`.
52
+
53
+ ```ts
54
+ import { SessionAccessDeniedError } from '@lpshanley/oidc-start-core'
55
+ import { auth } from './auth.server.js'
56
+
57
+ function authHeaders(cookies: string[]) {
58
+ const headers = new Headers({ 'Cache-Control': 'no-store' })
59
+ for (const cookie of cookies) headers.append('Set-Cookie', cookie)
60
+ return headers
61
+ }
62
+
63
+ export async function signIn(request: Request) {
64
+ const { cookies, setCookies } = await auth.resolve(request)
65
+ const flow = await auth.signIn(request, cookies, '/')
66
+ const headers = authHeaders([...setCookies, ...flow.setCookies])
67
+ headers.set('Location', flow.url)
68
+ return new Response(null, { status: 302, headers })
69
+ }
70
+
71
+ export async function callback(request: Request) {
72
+ const { cookies, setCookies } = await auth.resolve(request)
73
+ const result = await auth.callback(request, cookies)
74
+ const headers = authHeaders([...setCookies, ...result.setCookies])
75
+ if (!result.ok) {
76
+ const denied = result.error instanceof SessionAccessDeniedError
77
+ return new Response(denied ? 'Access denied.' : 'Please sign in again.', {
78
+ status: denied ? 403 : 400, headers,
79
+ })
80
+ }
81
+ headers.set('Location', result.location)
82
+ return new Response(null, { status: 302, headers })
83
+ }
84
+ ```
85
+
86
+ On an ordinary request, `const { session, setCookies } = await auth.resolve(request)` gives you `session?.user`. Forward **each** returned `Set-Cookie` header separately, even on a signed-out response. A read can refresh a session or clear an expired cookie. Keep the full session on the server because it contains provider tokens. Browser code can use `AuthUser`, `displayName`, and `hasRole` from `@lpshanley/oidc-start-core/models`.
87
+
88
+ ## End a session
89
+
90
+ For browser sign-out, use a same-origin, CSRF-protected POST handler. Resolve the request, pass its `cookies` and `session` to `auth.signOut(request, cookies, session)`, append both sets of cookies as above, and redirect with **303** to the returned `location`. If recording the revocation throws, return a retryable error. The default session lasts up to 14 days and expires after 12 hours of inactivity.
91
+
92
+ Implement these four methods on your `SessionRevocations` store. All times are in epoch seconds. Let storage errors propagate so the core can distinguish an outage from an active session.
93
+
94
+ | Method | Storage rule |
95
+ | --- | --- |
96
+ | `revoke(sid, until)` | Mark this app session as revoked through its expiry. |
97
+ | `revokeIdpSession(idpSid, until)` | Mark the provider session as revoked. Atomically keep the later expiry. |
98
+ | `revokeAllFor(sub, cutoff)` | Atomically keep the greatest cutoff for the subject. |
99
+ | `isRevoked({ sid, idpSid, sub, issuedAt })` | Return true for a live revocation of either session ID, or when `issuedAt <= cutoff` for this subject. |
100
+
101
+ For provider-initiated logout, register a POST endpoint and pass its request to `auth.backchannelLogout(request)`. Return the resulting HTTP status to the provider. The core verifies the signed logout token, including its expiry and nonempty token ID. A redelivered event uses the original issue time. [Revocation interface](https://github.com/lpshanley/oidc-start/blob/main/packages/core/src/revocation.ts)
20
102
 
21
- Back-channel logout requires a signed token with an expiration (`exp`) and a nonempty token ID (`jti`). The signed issue time sets the revocation cutoff, in epoch seconds. For `revokeAllFor` and `revokeIdpSession`, atomically keep the greater of the stored and incoming timestamp so an older delivery cannot undo a newer revocation. A subject cutoff includes sessions issued in the same second.
103
+ ## Obtain a separate provider grant
22
104
 
23
- Use `startGrant` and `finishGrant` on the server to obtain an independent provider grant. Register a separate `grantCallbackPath` (default `/api/auth/grant-callback`). The sealed state binds each flow to its purpose. The host checks the returned subject and permissions, stores the credentials and decides which client can use them. `refreshGrant` returns `refused` for a provider rejection of the grant. Other errors propagate. A `RefreshNotDispatched` from `/provider` means the refresh request was never sent.
105
+ Use this flow when your app needs credentials for provider API calls independently of the browser session. Register a second redirect URI with the provider. The default path is `/api/auth/grant-callback`, configurable with `grantCallbackPath`.
24
106
 
25
- The `/models` entry is safe to import in browser code. Credentials and session management belong on the server.
107
+ 1. Call `auth.startGrant(request, cookies, returnPathname)`, send its cookies, and redirect to its `url`.
108
+ 2. In the grant callback, call `auth.finishGrant(request, cookies)` and forward its cookies on success or failure. On success, check the returned user and permissions before storing the tokens on the server.
109
+ 3. Call `auth.refreshGrant(refreshToken)` to renew the grant. A `refused` result means the provider rejected it. Other errors propagate. `RefreshNotDispatched` from `@lpshanley/oidc-start-core/provider` means the refresh request was never sent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lpshanley/oidc-start-core",
3
- "version": "0.1.0-next.0",
3
+ "version": "0.1.0-next.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lpshanley/oidc-start.git",