@sourceregistry/sveltekit-oidc 2.0.2 → 2.1.0

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.
package/README.md CHANGED
@@ -56,7 +56,8 @@ export const oidc = createOIDC<Identity, RequestData>({
56
56
  }),
57
57
 
58
58
  beforeSessionPersist: async ({session, reason}) => {
59
- await synchronizeUser(session.identity, reason);
59
+ const identity = await synchronizeUser(session.identity, reason);
60
+ return {...session, identity};
60
61
  },
61
62
 
62
63
  loadRequestData: async ({session, event}) => ({
@@ -75,23 +76,30 @@ export const oidc = createOIDC<Identity, RequestData>({
75
76
 
76
77
  The extension points have deliberately literal names:
77
78
 
78
- | Extension point | When it runs | Persisted |
79
- | ---------------------- | --------------------------------------------------------------- | ----------------------- |
80
- | `resolveIdentity` | After provider data is validated, on login and refresh | Its result is persisted |
81
- | `beforeSessionPersist` | Immediately before a login or refreshed session is written | Side effects only |
82
- | `loadRequestData` | Once while `handle` builds an authenticated request context | Never |
83
- | `createPublicSession` | When `getPublicSession` or `toPublicSession` projects a session | Never |
79
+ | Extension point | When it runs | Persisted |
80
+ | ----------------------- | ----------------------------------------------------------------- | ------------------------------------------------ |
81
+ | `resolveIdentity` | After provider data is validated, on login and refresh | Its result is persisted |
82
+ | `beforeSessionPersist` | Immediately before a login or refreshed session is written | Returned session replaces it; `void` keeps it |
83
+ | `loadRequestData` | Once while `handle` builds an authenticated request context | Never |
84
+ | `createPublicSession` | When `getPublicSession` or `toPublicSession` projects a session | Never |
84
85
 
85
- Both login and refresh are explicit in the callback context:
86
+ Both login and refresh are explicit in the callback context. Returning a session from
87
+ `beforeSessionPersist` is what makes it the right place to provision or enrich application data —
88
+ e.g. upserting a user row — before the very first session for that user is persisted:
86
89
 
87
90
  ```ts
88
91
  beforeSessionPersist: async ({session, reason}) => {
89
- if (reason === 'login') {
90
- await recordLogin(session.identity);
91
- }
92
+ if (reason !== 'login') return;
93
+ const user = await upsertUser(session.identity);
94
+ return {...session, identity: {...session.identity, ...user}};
92
95
  };
93
96
  ```
94
97
 
98
+ `resolveIdentity` runs first and may only be able to *read* application data (the user may not
99
+ exist yet on a first login). `beforeSessionPersist` runs next, right before the write, so a session
100
+ mutated or replaced there is the one every subsequent read of that session — including the result
101
+ returned from `handleCallback`/`callbackHandler`'s `onsuccess` — actually sees.
102
+
95
103
  ## SvelteKit hook
96
104
 
97
105
  ```ts
@@ -159,6 +167,49 @@ The underlying operations are also available directly when a route needs custom
159
167
  - `handleCallback(event)`
160
168
  - `logout(event, options)`
161
169
  - `handleBackChannelLogout(event)`
170
+
171
+ ### Request flow
172
+
173
+ ```mermaid
174
+ sequenceDiagram
175
+ participant Browser
176
+ participant login as loginHandler
177
+ participant callback as callbackHandler
178
+ participant logout as logoutHandler
179
+ participant bcl as backChannelLogoutHandler
180
+ participant OP as OpenID Provider
181
+
182
+ Browser->>login: GET /auth/login
183
+ login->>login: create PKCE pair, state, nonce
184
+ login-->>Browser: 302 redirect to OP authorize endpoint
185
+ Browser->>OP: authenticate
186
+ OP-->>Browser: 302 redirect with code & state
187
+
188
+ Browser->>callback: GET /auth/callback?code&state
189
+ callback->>OP: POST token endpoint (exchange code)
190
+ OP-->>callback: id_token, access_token, refresh_token
191
+ callback->>OP: verify id_token against JWKS
192
+ callback->>OP: GET userinfo endpoint (optional)
193
+ callback->>callback: resolveIdentity(idTokenClaims, userInfo)
194
+ callback->>callback: beforeSessionPersist(session, reason:'login')
195
+ Note over callback: a returned session here replaces<br/>what gets persisted and returned
196
+ callback->>callback: write session (cookie or sessionStore)
197
+ callback-->>Browser: onsuccess(event, result) or 302 redirect
198
+
199
+ Browser->>logout: POST /auth/logout
200
+ logout->>logout: clear persisted session
201
+ logout-->>Browser: 302 redirect to OP end_session endpoint or local page
202
+
203
+ OP->>bcl: POST /auth/backchannel-logout (logout_token)
204
+ bcl->>OP: verify logout_token against JWKS
205
+ bcl->>bcl: backChannelLogoutStore.revoke(sid/sub)
206
+ bcl-->>OP: 200 OK
207
+ Note over bcl: next getSession()/requireAuth() call<br/>for that sid/sub treats the session as revoked
208
+ ```
209
+
210
+ `handle` (the SvelteKit hook) wraps every request outside of these four routes: it calls
211
+ `getSession`, which transparently refreshes an expiring session — running `resolveIdentity` and
212
+ `beforeSessionPersist` again with `reason: 'refresh'` — before exposing `event.locals.oidc`.
162
213
  - `getSession(event)`
163
214
  - `requireAuth(event)`
164
215
  - `clearSession(cookies)`
@@ -381,19 +381,19 @@ export function createOIDC(options) {
381
381
  tokens: normalizeTokens(tokenResponse, defaultScope, session.tokens),
382
382
  refreshedAt: Math.floor(Date.now() / 1000)
383
383
  };
384
- await options.beforeSessionPersist?.({
384
+ const persistedSession = (await options.beforeSessionPersist?.({
385
385
  session: nextSession,
386
386
  reason: 'refresh',
387
387
  event,
388
388
  tokenResponse
389
- });
390
- await writePersistedSession(cookies, nextSession, persisted?.id);
389
+ })) ?? nextSession;
390
+ await writePersistedSession(cookies, persistedSession, persisted?.id);
391
391
  log.debug('OIDC session tokens refreshed', {
392
- expiresAt: nextSession.tokens.expiresAt,
393
- refreshExpiresAt: nextSession.tokens.refreshExpiresAt,
394
- hasRefreshToken: Boolean(nextSession.tokens.refreshToken)
392
+ expiresAt: persistedSession.tokens.expiresAt,
393
+ refreshExpiresAt: persistedSession.tokens.refreshExpiresAt,
394
+ hasRefreshToken: Boolean(persistedSession.tokens.refreshToken)
395
395
  });
396
- return nextSession;
396
+ return persistedSession;
397
397
  }
398
398
  catch (err) {
399
399
  log.error('Token refresh failed — clearing session', err);
@@ -494,16 +494,16 @@ export function createOIDC(options) {
494
494
  createdAt: now,
495
495
  refreshedAt: now
496
496
  };
497
- await options.beforeSessionPersist?.({
497
+ const persistedSession = (await options.beforeSessionPersist?.({
498
498
  session,
499
499
  reason: 'login',
500
500
  event: event,
501
501
  tokenResponse
502
- });
502
+ })) ?? session;
503
503
  const existingSession = stateCookie.prompt === 'none' ? await readPersistedSession(event.cookies) : null;
504
- await writePersistedSession(event.cookies, session, existingSession?.id);
504
+ await writePersistedSession(event.cookies, persistedSession, existingSession?.id);
505
505
  return {
506
- session,
506
+ session: persistedSession,
507
507
  returnTo: stateCookie.returnTo
508
508
  };
509
509
  }
@@ -181,13 +181,19 @@ export type OIDCOptions<TIdentity extends OIDCUserClaims = OIDCUserClaims, TRequ
181
181
  userInfo?: OIDCUserClaims;
182
182
  reason: OIDCSessionReason;
183
183
  }) => MaybePromise<TIdentity>;
184
- /** Runs immediately before a login or refreshed session is persisted. */
184
+ /**
185
+ * Runs immediately before a login or refreshed session is persisted.
186
+ * Returning a session replaces the one that gets persisted and handed back to the
187
+ * caller (`handleCallback`'s result, `getSession`'s result); returning `void` keeps it
188
+ * unchanged. Use this to enrich or provision application data — e.g. upsert a user row —
189
+ * before the session is written, rather than after via a route's own callback hook.
190
+ */
185
191
  beforeSessionPersist?: (context: {
186
192
  session: OIDCSession<TIdentity>;
187
193
  reason: OIDCSessionReason;
188
194
  event?: MinimalRequestEvent;
189
195
  tokenResponse: OIDCTokenResponse;
190
- }) => MaybePromise<void>;
196
+ }) => MaybePromise<OIDCSession<TIdentity> | void>;
191
197
  /**
192
198
  * Loads application-owned data once for each authenticated request handled by
193
199
  * `handle`. The result is exposed as `event.locals.oidc.data` and is never
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-oidc",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "OIDC authentication helpers for SvelteKit applications",
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
@@ -62,18 +62,18 @@
62
62
  "@sveltejs/kit": "^2.63.1",
63
63
  "@sveltejs/package": "^2.5.8",
64
64
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
65
- "@types/node": "^25.9.2",
65
+ "@types/node": "^26.2.0",
66
66
  "@semantic-release/changelog": "^6.0.3",
67
67
  "@semantic-release/git": "^10.0.1",
68
- "publint": "^0.3.21",
68
+ "publint": "^0.3.23",
69
69
  "svelte": "^5.56.3",
70
- "svelte-check": "^4.6.0",
70
+ "svelte-check": "^4.7.5",
71
71
  "typescript": "^6.0.3",
72
- "vite": "^8.0.16",
72
+ "vite": "^8.2.1",
73
73
  "@vitest/coverage-v8": "^4.1.8",
74
- "vitest": "^4.1.8",
74
+ "vitest": "^4.1.10",
75
75
  "@sourceregistry/semantic-release-jsr": "^1.1.1",
76
- "typedoc": "^0.28.19"
76
+ "typedoc": "^0.28.20"
77
77
  },
78
78
  "keywords": [
79
79
  "sveltekit",