@udibo/oauth2 0.41.0 → 0.43.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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/dist/errors.d.ts +5 -0
  3. package/dist/errors.js +9 -0
  4. package/dist/identity/errors.d.ts +2 -2
  5. package/dist/identity/errors.js +2 -0
  6. package/dist/identity/events.d.ts +44 -2
  7. package/dist/identity/events.js +2 -1
  8. package/dist/identity/passkeys/mod.d.ts +55 -0
  9. package/dist/identity/passkeys/mod.js +55 -0
  10. package/dist/identity/passkeys/service.d.ts +469 -0
  11. package/dist/identity/passkeys/service.js +558 -0
  12. package/dist/models/token.d.ts +7 -0
  13. package/dist/react/components/internal.d.ts +5 -1
  14. package/dist/react/components/internal.js +9 -0
  15. package/dist/react/components/mfa-challenge-form.d.ts +9 -3
  16. package/dist/react/components/mfa-challenge-form.js +3 -3
  17. package/dist/react/components/mod.d.ts +1 -1
  18. package/dist/react/components/sign-in-form.d.ts +11 -3
  19. package/dist/react/components/sign-in-form.js +4 -4
  20. package/dist/react/components/types.d.ts +12 -0
  21. package/dist/server/_resource-indicator.d.ts +7 -0
  22. package/dist/server/_resource-indicator.js +67 -0
  23. package/dist/server/authorization-server.js +2 -0
  24. package/dist/server/grants/client-credentials.d.ts +6 -0
  25. package/dist/server/grants/client-credentials.js +22 -3
  26. package/dist/server/grants/grant.d.ts +4 -2
  27. package/dist/server/grants/grant.js +17 -3
  28. package/dist/server/grants/refresh-token.d.ts +2 -0
  29. package/dist/server/grants/refresh-token.js +8 -1
  30. package/dist/server/mod.d.ts +1 -1
  31. package/dist/server/mod.js +1 -1
  32. package/dist/server/services/token.d.ts +20 -2
  33. package/dist/server/services/token.js +1 -1
  34. package/dist/server/signing-keys.d.ts +3 -1
  35. package/dist/server/signing-keys.js +2 -2
  36. package/dist/testing/contract/mod.d.ts +5 -3
  37. package/dist/testing/contract/mod.js +5 -3
  38. package/dist/testing/contract/passkey-store.d.ts +66 -0
  39. package/dist/testing/contract/passkey-store.js +196 -0
  40. package/dist/testing/contract/token.d.ts +7 -0
  41. package/dist/testing/contract/token.js +17 -0
  42. package/dist/testing/mod.d.ts +5 -0
  43. package/dist/testing/mod.js +5 -0
  44. package/dist/testing/passkey-authenticator.d.ts +86 -0
  45. package/dist/testing/passkey-authenticator.js +199 -0
  46. package/dist/testing/services.js +4 -0
  47. package/package.json +7 -1
package/README.md CHANGED
@@ -164,6 +164,7 @@ and examples in the [reference](https://jsr.io/@udibo/oauth2/doc).
164
164
  | `/hono/bff` | BFF routes, server-held tokens, session and pending-login stores |
165
165
  | `/identity` | App-owned signup, login, password reset, email verification, passwordless, and protection hooks |
166
166
  | `/identity/mfa` | TOTP, recovery codes, and MFA storage contract |
167
+ | `/identity/passkeys` | Passkey (WebAuthn) registration and sign-in, as a first or second factor |
167
168
  | `/identity/external` | Social and external OIDC sign-in for your own login pages |
168
169
  | `/identity/migration` | Verify imported password hashes and upgrade them at login |
169
170
  | `/hono/identity` | Hono routes for `IdentityService` |
package/dist/errors.d.ts CHANGED
@@ -159,6 +159,11 @@ export declare const InsufficientPermissionsError: OAuth2ErrorClass;
159
159
  * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
160
160
  */
161
161
  export declare const UnsupportedResponseTypeError: OAuth2ErrorClass;
162
+ /**
163
+ * The requested resource is missing, malformed or not allowed by the server's
164
+ * resource policy (RFC 8707 section 2). Descriptions should not echo targets.
165
+ */
166
+ export declare const InvalidTargetError: OAuth2ErrorClass;
162
167
  /**
163
168
  * The requested scope is invalid, unknown, or malformed.
164
169
  *
package/dist/errors.js CHANGED
@@ -205,6 +205,15 @@ export const UnsupportedResponseTypeError = createHttpErrorClass({
205
205
  error: "unsupported_response_type",
206
206
  },
207
207
  });
208
+ /**
209
+ * The requested resource is missing, malformed or not allowed by the server's
210
+ * resource policy (RFC 8707 section 2). Descriptions should not echo targets.
211
+ */
212
+ export const InvalidTargetError = createHttpErrorClass({
213
+ name: "Invalid Target",
214
+ status: 400,
215
+ extensions: { error: "invalid_target" },
216
+ });
208
217
  /**
209
218
  * The requested scope is invalid, unknown, or malformed.
210
219
  *
@@ -7,13 +7,13 @@
7
7
  */
8
8
  /**
9
9
  * Stable, machine-readable identity error codes. The package raises
10
- * `rate_limited`, `weak_password`, and the `mfa_*` codes, and the Hono route
10
+ * `rate_limited`, `weak_password`, and the `mfa_*` and `passkey_*` codes, and the Hono route
11
11
  * factory answers `forbidden_origin` (its same-origin guard, before a handler
12
12
  * runs) and `invalid_credentials` / `invalid_token` (a failed sign-in or token
13
13
  * check); the rest are for your store and routes to throw (e.g. `identifier_taken` on
14
14
  * a duplicate sign-up, `captcha_failed` after `verifyCaptcha`).
15
15
  */
16
- export type IdentityErrorCode = "invalid_credentials" | "invalid_token" | "rate_limited" | "weak_password" | "identifier_taken" | "email_not_verified" | "mfa_not_enrolled" | "mfa_already_enrolled" | "captcha_failed" | "forbidden_origin";
16
+ export type IdentityErrorCode = "invalid_credentials" | "invalid_token" | "rate_limited" | "weak_password" | "identifier_taken" | "email_not_verified" | "mfa_not_enrolled" | "mfa_already_enrolled" | "passkey_limit_reached" | "passkey_not_registered" | "captcha_failed" | "forbidden_origin";
17
17
  /** Options for {@link IdentityError}. */
18
18
  export interface IdentityErrorOptions extends ErrorOptions {
19
19
  /** For `rate_limited`: milliseconds until the caller may retry. */
@@ -40,6 +40,8 @@ const STATUS = {
40
40
  email_not_verified: 403,
41
41
  mfa_not_enrolled: 409,
42
42
  mfa_already_enrolled: 409,
43
+ passkey_limit_reached: 409,
44
+ passkey_not_registered: 409,
43
45
  captcha_failed: 403,
44
46
  forbidden_origin: 403,
45
47
  };
@@ -4,7 +4,8 @@
4
4
  * {@link IdentityService} emits an {@link IdentityEvent} after each significant
5
5
  * auth outcome (sign-in success/failure, sign-up, credential upgrade-on-login,
6
6
  * reset and verification lifecycle, passwordless sign-in links and codes),
7
- * `MfaService` emits the `mfa.*` events, and a password-policy validator emits
7
+ * `MfaService` emits the `mfa.*` events, `PasskeyService` the `passkey.*`
8
+ * events, and a password-policy validator emits
8
9
  * `password_policy.check_unavailable` when it cannot complete its check. Wire
9
10
  * an {@link IdentityEventHook} via the service's `onEvent`
10
11
  * option to persist them in your own audit store — storage stays app-owned,
@@ -34,7 +35,8 @@ export type SignInFailureReason = "unknown_identifier" | "no_password" | "wrong_
34
35
  */
35
36
  export type SignInCodeFailureReason = "invalid" | "expired" | "locked" | "unknown_email";
36
37
  /**
37
- * An auth outcome emitted by {@link IdentityService}, `MfaService`, or a
38
+ * An auth outcome emitted by {@link IdentityService}, `MfaService`,
39
+ * `PasskeyService`, or a
38
40
  * password-policy validator. Discriminated on `type`; `userId` is present when
39
41
  * the flow resolved an account.
40
42
  */
@@ -251,6 +253,46 @@ export type IdentityEvent = {
251
253
  } | {
252
254
  type: "mfa.disabled";
253
255
  userId: string;
256
+ } | {
257
+ type: "passkey.registered";
258
+ userId: string;
259
+ credentialId: string;
260
+ /** Whether the passkey may sync between devices. */
261
+ multiDevice: boolean;
262
+ } | {
263
+ type: "passkey.registration.failed";
264
+ userId: string;
265
+ reason: "expired_challenge" | "invalid" | "duplicate" | "limit_reached";
266
+ } | {
267
+ type: "passkey.verify.succeeded";
268
+ userId: string;
269
+ credentialId: string;
270
+ /** Whether the authenticator verified the person (PIN or biometric). */
271
+ userVerified: boolean;
272
+ } | {
273
+ type: "passkey.verify.failed";
274
+ /**
275
+ * `"sign_count"` means the authenticator's counter went backwards — a
276
+ * possible cloned authenticator, worth an alert.
277
+ */
278
+ reason: "expired_challenge" | "unknown_passkey" | "invalid" | "sign_count" | "replayed";
279
+ /** Present when the passkey resolved to an account. */
280
+ userId?: string;
281
+ credentialId?: string;
282
+ } | {
283
+ type: "passkey.verify.rate_limited";
284
+ userId?: string;
285
+ credentialId?: string;
286
+ retryAfterMs: number;
287
+ enforced: boolean;
288
+ } | {
289
+ type: "passkey.renamed";
290
+ userId: string;
291
+ credentialId: string;
292
+ } | {
293
+ type: "passkey.removed";
294
+ userId: string;
295
+ credentialId: string;
254
296
  };
255
297
  /**
256
298
  * Receives each {@link IdentityEvent} after the flow's outcome is decided. The
@@ -4,7 +4,8 @@
4
4
  * {@link IdentityService} emits an {@link IdentityEvent} after each significant
5
5
  * auth outcome (sign-in success/failure, sign-up, credential upgrade-on-login,
6
6
  * reset and verification lifecycle, passwordless sign-in links and codes),
7
- * `MfaService` emits the `mfa.*` events, and a password-policy validator emits
7
+ * `MfaService` emits the `mfa.*` events, `PasskeyService` the `passkey.*`
8
+ * events, and a password-policy validator emits
8
9
  * `password_policy.check_unavailable` when it cannot complete its check. Wire
9
10
  * an {@link IdentityEventHook} via the service's `onEvent`
10
11
  * option to persist them in your own audit store — storage stays app-owned,
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Passkeys (WebAuthn) for apps that own their login: registration and
3
+ * sign-in ceremonies over app-owned stores, usable as a first factor and as a
4
+ * second factor. Verification is delegated to `@simplewebauthn/server`; this
5
+ * module applies a consumer-friendly policy on top:
6
+ *
7
+ * - **No attestation.** Any passkey manager, platform, or FIDO2 security key
8
+ * works; nothing is allow-listed by authenticator model.
9
+ * - **Discoverable credentials**, so a person can sign in without typing an
10
+ * identifier, and sign-in without a known user lists no credentials and so
11
+ * reveals nothing about which accounts exist.
12
+ * - **User verification preferred and reported**, so the app decides whether a
13
+ * passkey alone satisfies its MFA policy.
14
+ * - **Single-use challenges**, a per-user cap (25 by default), sign-count clone
15
+ * detection, rate limiting, and `passkey.*` events.
16
+ *
17
+ * - {@link PasskeyService} — `startRegistration` / `finishRegistration`,
18
+ * `startAuthentication` / `finishAuthentication`, and list, rename, remove.
19
+ * - {@link PasskeyStore} and {@link PasskeyChallengeStore} — the app's
20
+ * persistence seams, with {@link MemoryPasskeyStore} and
21
+ * {@link MemoryPasskeyChallengeStore} for development and tests.
22
+ * - {@link PASSKEY_AMR} — the `amr` value a passkey contributes.
23
+ *
24
+ * The browser side is `navigator.credentials.create()` / `.get()` with the
25
+ * returned options; `@simplewebauthn/browser` or the native
26
+ * `PublicKeyCredential.parseCreationOptionsFromJSON` handle the conversion.
27
+ * `TestPasskeyAuthenticator` in `@udibo/oauth2/testing` produces real
28
+ * responses for tests.
29
+ *
30
+ * @example Serve a passkey sign-in
31
+ * ```ts
32
+ * import {
33
+ * MemoryPasskeyChallengeStore,
34
+ * MemoryPasskeyStore,
35
+ * PasskeyService,
36
+ * } from "@udibo/oauth2/identity/passkeys";
37
+ *
38
+ * const passkeys = new PasskeyService({
39
+ * store: new MemoryPasskeyStore(),
40
+ * challenges: new MemoryPasskeyChallengeStore(),
41
+ * });
42
+ * const relyingParty = {
43
+ * id: "login.example.com",
44
+ * name: "Example",
45
+ * origins: ["https://login.example.com"],
46
+ * };
47
+ * const { ceremonyId, options } = await passkeys.startAuthentication({
48
+ * relyingParty,
49
+ * });
50
+ * // set ceremonyId in an HttpOnly cookie and send options to the browser
51
+ * ```
52
+ *
53
+ * @module
54
+ */
55
+ export { type FinishPasskeyAuthenticationOptions, type FinishPasskeyRegistrationOptions, MemoryPasskeyChallengeStore, MemoryPasskeyStore, type Passkey, PASSKEY_AMR, type PasskeyAddResult, type PasskeyAuthenticationOptions, type PasskeyAuthenticationResponse, type PasskeyCeremony, type PasskeyChallenge, type PasskeyChallengeStore, type PasskeyRegistration, type PasskeyRegistrationFailureReason, type PasskeyRegistrationOptions, type PasskeyRegistrationResponse, type PasskeyRelyingParty, PasskeyService, type PasskeyServiceOptions, type PasskeyStore, type PasskeyUse, type PasskeyUser, type PasskeyVerification, type PasskeyVerifyFailureReason, type StartPasskeyAuthenticationOptions, type StartPasskeyRegistrationOptions, } from "./service.ts";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Passkeys (WebAuthn) for apps that own their login: registration and
3
+ * sign-in ceremonies over app-owned stores, usable as a first factor and as a
4
+ * second factor. Verification is delegated to `@simplewebauthn/server`; this
5
+ * module applies a consumer-friendly policy on top:
6
+ *
7
+ * - **No attestation.** Any passkey manager, platform, or FIDO2 security key
8
+ * works; nothing is allow-listed by authenticator model.
9
+ * - **Discoverable credentials**, so a person can sign in without typing an
10
+ * identifier, and sign-in without a known user lists no credentials and so
11
+ * reveals nothing about which accounts exist.
12
+ * - **User verification preferred and reported**, so the app decides whether a
13
+ * passkey alone satisfies its MFA policy.
14
+ * - **Single-use challenges**, a per-user cap (25 by default), sign-count clone
15
+ * detection, rate limiting, and `passkey.*` events.
16
+ *
17
+ * - {@link PasskeyService} — `startRegistration` / `finishRegistration`,
18
+ * `startAuthentication` / `finishAuthentication`, and list, rename, remove.
19
+ * - {@link PasskeyStore} and {@link PasskeyChallengeStore} — the app's
20
+ * persistence seams, with {@link MemoryPasskeyStore} and
21
+ * {@link MemoryPasskeyChallengeStore} for development and tests.
22
+ * - {@link PASSKEY_AMR} — the `amr` value a passkey contributes.
23
+ *
24
+ * The browser side is `navigator.credentials.create()` / `.get()` with the
25
+ * returned options; `@simplewebauthn/browser` or the native
26
+ * `PublicKeyCredential.parseCreationOptionsFromJSON` handle the conversion.
27
+ * `TestPasskeyAuthenticator` in `@udibo/oauth2/testing` produces real
28
+ * responses for tests.
29
+ *
30
+ * @example Serve a passkey sign-in
31
+ * ```ts
32
+ * import {
33
+ * MemoryPasskeyChallengeStore,
34
+ * MemoryPasskeyStore,
35
+ * PasskeyService,
36
+ * } from "@udibo/oauth2/identity/passkeys";
37
+ *
38
+ * const passkeys = new PasskeyService({
39
+ * store: new MemoryPasskeyStore(),
40
+ * challenges: new MemoryPasskeyChallengeStore(),
41
+ * });
42
+ * const relyingParty = {
43
+ * id: "login.example.com",
44
+ * name: "Example",
45
+ * origins: ["https://login.example.com"],
46
+ * };
47
+ * const { ceremonyId, options } = await passkeys.startAuthentication({
48
+ * relyingParty,
49
+ * });
50
+ * // set ceremonyId in an HttpOnly cookie and send options to the browser
51
+ * ```
52
+ *
53
+ * @module
54
+ */
55
+ export { MemoryPasskeyChallengeStore, MemoryPasskeyStore, PASSKEY_AMR, PasskeyService, } from "./service.js";