@sentropic/auth-hono 0.2.1 → 0.3.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 (90) hide show
  1. package/README.md +115 -1
  2. package/dist/contracts.d.ts +1 -1
  3. package/dist/contracts.d.ts.map +1 -1
  4. package/dist/contracts.js +2 -0
  5. package/dist/contracts.js.map +1 -1
  6. package/dist/index.d.ts +15 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +15 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/oauth/authorize-handler.d.ts +13 -0
  11. package/dist/oauth/authorize-handler.d.ts.map +1 -0
  12. package/dist/oauth/authorize-handler.js +143 -0
  13. package/dist/oauth/authorize-handler.js.map +1 -0
  14. package/dist/oauth/consent-decision-handler.d.ts +11 -0
  15. package/dist/oauth/consent-decision-handler.d.ts.map +1 -0
  16. package/dist/oauth/consent-decision-handler.js +58 -0
  17. package/dist/oauth/consent-decision-handler.js.map +1 -0
  18. package/dist/oauth/crypto-utils.d.ts +3 -0
  19. package/dist/oauth/crypto-utils.d.ts.map +1 -0
  20. package/dist/oauth/crypto-utils.js +13 -0
  21. package/dist/oauth/crypto-utils.js.map +1 -0
  22. package/dist/oauth/dpop.d.ts +18 -0
  23. package/dist/oauth/dpop.d.ts.map +1 -0
  24. package/dist/oauth/dpop.js +54 -0
  25. package/dist/oauth/dpop.js.map +1 -0
  26. package/dist/oauth/http-utils.d.ts +6 -0
  27. package/dist/oauth/http-utils.d.ts.map +1 -0
  28. package/dist/oauth/http-utils.js +27 -0
  29. package/dist/oauth/http-utils.js.map +1 -0
  30. package/dist/oauth/introspect-handler.d.ts +8 -0
  31. package/dist/oauth/introspect-handler.d.ts.map +1 -0
  32. package/dist/oauth/introspect-handler.js +63 -0
  33. package/dist/oauth/introspect-handler.js.map +1 -0
  34. package/dist/oauth/jwks-service.d.ts +25 -0
  35. package/dist/oauth/jwks-service.d.ts.map +1 -0
  36. package/dist/oauth/jwks-service.js +61 -0
  37. package/dist/oauth/jwks-service.js.map +1 -0
  38. package/dist/oauth/revoke-handler.d.ts +8 -0
  39. package/dist/oauth/revoke-handler.d.ts.map +1 -0
  40. package/dist/oauth/revoke-handler.js +55 -0
  41. package/dist/oauth/revoke-handler.js.map +1 -0
  42. package/dist/oauth/router.d.ts +8 -0
  43. package/dist/oauth/router.d.ts.map +1 -0
  44. package/dist/oauth/router.js +30 -0
  45. package/dist/oauth/router.js.map +1 -0
  46. package/dist/oauth/session-resolver.d.ts +9 -0
  47. package/dist/oauth/session-resolver.d.ts.map +1 -0
  48. package/dist/oauth/session-resolver.js +28 -0
  49. package/dist/oauth/session-resolver.js.map +1 -0
  50. package/dist/oauth/state-codec.d.ts +25 -0
  51. package/dist/oauth/state-codec.d.ts.map +1 -0
  52. package/dist/oauth/state-codec.js +60 -0
  53. package/dist/oauth/state-codec.js.map +1 -0
  54. package/dist/oauth/state-store-types.d.ts +86 -0
  55. package/dist/oauth/state-store-types.d.ts.map +1 -0
  56. package/dist/oauth/state-store-types.js +2 -0
  57. package/dist/oauth/state-store-types.js.map +1 -0
  58. package/dist/oauth/token-handler.d.ts +11 -0
  59. package/dist/oauth/token-handler.d.ts.map +1 -0
  60. package/dist/oauth/token-handler.js +176 -0
  61. package/dist/oauth/token-handler.js.map +1 -0
  62. package/dist/oauth/userinfo-handler.d.ts +9 -0
  63. package/dist/oauth/userinfo-handler.d.ts.map +1 -0
  64. package/dist/oauth/userinfo-handler.js +93 -0
  65. package/dist/oauth/userinfo-handler.js.map +1 -0
  66. package/dist/oauth/wellknown-handler.d.ts +9 -0
  67. package/dist/oauth/wellknown-handler.d.ts.map +1 -0
  68. package/dist/oauth/wellknown-handler.js +37 -0
  69. package/dist/oauth/wellknown-handler.js.map +1 -0
  70. package/dist/ports.d.ts +4 -0
  71. package/dist/ports.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/contracts.ts +2 -0
  74. package/src/index.ts +15 -0
  75. package/src/oauth/authorize-handler.ts +201 -0
  76. package/src/oauth/consent-decision-handler.ts +93 -0
  77. package/src/oauth/crypto-utils.ts +14 -0
  78. package/src/oauth/dpop.ts +93 -0
  79. package/src/oauth/http-utils.ts +58 -0
  80. package/src/oauth/introspect-handler.ts +88 -0
  81. package/src/oauth/jwks-service.ts +103 -0
  82. package/src/oauth/revoke-handler.ts +70 -0
  83. package/src/oauth/router.ts +42 -0
  84. package/src/oauth/session-resolver.ts +48 -0
  85. package/src/oauth/state-codec.ts +98 -0
  86. package/src/oauth/state-store-types.ts +94 -0
  87. package/src/oauth/token-handler.ts +252 -0
  88. package/src/oauth/userinfo-handler.ts +129 -0
  89. package/src/oauth/wellknown-handler.ts +52 -0
  90. package/src/ports.ts +16 -0
package/README.md CHANGED
@@ -68,13 +68,127 @@ All package handlers emit structured responses to keep contracts predictable acr
68
68
  - **Sentropic-style app** with a Drizzle/Postgres backend, WebAuthn-only login, and workspace-scoped sessions: implement `AuthHonoCredentialPort`, an `AuthHonoSessionService` adapter (or the package's `createAuthSessionService` when the full `AuthHonoPorts` bundle is wired), an `AuthHonoWebAuthnRegistrationService`/`AuthHonoWebAuthnAuthenticationService` adapter, and provide `prepareRegistrationOptions`/`resolveRegistrationUser` for first-admin + account-status policy and `finalizeRegistration`/`finalizeAuthentication` for session creation and cookie issuance. Sentropic's `api/src/services/auth/*-adapter.ts` modules show this end-to-end.
69
69
  - **DB-less admin flow** (e.g. `spa-transpose-cv` mounting at `/admin/auth/*`): use a file- or memory-backed implementation of `AuthHonoCredentialPort` and the relevant ports, skip workspace bootstrap entirely, and either rely on the default verify response or supply a minimal `finalizeAuthentication` that issues a host-managed session cookie. The package never touches workspace state, so no DB schema is required.
70
70
 
71
+ ## OAuth2 / OpenID Connect Identity Provider (since 0.3.0)
72
+
73
+ `@sentropic/auth-hono` ships a complete OAuth2 Authorization Server + OIDC Identity Provider built on the same ports-and-adapters model as the existing WebAuthn routes.
74
+
75
+ ### Endpoints
76
+
77
+ | Endpoint | Description |
78
+ | --- | --- |
79
+ | `GET /oauth/authorize` | Authorization Code + PKCE entry point (S256 required) |
80
+ | `POST /oauth/token` | Token issuance (`grant_type=authorization_code` only in 0.3.x) |
81
+ | `GET\|POST /oauth/userinfo` | Returns claims for a valid access token |
82
+ | `POST /oauth/revoke` | Revokes an access token (RFC 7009) |
83
+ | `POST /oauth/introspect` | Token introspection (RFC 7662, client-auth required) |
84
+ | `POST /oauth/consent/decision` | Host-private consent approval/denial — NOT in discovery doc |
85
+ | `GET /.well-known/openid-configuration` | OIDC discovery document |
86
+ | `GET /.well-known/jwks.json` | Public Ed25519 signing keys (RFC 7517) |
87
+
88
+ ### Wiring the OAuth router
89
+
90
+ ```ts
91
+ import { createOAuthRouter, createWellKnownRouter } from '@sentropic/auth-hono';
92
+ import { Hono } from 'hono';
93
+
94
+ // Router mounted under /api/v1/auth
95
+ const authRouter = new Hono();
96
+ authRouter.route('/oauth', createOAuthRouter({
97
+ ports, // AuthHonoPorts — includes oauthStateStore + jwks
98
+ issuer, // e.g. 'https://api.example.com'
99
+ loginUrl, // e.g. 'https://app.example.com/auth/login'
100
+ consentUrl, // e.g. 'https://app.example.com/auth/oauth/consent'
101
+ }));
102
+
103
+ // Well-known router mounted at root level
104
+ const app = new Hono();
105
+ app.route('/.well-known', createWellKnownRouter({ ports, issuer }));
106
+ app.route('/api/v1/auth', authRouter);
107
+ ```
108
+
109
+ ### JwksService and signing
110
+
111
+ `createJwksService({ jwksPort, clock })` returns `{ signJwt, verifyJwt, getPublicJwks }`:
112
+
113
+ ```ts
114
+ import { createJwksService } from '@sentropic/auth-hono';
115
+
116
+ const jwksService = createJwksService({ jwksPort: hostJwksPort, clock: Date.now });
117
+
118
+ // Sign an access token
119
+ const accessToken = await jwksService.signJwt(
120
+ { sub: userId, scope: 'openid profile', iss: issuer, aud: `${issuer}/userinfo` },
121
+ { expiresIn: 3600 }
122
+ );
123
+
124
+ // Verify any token (looks up kid from JWKS, accepts active and rotated keys)
125
+ const payload = await jwksService.verifyJwt(incomingJwt);
126
+ ```
127
+
128
+ Signing algorithm: EdDSA (Ed25519) only. No RS256 fallback. JWKS response includes the active key and all rotated keys for at least `access_token TTL + JWKS cache TTL` (≥ 65 minutes). Discovery response sends `Cache-Control: public, max-age=300` on JWKS.
129
+
130
+ ### Key rotation policy
131
+
132
+ 1. The active signing key is created once via the host's `make exec-api CMD="npm run oauth:init-keys"` (or `make oauth-init-keys`).
133
+ 2. Rotate via `make oauth-rotate-keys` (calls `JwksService.rotateKey()` through `api/src/scripts/oauth-rotate-keys.ts`).
134
+ 3. Rotated keys remain in JWKS for ≥ 65 minutes so in-flight tokens stay verifiable.
135
+ 4. Rotate the KEK (`OAUTH_SIGNING_KEK`) separately, every 90 days; re-encrypt stored private keys.
136
+
137
+ ### OauthStateStorePort
138
+
139
+ `AuthHonoPorts.oauthStateStore` must implement:
140
+
141
+ ```ts
142
+ interface OauthStateStorePort {
143
+ findClient(clientId: string): Promise<OauthClientRecord | null>;
144
+ saveAuthCode(code: string, payload: AuthCodePayload, ttlSec: number): Promise<void>;
145
+ consumeAuthCode(code: string): Promise<AuthCodePayload | null>; // atomic single-use
146
+ saveTokenMeta(jti: string, meta: TokenMeta, ttlSec: number): Promise<void>;
147
+ findTokenMeta(jti: string): Promise<TokenMeta | null>;
148
+ revokeToken(jti: string): Promise<void>;
149
+ isTokenRevoked(jti: string): Promise<boolean>;
150
+ recordDpopJti(jti: string, expiresAt: Date): Promise<boolean>; // false = duplicate
151
+ purgeExpired(): Promise<number>;
152
+ }
153
+ ```
154
+
155
+ The package never imports Postgres or any persistence library. Sentropic supplies `api/src/services/auth/oauth-state-adapter.ts` (Drizzle/Postgres). Package tests use the in-memory fixture at `packages/auth-hono/tests/__fixtures__/memory-oauth-state-store.ts`.
156
+
157
+ ### DPoP opt-in (RFC 9449)
158
+
159
+ Set `dpop_bound_access_tokens: true` on the OAuth client record. Bound clients must send a `DPoP: <proof-jwt>` header on `/token`, `/userinfo`, and `/revoke`. The IdP verifies `htm`, `htu`, `iat` skew, unique proof `jti`, and `ath` on resource calls. Access and ID tokens include `cnf.jkt`.
160
+
161
+ ### Claims and ACR levels
162
+
163
+ | Claim | Source | Notes |
164
+ | --- | --- | --- |
165
+ | `acr` | Session type | `urn:sentropic:loa:passkey-fresh` (passkey session), `urn:sentropic:loa:bearer` (magic-link session) |
166
+ | `auth_time` | `session.createdAt` | Strong-auth timestamp tracking lands in BR-39j |
167
+ | `tenant_id` | `oauth_clients.tenant_id` | Forward-compat column; real tenants in BR-39e |
168
+
169
+ ### Required environment variables
170
+
171
+ | Variable | Description | Default |
172
+ | --- | --- | --- |
173
+ | `OAUTH_SIGNING_KEK` | Passphrase for Postgres `pgp_sym_encrypt` of private key | **Required in production** — see `docs/secrets.md` |
174
+ | `OAUTH_ISSUER_URL` | Override issuer claim | Derived from `AUTH_CALLBACK_BASE_URL` |
175
+ | `OAUTH_ACCESS_TOKEN_TTL_SEC` | Access token lifetime | `3600` |
176
+ | `OAUTH_ID_TOKEN_TTL_SEC` | ID token lifetime | `3600` |
177
+ | `OAUTH_AUTHORIZATION_CODE_TTL_SEC` | Authorization code TTL | `60` |
178
+ | `OAUTH_DPOP_IAT_SKEW_SEC` | DPoP proof `iat` tolerance | `60` |
179
+
180
+ ### End-to-end example
181
+
182
+ See `packages/auth-hono/tests/example-oauth-rp.test.ts` for a complete in-process test that walks the full flow: authorize → consent → callback → token → userinfo → revoke → userinfo 401.
183
+
71
184
  ## First Publish
72
185
 
73
186
  This is a brand-new public package. First publish requires the one-shot bootstrap flow from `rules/workflow.md`: trigger `ci.yml` with `bootstrap_publish_target=auth-hono`, handle any npm token or 2FA requirement with the npm owner, then attach the npm OIDC trusted publisher for `rhanka/sentropic` workflow `ci.yml`. Steady-state publishes should use trusted publishing and skip if the version already exists.
74
187
 
75
188
  ## Versioning
76
189
 
77
- This branch ships `0.2.1`:
190
+ This branch ships `0.3.0`:
78
191
 
79
192
  - `0.2.0` adds `AuthHonoRouteHandlerError` short-circuit on WebAuthn prepare/resolve hooks and the `finalizeRegistration`/`finalizeAuthentication` post-verify hooks. Additive; existing handler signatures stay valid.
80
193
  - `0.2.1` patches `extractChallenge` (both WebAuthn handlers) to handle `credential.response === null` defensively (returns 400 `invalid_credential` instead of throwing 500).
194
+ - `0.3.0` adds the OAuth2/OIDC IdP surface: `createOAuthRouter`, `createWellKnownRouter`, `createJwksService`, `OauthStateStorePort`, `JwksPort`, Ed25519 signing, DPoP opt-in, and all six OAuth endpoints. Additive; existing WebAuthn/session handler signatures unchanged.
@@ -59,6 +59,6 @@ export declare const AUTH_HONO_ROUTE_MAP: {
59
59
  readonly path: "/credentials/:id";
60
60
  };
61
61
  };
62
- export declare const AUTH_HONO_REQUIRED_PORTS: readonly ["users", "credentials", "challenges", "sessions", "emailVerification", "magicLinks", "emailDelivery", "cookies", "tokens", "auditLog", "clock", "random", "accountPolicy"];
62
+ export declare const AUTH_HONO_REQUIRED_PORTS: readonly ["users", "credentials", "challenges", "sessions", "emailVerification", "magicLinks", "emailDelivery", "cookies", "tokens", "auditLog", "clock", "random", "accountPolicy", "oauthStateStore", "jwks"];
63
63
  export type AuthHonoRequiredPort = (typeof AUTH_HONO_REQUIRED_PORTS)[number];
64
64
  //# sourceMappingURL=contracts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,yBAAyB,sTAc5B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9E,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDwC,CAAC;AAEzE,eAAO,MAAM,wBAAwB,sLAcgB,CAAC;AAEtD,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,yBAAyB,sTAc5B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9E,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDwC,CAAC;AAEzE,eAAO,MAAM,wBAAwB,iNAgBgB,CAAC;AAEtD,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC"}
package/dist/contracts.js CHANGED
@@ -81,5 +81,7 @@ export const AUTH_HONO_REQUIRED_PORTS = [
81
81
  'clock',
82
82
  'random',
83
83
  'accountPolicy',
84
+ 'oauthStateStore',
85
+ 'jwks',
84
86
  ];
85
87
  //# sourceMappingURL=contracts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,kBAAkB;IAClB,iBAAiB;IACjB,kBAAkB;IAClB,iBAAiB;IACjB,kCAAkC;IAClC,2BAA2B;IAC3B,oCAAoC;IACpC,6BAA6B;IAC7B,gBAAgB;IAChB,QAAQ;IACR,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;CACV,CAAC;AAWX,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,uBAAuB;KAC9B;IACD,eAAe,EAAE;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,oBAAoB;KAC3B;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,qBAAqB;KAC5B;IACD,eAAe,EAAE;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,oBAAoB;KAC3B;IACD,gCAAgC,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,mBAAmB;KAC1B;IACD,yBAAyB,EAAE;QACzB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,kBAAkB;KACzB;IACD,kCAAkC,EAAE;QAClC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,gBAAgB;KACvB;IACD,2BAA2B,EAAE;QAC3B,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,eAAe;KACtB;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,kBAAkB;KACzB;IACD,MAAM,EAAE;QACN,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,UAAU;KACjB;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,cAAc;KACrB;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,kBAAkB;KACzB;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,kBAAkB;KACzB;CACqE,CAAC;AAEzE,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,OAAO;IACP,aAAa;IACb,YAAY;IACZ,UAAU;IACV,mBAAmB;IACnB,YAAY;IACZ,eAAe;IACf,SAAS;IACT,QAAQ;IACR,UAAU;IACV,OAAO;IACP,QAAQ;IACR,eAAe;CACoC,CAAC"}
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,kBAAkB;IAClB,iBAAiB;IACjB,kBAAkB;IAClB,iBAAiB;IACjB,kCAAkC;IAClC,2BAA2B;IAC3B,oCAAoC;IACpC,6BAA6B;IAC7B,gBAAgB;IAChB,QAAQ;IACR,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;CACV,CAAC;AAWX,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,uBAAuB;KAC9B;IACD,eAAe,EAAE;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,oBAAoB;KAC3B;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,qBAAqB;KAC5B;IACD,eAAe,EAAE;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,oBAAoB;KAC3B;IACD,gCAAgC,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,mBAAmB;KAC1B;IACD,yBAAyB,EAAE;QACzB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,kBAAkB;KACzB;IACD,kCAAkC,EAAE;QAClC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,gBAAgB;KACvB;IACD,2BAA2B,EAAE;QAC3B,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,eAAe;KACtB;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,kBAAkB;KACzB;IACD,MAAM,EAAE;QACN,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,UAAU;KACjB;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,cAAc;KACrB;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,kBAAkB;KACzB;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,kBAAkB;KACzB;CACqE,CAAC;AAEzE,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,OAAO;IACP,aAAa;IACb,YAAY;IACZ,UAAU;IACV,mBAAmB;IACnB,YAAY;IACZ,eAAe;IACf,SAAS;IACT,QAAQ;IACR,UAAU;IACV,OAAO;IACP,QAAQ;IACR,eAAe;IACf,iBAAiB;IACjB,MAAM;CAC6C,CAAC"}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,21 @@ export * from './credential-route-handlers.js';
3
3
  export * from './email-verification.js';
4
4
  export * from './magic-link.js';
5
5
  export * from './middleware.js';
6
+ export * from './oauth/authorize-handler.js';
7
+ export * from './oauth/consent-decision-handler.js';
8
+ export * from './oauth/crypto-utils.js';
9
+ export * from './oauth/dpop.js';
10
+ export * from './oauth/http-utils.js';
11
+ export * from './oauth/introspect-handler.js';
12
+ export * from './oauth/jwks-service.js';
13
+ export * from './oauth/router.js';
14
+ export * from './oauth/revoke-handler.js';
15
+ export * from './oauth/session-resolver.js';
16
+ export * from './oauth/state-store-types.js';
17
+ export * from './oauth/state-codec.js';
18
+ export * from './oauth/token-handler.js';
19
+ export * from './oauth/userinfo-handler.js';
20
+ export * from './oauth/wellknown-handler.js';
6
21
  export * from './ports.js';
7
22
  export * from './route-handlers.js';
8
23
  export * from './router.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2CAA2C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2CAA2C,CAAC"}
package/dist/index.js CHANGED
@@ -3,6 +3,21 @@ export * from './credential-route-handlers.js';
3
3
  export * from './email-verification.js';
4
4
  export * from './magic-link.js';
5
5
  export * from './middleware.js';
6
+ export * from './oauth/authorize-handler.js';
7
+ export * from './oauth/consent-decision-handler.js';
8
+ export * from './oauth/crypto-utils.js';
9
+ export * from './oauth/dpop.js';
10
+ export * from './oauth/http-utils.js';
11
+ export * from './oauth/introspect-handler.js';
12
+ export * from './oauth/jwks-service.js';
13
+ export * from './oauth/router.js';
14
+ export * from './oauth/revoke-handler.js';
15
+ export * from './oauth/session-resolver.js';
16
+ export * from './oauth/state-store-types.js';
17
+ export * from './oauth/state-codec.js';
18
+ export * from './oauth/token-handler.js';
19
+ export * from './oauth/userinfo-handler.js';
20
+ export * from './oauth/wellknown-handler.js';
6
21
  export * from './ports.js';
7
22
  export * from './route-handlers.js';
8
23
  export * from './router.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2CAA2C,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2CAA2C,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { Context } from 'hono';
2
+ import type { AuthHonoPorts } from '../ports.js';
3
+ import type { OAuthContinuationCodec } from './state-codec.js';
4
+ export interface OAuthAuthorizeHandlerOptions {
5
+ consentUrl: string;
6
+ issuer: string;
7
+ loginUrl: string;
8
+ ports: AuthHonoPorts;
9
+ stateCodec: OAuthContinuationCodec;
10
+ stateTtlSeconds?: number;
11
+ }
12
+ export declare const createOAuthAuthorizeHandler: (options: OAuthAuthorizeHandlerOptions) => (c: Context) => Promise<Response>;
13
+ //# sourceMappingURL=authorize-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize-handler.d.ts","sourceRoot":"","sources":["../../src/oauth/authorize-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAA0B,MAAM,kBAAkB,CAAC;AAIvF,MAAM,WAAW,4BAA4B;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,EAAE,sBAAsB,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAYD,eAAO,MAAM,2BAA2B,YAC5B,4BAA4B,SAC5B,OAAO,KAAG,QAAQ,QAAQ,CAgCnC,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { appendParams, oauthJsonError, redirectWithOAuthError } from './http-utils.js';
2
+ import { resolveOAuthAcr, resolveOAuthSession } from './session-resolver.js';
3
+ export const createOAuthAuthorizeHandler = (options) => async (c) => {
4
+ const continuation = c.req.query('continue');
5
+ if (continuation) {
6
+ return resumeLoginContinuation(c, options, continuation);
7
+ }
8
+ const validation = await validateAuthorizeRequest(c, options.ports);
9
+ if (validation instanceof Response)
10
+ return validation;
11
+ const prompt = c.req.query('prompt') ?? '';
12
+ const session = await resolveOAuthSession(c.req.raw, options.ports);
13
+ if (!session || prompt === 'login') {
14
+ if (prompt === 'none') {
15
+ return redirectWithOAuthError(validation.redirectUri, 'login_required', validation.state, c.req.url);
16
+ }
17
+ const continuation = await sealContinuation(c, options, validation);
18
+ return c.redirect(appendParams(options.loginUrl, { continue: continuation }, c.req.url), 302);
19
+ }
20
+ if (prompt === 'none') {
21
+ return redirectWithOAuthError(validation.redirectUri, 'consent_required', validation.state, c.req.url);
22
+ }
23
+ const sealedState = await sealContinuation(c, options, validation, {
24
+ acr: resolveOAuthAcr(session.sessionRecord),
25
+ authTime: session.sessionRecord.createdAt.toISOString(),
26
+ userId: session.user.id,
27
+ });
28
+ return c.redirect(appendParams(options.consentUrl, { state: sealedState }, c.req.url), 302);
29
+ };
30
+ const resumeLoginContinuation = async (c, options, continuation) => {
31
+ const payload = await options.stateCodec.unseal(continuation);
32
+ const now = options.ports.clock.now();
33
+ if (!payload || payload.userId || payload.codeChallengeMethod !== 'S256' || new Date(payload.expiresAt) <= now) {
34
+ return oauthJsonError(c, 400, 'invalid_request', 'OAuth continuation is invalid or expired.');
35
+ }
36
+ const client = await options.ports.oauthStateStore.findClient(payload.clientId);
37
+ if (!client)
38
+ return oauthJsonError(c, 400, 'invalid_request', 'Unknown OAuth client.');
39
+ const redirectError = validateRedirectUri(client, payload.redirectUri);
40
+ if (redirectError)
41
+ return oauthJsonError(c, 400, 'invalid_request', redirectError);
42
+ const scopeResult = validateScope(payload.scope, client, payload.redirectUri, payload.state, c.req.url);
43
+ if (scopeResult instanceof Response)
44
+ return scopeResult;
45
+ const session = await resolveOAuthSession(c.req.raw, options.ports);
46
+ if (!session) {
47
+ return c.redirect(appendParams(options.loginUrl, { continue: continuation }, c.req.url), 302);
48
+ }
49
+ const expiresAt = options.ports.clock.addSeconds(now, options.stateTtlSeconds ?? 10 * 60);
50
+ const sealedState = await options.stateCodec.seal({
51
+ ...payload,
52
+ acr: resolveOAuthAcr(session.sessionRecord),
53
+ authTime: session.sessionRecord.createdAt.toISOString(),
54
+ createdAt: now.toISOString(),
55
+ expiresAt: expiresAt.toISOString(),
56
+ scope: scopeResult,
57
+ userId: session.user.id,
58
+ });
59
+ return c.redirect(appendParams(options.consentUrl, { state: sealedState }, c.req.url), 302);
60
+ };
61
+ const validateAuthorizeRequest = async (c, ports) => {
62
+ const clientId = c.req.query('client_id');
63
+ const client = clientId ? await ports.oauthStateStore.findClient(clientId) : null;
64
+ if (!client) {
65
+ return oauthJsonError(c, 400, 'invalid_request', 'Unknown OAuth client.');
66
+ }
67
+ const redirectUri = c.req.query('redirect_uri') ?? '';
68
+ const redirectError = validateRedirectUri(client, redirectUri);
69
+ if (redirectError) {
70
+ return oauthJsonError(c, 400, 'invalid_request', redirectError);
71
+ }
72
+ const state = c.req.query('state') ?? null;
73
+ if (c.req.query('response_type') !== 'code') {
74
+ return redirectWithOAuthError(redirectUri, 'unsupported_response_type', state, c.req.url);
75
+ }
76
+ const codeChallenge = c.req.query('code_challenge') ?? '';
77
+ if (!codeChallenge || c.req.query('code_challenge_method') !== 'S256') {
78
+ return redirectWithOAuthError(redirectUri, 'invalid_request', state, c.req.url);
79
+ }
80
+ const scopeResult = validateScope(c.req.query('scope') ?? '', client, redirectUri, state, c.req.url);
81
+ if (scopeResult instanceof Response)
82
+ return scopeResult;
83
+ return {
84
+ client,
85
+ codeChallenge,
86
+ dpopJkt: c.req.query('dpop_jkt') ?? null,
87
+ nonce: c.req.query('nonce') ?? null,
88
+ redirectUri,
89
+ scope: scopeResult,
90
+ state,
91
+ };
92
+ };
93
+ const validateRedirectUri = (client, redirectUri) => {
94
+ if (!client.redirectUris.includes(redirectUri))
95
+ return 'redirect_uri is not registered for this client.';
96
+ let parsed;
97
+ try {
98
+ parsed = new URL(redirectUri);
99
+ }
100
+ catch {
101
+ return 'redirect_uri must be an absolute URI.';
102
+ }
103
+ if (parsed.hash)
104
+ return 'redirect_uri must not contain a fragment.';
105
+ if (parsed.username || parsed.password)
106
+ return 'redirect_uri must not contain credentials.';
107
+ if (parsed.protocol === 'https:')
108
+ return null;
109
+ if (parsed.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(parsed.hostname))
110
+ return null;
111
+ return 'redirect_uri must use https except for localhost development callbacks.';
112
+ };
113
+ const validateScope = (scope, client, redirectUri, state, baseUrl) => {
114
+ const requestedScopes = scope.split(/\s+/).filter(Boolean);
115
+ if (requestedScopes.includes('offline_access')) {
116
+ return redirectWithOAuthError(redirectUri, 'invalid_scope', state, baseUrl);
117
+ }
118
+ if (requestedScopes.some((requestedScope) => !client.allowedScopes.includes(requestedScope))) {
119
+ return redirectWithOAuthError(redirectUri, 'invalid_scope', state, baseUrl);
120
+ }
121
+ return requestedScopes.join(' ');
122
+ };
123
+ const sealContinuation = async (c, options, request, session) => {
124
+ const now = options.ports.clock.now();
125
+ const expiresAt = options.ports.clock.addSeconds(now, options.stateTtlSeconds ?? 10 * 60);
126
+ return options.stateCodec.seal({
127
+ acr: session?.acr,
128
+ authTime: session?.authTime,
129
+ clientId: request.client.clientId,
130
+ codeChallenge: request.codeChallenge,
131
+ codeChallengeMethod: 'S256',
132
+ createdAt: now.toISOString(),
133
+ dpopJkt: request.dpopJkt,
134
+ expiresAt: expiresAt.toISOString(),
135
+ nonce: request.nonce,
136
+ redirectUri: request.redirectUri,
137
+ scope: request.scope,
138
+ state: request.state,
139
+ tenantId: request.client.tenantId,
140
+ userId: session?.userId,
141
+ });
142
+ };
143
+ //# sourceMappingURL=authorize-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize-handler.js","sourceRoot":"","sources":["../../src/oauth/authorize-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Context } from 'hono';
2
+ import type { AuthHonoPorts } from '../ports.js';
3
+ import type { OAuthContinuationCodec } from './state-codec.js';
4
+ export interface OAuthConsentHandlerOptions {
5
+ authorizationCodeTtlSeconds?: number;
6
+ ports: AuthHonoPorts;
7
+ stateCodec: OAuthContinuationCodec;
8
+ }
9
+ export declare const createOAuthConsentDetailsHandler: (options: OAuthConsentHandlerOptions) => (c: Context) => Promise<Response>;
10
+ export declare const createOAuthConsentDecisionHandler: (options: OAuthConsentHandlerOptions) => (c: Context) => Promise<Response>;
11
+ //# sourceMappingURL=consent-decision-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-decision-handler.d.ts","sourceRoot":"","sources":["../../src/oauth/consent-decision-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAA0B,MAAM,kBAAkB,CAAC;AAGvF,MAAM,WAAW,0BAA0B;IACzC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,EAAE,sBAAsB,CAAC;CACpC;AAED,eAAO,MAAM,gCAAgC,YACjC,0BAA0B,SAC1B,OAAO,KAAG,QAAQ,QAAQ,CAanC,CAAC;AAEJ,eAAO,MAAM,iCAAiC,YAClC,0BAA0B,SAC1B,OAAO,KAAG,QAAQ,QAAQ,CA0CnC,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { appendParams, oauthJsonError, redirectOrJson } from './http-utils.js';
2
+ import { resolveOAuthSession } from './session-resolver.js';
3
+ export const createOAuthConsentDetailsHandler = (options) => async (c) => {
4
+ const state = c.req.query('state') ?? '';
5
+ const payload = await validateConsentState(c, options, state);
6
+ if (payload instanceof Response)
7
+ return payload;
8
+ const client = await options.ports.oauthStateStore.findClient(payload.clientId);
9
+ if (!client)
10
+ return oauthJsonError(c, 400, 'invalid_request', 'Unknown OAuth client.');
11
+ return c.json({
12
+ clientName: client.name,
13
+ redirectUri: payload.redirectUri,
14
+ scopes: payload.scope.split(/\s+/).filter(Boolean),
15
+ });
16
+ };
17
+ export const createOAuthConsentDecisionHandler = (options) => async (c) => {
18
+ const body = await c.req.json().catch(() => null);
19
+ if (!body?.state || !['approve', 'deny'].includes(body.decision ?? '')) {
20
+ return oauthJsonError(c, 400, 'invalid_request', 'Consent decision and state are required.');
21
+ }
22
+ const payload = await validateConsentState(c, options, body.state);
23
+ if (payload instanceof Response)
24
+ return payload;
25
+ if (body.decision === 'deny') {
26
+ return redirectOrJson(c, appendParams(payload.redirectUri, { error: 'access_denied', state: payload.state }, c.req.url));
27
+ }
28
+ const code = options.ports.random.token(32);
29
+ const now = options.ports.clock.now();
30
+ await options.ports.oauthStateStore.saveAuthCode(code, {
31
+ acr: payload.acr ?? 'urn:sentropic:loa:bearer',
32
+ authTime: new Date(payload.authTime ?? now.toISOString()),
33
+ clientId: payload.clientId,
34
+ codeChallenge: payload.codeChallenge,
35
+ codeChallengeMethod: 'S256',
36
+ createdAt: now,
37
+ dpopJkt: payload.dpopJkt,
38
+ expiresAt: options.ports.clock.addSeconds(now, options.authorizationCodeTtlSeconds ?? 60),
39
+ nonce: payload.nonce,
40
+ redirectUri: payload.redirectUri,
41
+ scope: payload.scope,
42
+ tenantId: payload.tenantId,
43
+ userId: payload.userId ?? '',
44
+ }, options.authorizationCodeTtlSeconds ?? 60);
45
+ return redirectOrJson(c, appendParams(payload.redirectUri, { code, state: payload.state }, c.req.url));
46
+ };
47
+ const validateConsentState = async (c, options, sealedState) => {
48
+ const payload = await options.stateCodec.unseal(sealedState);
49
+ if (!payload || !payload.userId || new Date(payload.expiresAt) <= options.ports.clock.now()) {
50
+ return oauthJsonError(c, 400, 'invalid_request', 'OAuth consent state is invalid or expired.');
51
+ }
52
+ const session = await resolveOAuthSession(c.req.raw, options.ports);
53
+ if (!session || session.user.id !== payload.userId) {
54
+ return oauthJsonError(c, 401, 'login_required', 'A valid user session is required.');
55
+ }
56
+ return payload;
57
+ };
58
+ //# sourceMappingURL=consent-decision-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-decision-handler.js","sourceRoot":"","sources":["../../src/oauth/consent-decision-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare const sha256Base64url: (value: string) => Promise<string>;
2
+ export declare const base64urlEncode: (bytes: Uint8Array) => string;
3
+ //# sourceMappingURL=crypto-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto-utils.d.ts","sourceRoot":"","sources":["../../src/oauth/crypto-utils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe,UAAiB,MAAM,KAAG,QAAQ,MAAM,CAGnE,CAAC;AAEF,eAAO,MAAM,eAAe,UAAW,UAAU,KAAG,MAMnD,CAAC"}
@@ -0,0 +1,13 @@
1
+ const textEncoder = new TextEncoder();
2
+ export const sha256Base64url = async (value) => {
3
+ const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(value));
4
+ return base64urlEncode(new Uint8Array(digest));
5
+ };
6
+ export const base64urlEncode = (bytes) => {
7
+ let binary = '';
8
+ for (const byte of bytes) {
9
+ binary += String.fromCharCode(byte);
10
+ }
11
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
12
+ };
13
+ //# sourceMappingURL=crypto-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto-utils.js","sourceRoot":"","sources":["../../src/oauth/crypto-utils.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAEtC,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,KAAa,EAAmB,EAAE;IACtE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAChF,OAAO,eAAe,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AACjD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAiB,EAAU,EAAE;IAC3D,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACpF,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { AuthHonoPorts } from '../ports.js';
2
+ export interface VerifyDpopProofOptions {
3
+ accessToken?: string;
4
+ htm: string;
5
+ htu: string;
6
+ iatSkewSeconds?: number;
7
+ ports: AuthHonoPorts;
8
+ proof: string;
9
+ }
10
+ export interface VerifiedDpopProof {
11
+ jkt: string;
12
+ jti: string;
13
+ }
14
+ export declare class OAuthDpopProofError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ export declare const verifyOAuthDpopProof: (options: VerifyDpopProofOptions) => Promise<VerifiedDpopProof>;
18
+ //# sourceMappingURL=dpop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../../src/oauth/dpop.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,eAAO,MAAM,oBAAoB,YACtB,sBAAsB,KAC9B,QAAQ,iBAAiB,CAwB3B,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { calculateJwkThumbprint, decodeProtectedHeader, importJWK, jwtVerify, } from 'jose';
2
+ import { sha256Base64url } from './crypto-utils.js';
3
+ export class OAuthDpopProofError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'OAuthDpopProofError';
7
+ }
8
+ }
9
+ export const verifyOAuthDpopProof = async (options) => {
10
+ const header = decodeProtectedHeader(options.proof);
11
+ const publicJwk = header.jwk;
12
+ if (!publicJwk || !header.alg || header.typ !== 'dpop+jwt') {
13
+ throw new OAuthDpopProofError('DPoP proof header is invalid.');
14
+ }
15
+ const key = await importJWK(publicJwk, header.alg);
16
+ const { payload } = await jwtVerify(options.proof, key);
17
+ await validateDpopPayload(payload, options);
18
+ const expiresAt = options.ports.clock.addSeconds(options.ports.clock.now(), options.iatSkewSeconds ?? 60);
19
+ const recorded = await options.ports.oauthStateStore.recordDpopJti(String(payload.jti), expiresAt);
20
+ if (!recorded) {
21
+ throw new OAuthDpopProofError('DPoP proof jti was already used.');
22
+ }
23
+ return {
24
+ jkt: await calculateJwkThumbprint(publicJwk),
25
+ jti: String(payload.jti),
26
+ };
27
+ };
28
+ const validateDpopPayload = async (payload, options) => {
29
+ if (payload.htm !== options.htm.toUpperCase()) {
30
+ throw new OAuthDpopProofError('DPoP htm claim does not match the request method.');
31
+ }
32
+ if (payload.htu !== options.htu) {
33
+ throw new OAuthDpopProofError('DPoP htu claim does not match the request URL.');
34
+ }
35
+ if (!payload.jti || typeof payload.jti !== 'string') {
36
+ throw new OAuthDpopProofError('DPoP jti claim is required.');
37
+ }
38
+ if (typeof payload.iat !== 'number') {
39
+ throw new OAuthDpopProofError('DPoP iat claim is required.');
40
+ }
41
+ const nowSeconds = Math.floor(options.ports.clock.now().getTime() / 1000);
42
+ if (Math.abs(payload.iat - nowSeconds) > (options.iatSkewSeconds ?? 60)) {
43
+ throw new OAuthDpopProofError('DPoP iat claim is outside the allowed skew.');
44
+ }
45
+ if (options.accessToken) {
46
+ await validateAth(payload, options.accessToken);
47
+ }
48
+ };
49
+ const validateAth = async (payload, accessToken) => {
50
+ if (payload.ath !== (await sha256Base64url(accessToken))) {
51
+ throw new OAuthDpopProofError('DPoP ath claim does not match the access token.');
52
+ }
53
+ };
54
+ //# sourceMappingURL=dpop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpop.js","sourceRoot":"","sources":["../../src/oauth/dpop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { Context } from 'hono';
2
+ export declare const appendParams: (target: string, params: Record<string, string | null | undefined>, baseUrl: string) => string;
3
+ export declare const oauthJsonError: (c: Context, status: 400 | 401, code: string, message: string) => Response;
4
+ export declare const redirectWithOAuthError: (redirectUri: string, code: string, state: string | null, baseUrl: string) => Response;
5
+ export declare const redirectOrJson: (c: Context, redirectTo: string) => Response;
6
+ //# sourceMappingURL=http-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-utils.d.ts","sourceRoot":"","sources":["../../src/oauth/http-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,eAAO,MAAM,YAAY,WACf,MAAM,UACN,OAAO,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,WACxC,MAAM,KACd,MAQF,CAAC;AAEF,eAAO,MAAM,cAAc,MACtB,OAAO,UACF,GAAG,GAAG,GAAG,QACX,MAAM,WACH,MAAM,KACd,QASA,CAAC;AAEJ,eAAO,MAAM,sBAAsB,gBACpB,MAAM,QACb,MAAM,SACL,MAAM,GAAG,IAAI,WACX,MAAM,KACd,QAWA,CAAC;AAEJ,eAAO,MAAM,cAAc,MAAO,OAAO,cAAc,MAAM,KAAG,QAO/D,CAAC"}