@startino/better-auth-oidc 0.1.7 → 0.1.8

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 +366 -32
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  OIDC-only SSO plugin for [Better Auth](https://www.better-auth.com/). Runs on any JavaScript runtime without Node.js-specific APIs.
4
4
 
5
- ## Why
5
+ ## Why this exists
6
6
 
7
7
  The official [`@better-auth/sso`](https://www.better-auth.com/docs/plugins/sso) plugin imports `samlify` at module load, which requires Node.js-only APIs (`node:crypto`, `node:buffer`). This breaks in edge runtimes, serverless environments, or any platform without full Node.js compatibility, even if you only need OIDC.
8
8
 
9
- This package extracts the OIDC code paths into a standalone package. SAML code and Node.js dependencies are removed entirely.
9
+ This package extracts the OIDC code paths into a standalone plugin. SAML code and Node.js dependencies are removed entirely.
10
10
 
11
11
  ## Runtime compatibility
12
12
 
@@ -32,7 +32,7 @@ npm install @startino/better-auth-oidc
32
32
  pnpm add @startino/better-auth-oidc
33
33
  ```
34
34
 
35
- Peer dependencies: `better-auth` (>=1.4.0) and `better-call` (>=1.0.0).
35
+ Peer dependencies: `better-auth` (>=1.4.0), `better-call` (>=1.0.0).
36
36
 
37
37
  ## Quick start
38
38
 
@@ -92,49 +92,348 @@ await client.signIn.sso({
92
92
  });
93
93
  ```
94
94
 
95
+ ## SSO flow overview
96
+
97
+ Here is what happens during an SSO sign-in:
98
+
99
+ ```
100
+ ┌──────────┐ ┌──────────────┐ ┌─────────┐ ┌─────────┐
101
+ │ Client │ │ Auth Server │ │ IdP │ │ App │
102
+ └────┬─────┘ └──────┬───────┘ └────┬────┘ └────┬────┘
103
+ │ POST /sign-in/sso│ │ │
104
+ │──────────────────>│ │ │
105
+ │ │ 302 → authorize │ │
106
+ │ │─────────────────>│ │
107
+ │ │ │ │
108
+ │ │ User logs in │ │
109
+ │ │ │ │
110
+ │ │ GET /sso/callback│ │
111
+ │ │<─────────────────│ │
112
+ │ │ │ │
113
+ │ │ Exchange code │ │
114
+ │ │ for tokens │ │
115
+ │ │─────────────────>│ │
116
+ │ │<─────────────────│ │
117
+ │ │ │ │
118
+ │ │ Create/link user │ │
119
+ │ │ Create session │ │
120
+ │ │ Generate OTT │ │
121
+ │ │ │ │
122
+ │ │ 302 → callbackURL?ott=TOKEN │
123
+ │ │─────────────────────────────────>│
124
+ │ │ │ │
125
+ │ │ GET /sso/verify-ott?token=… │
126
+ │ │<─────────────────────────────────│
127
+ │ │ │ │
128
+ │ │ Set session cookie on app domain│
129
+ │ │─────────────────────────────────>│
130
+ └ └ └ └
131
+ ```
132
+
133
+ 1. **Sign-in request.** The client calls `POST /sign-in/sso` with an email, domain, or provider ID. The plugin finds the matching SSO provider and builds the OIDC authorization URL.
134
+ 2. **Redirect to IdP.** The user is redirected to the identity provider (Okta, Entra ID, Google Workspace, etc.) to authenticate.
135
+ 3. **Callback.** The IdP redirects back to `GET /sso/callback/:providerId` with an authorization code.
136
+ 4. **Token exchange.** The plugin exchanges the code for an ID token and (optionally) access token. The ID token is verified against the provider's JWKS.
137
+ 5. **User creation/linking.** A user account is created or linked. Organization membership is assigned if configured.
138
+ 6. **OTT generation.** A one-time token is created (32 random characters, 60-second TTL) and appended to the callback URL as `?ott=TOKEN`.
139
+ 7. **OTT verification.** The app calls `GET /sso/verify-ott?token=TOKEN` through its proxy to the auth server. This sets the session cookie on the app's domain.
140
+
141
+ ## Cross-domain session transfer (OTT)
142
+
143
+ ### The problem
144
+
145
+ In proxy-based architectures (SvelteKit + Convex, Next.js + external auth server, etc.), the auth server runs on a different domain than your app. The SSO callback hits the auth server's domain, so the session cookie is set there, not on the app's domain.
146
+
147
+ Without OTT, the user would be redirected back to the app with no session.
148
+
149
+ ### How it works
150
+
151
+ After the SSO callback processes the sign-in, the plugin:
152
+
153
+ 1. Creates a session and sets the cookie on the auth server's domain
154
+ 2. Generates a one-time token (32 characters, stored in the `verification` table, expires in 60 seconds)
155
+ 3. Redirects to your `callbackURL` with `?ott=TOKEN` appended
156
+
157
+ Your app then verifies the OTT through its auth proxy, which sets the session cookie on the correct domain.
158
+
159
+ ### Client-side implementation
160
+
161
+ On whichever page your `callbackURL` points to, check for the `ott` query parameter and verify it:
162
+
163
+ ```ts
164
+ // Example: SvelteKit +page.ts or +page.server.ts
165
+ import { redirect } from "@sveltejs/kit";
166
+
167
+ export const load = async ({ url, fetch }) => {
168
+ const ott = url.searchParams.get("ott");
169
+ if (ott) {
170
+ // Call through your proxy so the cookie is set on your domain
171
+ const res = await fetch(`/api/auth/sso/verify-ott?token=${ott}`);
172
+ if (!res.ok) {
173
+ throw redirect(303, "/signin?error=sso-verification-failed");
174
+ }
175
+ // Remove the ott param from the URL
176
+ throw redirect(303, url.pathname);
177
+ }
178
+ };
179
+ ```
180
+
181
+ ```ts
182
+ // Example: Next.js middleware or page
183
+ const ott = searchParams.get("ott");
184
+ if (ott) {
185
+ await fetch(`${process.env.NEXT_PUBLIC_URL}/api/auth/sso/verify-ott?token=${ott}`, {
186
+ credentials: "include",
187
+ });
188
+ redirect("/dashboard");
189
+ }
190
+ ```
191
+
192
+ The key requirement: the `verify-ott` call must go through your app's proxy to the auth server so the `Set-Cookie` header lands on your app's domain.
193
+
95
194
  ## Configuration options
96
195
 
196
+ ### `SSOOptions`
197
+
97
198
  | Option | Type | Default | Description |
98
199
  |---|---|---|---|
99
- | `provisionUser` | `function` | - | Custom function called when a new user signs in via SSO |
100
- | `organizationProvisioning` | `object` | - | Auto-assign users to orgs based on SSO provider |
101
- | `organizationProvisioning.defaultRole` | `"member" \| "admin"` | `"member"` | Default role for auto-provisioned members |
102
- | `organizationProvisioning.getRole` | `function` | - | Dynamic role assignment function |
103
- | `defaultSSO` | `array` | - | Default provider configs for testing (takes precedence over DB) |
104
- | `defaultOverrideUserInfo` | `boolean` | `false` | Override user info with provider data on each sign-in |
105
- | `disableImplicitSignUp` | `boolean` | `false` | Require explicit `requestSignUp: true` to create new users |
106
- | `modelName` | `string` | `"ssoProvider"` | Custom table name for SSO providers |
107
- | `fields` | `object` | - | Custom field name mappings for the provider table |
108
- | `providersLimit` | `number \| function` | `10` | Max providers per user (0 to disable registration) |
109
- | `trustEmailVerified` | `boolean` | `false` | Trust the `email_verified` claim from the IdP (deprecated) |
110
- | `domainVerification` | `object` | - | Enable DNS-based domain ownership verification |
111
- | `domainVerification.enabled` | `boolean` | `false` | Enable/disable the feature |
112
- | `domainVerification.tokenPrefix` | `string` | `"better-auth-token"` | Prefix for the DNS TXT record identifier |
200
+ | `provisionUser` | `(data) => void` | - | Called after a user signs in or signs up via SSO. Receives `{ user, userInfo, token, provider }`. Use for custom role assignment, feature flags, syncing to external systems, etc. |
201
+ | `organizationProvisioning` | `object` | - | Auto-assign users to organizations based on SSO provider. See [Organization provisioning](#organization-provisioning). |
202
+ | `defaultSSO` | `Array<{ domain, providerId, oidcConfig? }>` | - | Default provider configs (takes precedence over DB). Useful for development/testing without storing providers in the database. |
203
+ | `defaultOverrideUserInfo` | `boolean` | `false` | Override user name/image with provider data on every sign-in, not just the first. |
204
+ | `disableImplicitSignUp` | `boolean` | `false` | Reject new users unless `requestSignUp: true` is passed in the sign-in call. |
205
+ | `modelName` | `string` | `"ssoProvider"` | Custom table name for SSO providers in the database. |
206
+ | `fields` | `object` | - | Remap column names: `{ issuer?, oidcConfig?, userId?, providerId?, organizationId?, domain? }`. |
207
+ | `providersLimit` | `number \| (user) => number` | `10` | Max providers a user can register. Set to `0` to disable registration entirely. |
208
+ | `trustEmailVerified` | `boolean` | `false` | Trust the `email_verified` claim from the IdP. **Deprecated.** See [Account linking](#account-linking-and-trustemailverified). |
209
+ | `domainVerification` | `object` | - | DNS-based domain ownership verification. See [Domain verification](#domain-verification). |
210
+ | `domainVerification.enabled` | `boolean` | `false` | Enable/disable domain verification. |
211
+ | `domainVerification.tokenPrefix` | `string` | `"better-auth-token"` | Prefix for the DNS TXT record. |
212
+
213
+ ## OIDC configuration
214
+
215
+ When registering or updating a provider, the `oidcConfig` object controls how the plugin communicates with the identity provider.
216
+
217
+ | Field | Type | Required | Default | Description |
218
+ |---|---|---|---|---|
219
+ | `clientId` | `string` | Yes | - | OAuth client ID from your IdP. |
220
+ | `clientSecret` | `string` | Yes | - | OAuth client secret from your IdP. |
221
+ | `issuer` | `string` | - | Parent `issuer` | The OIDC issuer URL. Usually set on the provider, not inside `oidcConfig`. |
222
+ | `discoveryEndpoint` | `string` | No | `{issuer}/.well-known/openid-configuration` | Override the discovery URL if your IdP uses a non-standard path. |
223
+ | `skipDiscovery` | `boolean` | No | `false` | Skip automatic OIDC discovery. When `true`, you must provide `authorizationEndpoint`, `tokenEndpoint`, and `jwksEndpoint` manually. |
224
+ | `authorizationEndpoint` | `string` | If `skipDiscovery` | Discovered | OAuth authorization URL. |
225
+ | `tokenEndpoint` | `string` | If `skipDiscovery` | Discovered | OAuth token exchange URL. |
226
+ | `jwksEndpoint` | `string` | If `skipDiscovery` | Discovered | JWKS URL for ID token signature verification. |
227
+ | `userInfoEndpoint` | `string` | No | Discovered | UserInfo endpoint. Only needed if ID token claims are insufficient. |
228
+ | `tokenEndpointAuthentication` | `"client_secret_basic" \| "client_secret_post"` | No | Auto-detected | How to authenticate at the token endpoint. `client_secret_basic` sends credentials in the Authorization header. `client_secret_post` sends them in the request body. The plugin auto-selects based on IdP discovery, preferring `client_secret_basic`. |
229
+ | `pkce` | `boolean` | No | `true` | Use PKCE (Proof Key for Code Exchange) with S256 challenge method. Recommended to leave enabled. |
230
+ | `scopes` | `string[]` | No | `["openid", "email", "profile"]` | OAuth scopes to request. |
231
+ | `overrideUserInfo` | `boolean` | No | `false` | Override stored user info with provider data on this specific provider. |
232
+ | `mapping` | `OIDCMapping` | No | See below | Map non-standard claim names to expected fields. |
233
+
234
+ ### Field mapping
235
+
236
+ If your IdP returns claims with non-standard names, use the `mapping` object to tell the plugin where to find each field.
237
+
238
+ ```ts
239
+ oidcConfig: {
240
+ clientId: "...",
241
+ clientSecret: "...",
242
+ mapping: {
243
+ id: "sub", // Default: "sub"
244
+ email: "email", // Default: "email"
245
+ emailVerified: "email_verified", // Default: "email_verified"
246
+ name: "name", // Default: "name"
247
+ image: "picture", // Default: "picture"
248
+ extraFields: { // Map additional claims
249
+ department: "custom:department",
250
+ employeeId: "custom:employee_id",
251
+ },
252
+ },
253
+ }
254
+ ```
255
+
256
+ The `extraFields` values are passed through to the `provisionUser` callback in `userInfo`.
113
257
 
114
258
  ## Endpoints
115
259
 
116
- | Endpoint | Method | Description |
117
- |---|---|---|
118
- | `/sso/register` | POST | Register a new OIDC provider |
119
- | `/sign-in/sso` | POST | Initiate SSO sign-in (redirects to IdP) |
120
- | `/sso/callback/:providerId` | GET | OAuth2 callback handler |
121
- | `/sso/providers` | GET | List providers the user has access to |
122
- | `/sso/get-provider` | GET | Get details for a specific provider |
123
- | `/sso/update-provider` | POST | Update an existing provider |
124
- | `/sso/delete-provider` | POST | Delete a provider |
125
- | `/sso/request-domain-verification` | POST | Request domain verification (if enabled) |
126
- | `/sso/verify-domain` | POST | Verify domain via DNS TXT record (if enabled) |
260
+ | Endpoint | Method | Auth | Description |
261
+ |---|---|---|---|
262
+ | `/sso/register` | POST | Yes | Register a new OIDC provider. |
263
+ | `/sign-in/sso` | POST | No | Initiate SSO sign-in. Accepts `email`, `providerId`, `domain`, or `organizationSlug` to find the provider. Returns `{ url, redirect: true }`. |
264
+ | `/sso/callback/:providerId` | GET | No | OAuth2 callback handler. Exchanges code for tokens, creates/links user, generates OTT, redirects to `callbackURL`. |
265
+ | `/sso/verify-ott` | GET | No | Exchange a one-time token for a session cookie. Query: `?token=TOKEN`. |
266
+ | `/sso/providers` | GET | Yes | List providers the authenticated user has access to. |
267
+ | `/sso/get-provider` | GET | Yes | Get a single provider. Query: `?providerId=ID`. |
268
+ | `/sso/update-provider` | POST | Yes | Update provider fields (partial update). Changing `domain` resets `domainVerified`. |
269
+ | `/sso/delete-provider` | POST | Yes | Delete a provider. Body: `{ providerId }`. |
270
+ | `/sso/request-domain-verification` | POST | Yes | Request a domain verification token (if enabled). |
271
+ | `/sso/verify-domain` | POST | Yes | Verify domain via DNS TXT record lookup (if enabled). |
272
+
273
+ ### Sign-in options
274
+
275
+ The `POST /sign-in/sso` endpoint accepts these fields:
276
+
277
+ | Field | Type | Required | Description |
278
+ |---|---|---|---|
279
+ | `email` | `string` | One of these | Extracts the domain to find a matching provider. |
280
+ | `providerId` | `string` | | Direct provider reference. |
281
+ | `domain` | `string` | | Direct domain reference. |
282
+ | `organizationSlug` | `string` | | Finds the org, then finds a provider linked to it. |
283
+ | `callbackURL` | `string` | Yes | Where to redirect after sign-in. The OTT is appended here. |
284
+ | `errorCallbackURL` | `string` | No | Redirect on error. Falls back to `callbackURL`. |
285
+ | `newUserCallbackURL` | `string` | No | Redirect for first-time users. Falls back to `callbackURL`. |
286
+ | `scopes` | `string[]` | No | Override the provider's default scopes for this sign-in. |
287
+ | `loginHint` | `string` | No | Pre-fill the email/username at the IdP login screen. |
288
+ | `requestSignUp` | `boolean` | No | Required when `disableImplicitSignUp` is `true`. |
289
+
290
+ ## Organization provisioning
291
+
292
+ If you use Better Auth's [organization plugin](https://www.better-auth.com/docs/plugins/organization), you can auto-assign users to organizations when they sign in via SSO.
293
+
294
+ ```ts
295
+ oidcSso({
296
+ organizationProvisioning: {
297
+ defaultRole: "member", // "member" or "admin"
298
+ getRole: async ({ user, userInfo, token, provider }) => {
299
+ // Custom logic, e.g. check a group claim
300
+ const groups = userInfo["groups"] || [];
301
+ return groups.includes("admins") ? "admin" : "member";
302
+ },
303
+ },
304
+ });
305
+ ```
306
+
307
+ How it works:
308
+
309
+ 1. When registering a provider, pass `organizationId` to link it to an org.
310
+ 2. On sign-in, after the user account is created or linked, the plugin checks if the provider has an `organizationId`.
311
+ 3. If the user is not already a member of that org, they are added with the role from `getRole()` (or `defaultRole` if no function provided).
312
+
313
+ Set `organizationProvisioning.disabled: true` to turn off auto-provisioning while keeping the config.
314
+
315
+ ### `provisionUser` callback
316
+
317
+ For more control beyond org assignment, use `provisionUser`. It runs after the user is created/linked and after org assignment.
318
+
319
+ ```ts
320
+ oidcSso({
321
+ provisionUser: async ({ user, userInfo, token, provider }) => {
322
+ // Sync to your own database, assign feature flags, send a welcome email, etc.
323
+ await myDb.upsertUser({
324
+ id: user.id,
325
+ department: userInfo.department,
326
+ ssoProvider: provider.providerId,
327
+ });
328
+ },
329
+ });
330
+ ```
331
+
332
+ The `token` parameter contains the OAuth2 tokens (access token, refresh token, ID token) from the provider if you need to make further API calls.
333
+
334
+ ## Multi-domain SSO
335
+
336
+ A single SSO provider can serve multiple email domains. Pass a comma-separated string:
337
+
338
+ ```ts
339
+ await client.sso.register({
340
+ providerId: "acme-corp",
341
+ issuer: "https://acme.okta.com",
342
+ domain: "acme.com,subsidiary.com,acquired-co.com",
343
+ oidcConfig: { clientId: "...", clientSecret: "..." },
344
+ });
345
+ ```
346
+
347
+ When a user signs in with `user@subsidiary.com`, the plugin finds the provider by checking each domain in the list. Subdomain matching is also supported: `user@eng.acme.com` matches `acme.com`.
348
+
349
+ The `domainMatches(searchDomain, domainList)` utility function handles this logic. It splits on commas, trims whitespace, and checks for exact or subdomain matches (case-insensitive).
127
350
 
128
351
  ## Domain verification
129
352
 
130
- When `domainVerification.enabled` is `true`, new providers require DNS-based domain ownership verification before sign-ins are allowed.
353
+ When `domainVerification.enabled` is `true`, new providers require DNS-based domain ownership proof before sign-ins are allowed.
354
+
355
+ ### Setup flow
131
356
 
132
- 1. Register a provider. The response includes a `domainVerificationToken`.
133
- 2. Create a DNS TXT record: `_better-auth-token-<providerId>.<domain>` with value `_better-auth-token-<providerId>=<token>`.
134
- 3. Call the verify endpoint. The plugin resolves the TXT record via DNS-over-HTTPS (Cloudflare) and confirms ownership.
357
+ 1. **Register a provider.** The response includes `domainVerificationToken` and `domainVerified: false`.
358
+
359
+ 2. **Create a DNS TXT record:**
360
+ ```
361
+ Name: _better-auth-token-<providerId>.<domain>
362
+ Value: _better-auth-token-<providerId>=<token>
363
+ ```
364
+ Example for provider `okta-acme` on `acme.com`:
365
+ ```
366
+ _better-auth-token-okta-acme.acme.com TXT "_better-auth-token-okta-acme=abc123xyz..."
367
+ ```
368
+
369
+ 3. **Call the verify endpoint.** The plugin resolves the TXT record via DNS-over-HTTPS (Cloudflare) and confirms ownership.
135
370
 
136
371
  No `node:dns` required. Verification works on any runtime with `fetch`.
137
372
 
373
+ ### How DNS verification works
374
+
375
+ - The plugin queries `https://cloudflare-dns.com/dns-query` with the TXT record name.
376
+ - The record name follows RFC 8552 (underscore-prefixed labels): `_{tokenPrefix}-{providerId}.{domain}`.
377
+ - The token prefix defaults to `better-auth-token`. You can change it in the config.
378
+ - Verification tokens last 7 days. You can re-request them if expired.
379
+ - If the domain changes on a provider, `domainVerified` resets to `false`.
380
+ - DNS labels are capped at 63 characters. The plugin validates this before lookup.
381
+
382
+ ## OIDC discovery
383
+
384
+ By default, the plugin fetches the provider's `/.well-known/openid-configuration` document when you register a provider. This auto-fills:
385
+
386
+ - `authorizationEndpoint`
387
+ - `tokenEndpoint`
388
+ - `jwksEndpoint`
389
+ - `userInfoEndpoint`
390
+ - `tokenEndpointAuthentication` (selected from `token_endpoint_auth_methods_supported`)
391
+
392
+ If the provider's discovery document is missing required fields (`issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`), registration fails with a descriptive error.
393
+
394
+ ### Skipping discovery
395
+
396
+ Some providers don't support standard OIDC discovery. Set `skipDiscovery: true` and provide endpoints manually:
397
+
398
+ ```ts
399
+ oidcConfig: {
400
+ clientId: "...",
401
+ clientSecret: "...",
402
+ skipDiscovery: true,
403
+ authorizationEndpoint: "https://idp.example.com/authorize",
404
+ tokenEndpoint: "https://idp.example.com/token",
405
+ jwksEndpoint: "https://idp.example.com/.well-known/jwks.json",
406
+ }
407
+ ```
408
+
409
+ ### Custom discovery endpoint
410
+
411
+ If your IdP uses a non-standard discovery path:
412
+
413
+ ```ts
414
+ oidcConfig: {
415
+ clientId: "...",
416
+ clientSecret: "...",
417
+ discoveryEndpoint: "https://idp.example.com/custom/.well-known/openid-configuration",
418
+ }
419
+ ```
420
+
421
+ ### Runtime discovery
422
+
423
+ If the stored config is missing `tokenEndpoint` or `jwksEndpoint` at sign-in time, the plugin re-runs discovery to fill them in. This handles cases where you registered a provider before certain fields were required.
424
+
425
+ ## Account linking and `trustEmailVerified`
426
+
427
+ When a user signs in via SSO with an email that already exists in your database, Better Auth's [account linking](https://www.better-auth.com/docs/concepts/users-accounts#account-linking) determines what happens.
428
+
429
+ The `trustEmailVerified` option on this plugin controls whether the `email_verified` claim from the IdP is passed to Better Auth as the user's `emailVerified` field. If `true`, and the IdP says the email is verified, Better Auth may auto-link the account (depending on your `accountLinking` config).
430
+
431
+ **This option is deprecated.** The IdP's `email_verified` claim is a weak trust signal. Instead:
432
+
433
+ - Use `domainVerification: { enabled: true }` to verify that the SSO provider actually owns the domain.
434
+ - Configure Better Auth's `accountLinking.trustedProviders` to trust specific providers.
435
+ - Or set up `accountLinking.allowDifferentEmails` per your needs.
436
+
138
437
  ## Migration from `@better-auth/sso`
139
438
 
140
439
  | `@better-auth/sso` | `@startino/better-auth-oidc` |
@@ -151,6 +450,41 @@ No `node:dns` required. Verification works on any runtime with `fetch`.
151
450
 
152
451
  The database schema is the same minus the `samlConfig` column. If migrating from `@better-auth/sso`, you can drop the `samlConfig` column from your `ssoProvider` table, or leave it (it will be ignored).
153
452
 
453
+ ## Exported utilities
454
+
455
+ The package exports OIDC discovery functions and types for advanced use cases:
456
+
457
+ ### Discovery functions
458
+
459
+ | Export | Description |
460
+ |---|---|
461
+ | `discoverOIDCConfig(params)` | Main entry point. Fetches and hydrates OIDC config from an issuer URL. |
462
+ | `computeDiscoveryUrl(issuer)` | Returns `{issuer}/.well-known/openid-configuration`. |
463
+ | `fetchDiscoveryDocument(url, timeout?)` | Fetches and parses a discovery document. Default timeout: 10 seconds. |
464
+ | `validateDiscoveryUrl(url, isTrustedOrigin)` | Validates that a discovery URL is trusted. |
465
+ | `validateDiscoveryDocument(doc, issuer)` | Checks required fields and issuer match. |
466
+ | `normalizeDiscoveryUrls(doc, issuer, isTrustedOrigin)` | Validates and normalizes all endpoint URLs in a discovery document. |
467
+ | `normalizeUrl(name, endpoint, issuer)` | Normalizes a single URL, resolving relative paths against the issuer. |
468
+ | `selectTokenEndpointAuthMethod(doc, existing?)` | Picks the best token endpoint auth method from a discovery document. |
469
+ | `needsRuntimeDiscovery(config)` | Returns `true` if the config is missing `tokenEndpoint` or `jwksEndpoint`. |
470
+ | `mapDiscoveryErrorToAPIError(error)` | Converts a `DiscoveryError` to a Better Auth `APIError`. |
471
+ | `REQUIRED_DISCOVERY_FIELDS` | Array of required fields: `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`. |
472
+
473
+ ### Types
474
+
475
+ | Export | Description |
476
+ |---|---|
477
+ | `OIDCConfig` | Full OIDC provider configuration object. |
478
+ | `SSOOptions` | Plugin configuration options. |
479
+ | `SSOProvider` | SSO provider record (conditional on `domainVerification`). |
480
+ | `OIDCDiscoveryDocument` | OpenID Connect Discovery 1.0 document shape. |
481
+ | `HydratedOIDCConfig` | Discovery-resolved config with all endpoints filled in. |
482
+ | `DiscoverOIDCConfigParams` | Parameters for `discoverOIDCConfig()`. |
483
+ | `DiscoveryErrorCode` | Union of discovery error codes. |
484
+ | `DiscoveryError` | Error class with `code` and `details`. |
485
+ | `RequiredDiscoveryField` | Union type of required discovery field names. |
486
+ | `OIDCSSOPlugin` | Plugin type for type inference. |
487
+
154
488
  ## Credits
155
489
 
156
490
  This package is an OIDC-only extraction of [`@better-auth/sso`](https://github.com/better-auth/better-auth/tree/main/packages/sso) by [Bereket Engida](https://github.com/bereketa). All OIDC logic, discovery pipeline, organization linking, and provider management code originates from that package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startino/better-auth-oidc",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",