@sentropic/auth-hono 0.2.1 → 0.4.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 (95) hide show
  1. package/README.md +168 -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 +16 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +16 -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/service-auth-middleware.d.ts +30 -0
  47. package/dist/oauth/service-auth-middleware.d.ts.map +1 -0
  48. package/dist/oauth/service-auth-middleware.js +170 -0
  49. package/dist/oauth/service-auth-middleware.js.map +1 -0
  50. package/dist/oauth/session-resolver.d.ts +9 -0
  51. package/dist/oauth/session-resolver.d.ts.map +1 -0
  52. package/dist/oauth/session-resolver.js +28 -0
  53. package/dist/oauth/session-resolver.js.map +1 -0
  54. package/dist/oauth/state-codec.d.ts +25 -0
  55. package/dist/oauth/state-codec.d.ts.map +1 -0
  56. package/dist/oauth/state-codec.js +60 -0
  57. package/dist/oauth/state-codec.js.map +1 -0
  58. package/dist/oauth/state-store-types.d.ts +100 -0
  59. package/dist/oauth/state-store-types.d.ts.map +1 -0
  60. package/dist/oauth/state-store-types.js +2 -0
  61. package/dist/oauth/state-store-types.js.map +1 -0
  62. package/dist/oauth/token-handler.d.ts +12 -0
  63. package/dist/oauth/token-handler.d.ts.map +1 -0
  64. package/dist/oauth/token-handler.js +294 -0
  65. package/dist/oauth/token-handler.js.map +1 -0
  66. package/dist/oauth/userinfo-handler.d.ts +9 -0
  67. package/dist/oauth/userinfo-handler.d.ts.map +1 -0
  68. package/dist/oauth/userinfo-handler.js +93 -0
  69. package/dist/oauth/userinfo-handler.js.map +1 -0
  70. package/dist/oauth/wellknown-handler.d.ts +9 -0
  71. package/dist/oauth/wellknown-handler.d.ts.map +1 -0
  72. package/dist/oauth/wellknown-handler.js +37 -0
  73. package/dist/oauth/wellknown-handler.js.map +1 -0
  74. package/dist/ports.d.ts +4 -0
  75. package/dist/ports.d.ts.map +1 -1
  76. package/package.json +1 -1
  77. package/src/contracts.ts +2 -0
  78. package/src/index.ts +16 -0
  79. package/src/oauth/authorize-handler.ts +201 -0
  80. package/src/oauth/consent-decision-handler.ts +93 -0
  81. package/src/oauth/crypto-utils.ts +14 -0
  82. package/src/oauth/dpop.ts +93 -0
  83. package/src/oauth/http-utils.ts +58 -0
  84. package/src/oauth/introspect-handler.ts +88 -0
  85. package/src/oauth/jwks-service.ts +103 -0
  86. package/src/oauth/revoke-handler.ts +70 -0
  87. package/src/oauth/router.ts +42 -0
  88. package/src/oauth/service-auth-middleware.ts +250 -0
  89. package/src/oauth/session-resolver.ts +48 -0
  90. package/src/oauth/state-codec.ts +98 -0
  91. package/src/oauth/state-store-types.ts +109 -0
  92. package/src/oauth/token-handler.ts +423 -0
  93. package/src/oauth/userinfo-handler.ts +129 -0
  94. package/src/oauth/wellknown-handler.ts +52 -0
  95. package/src/ports.ts +17 -0
@@ -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"}
@@ -0,0 +1,27 @@
1
+ export const appendParams = (target, params, baseUrl) => {
2
+ const url = new URL(target, baseUrl);
3
+ for (const [key, value] of Object.entries(params)) {
4
+ if (value !== null && value !== undefined) {
5
+ url.searchParams.set(key, value);
6
+ }
7
+ }
8
+ return url.toString();
9
+ };
10
+ export const oauthJsonError = (c, status, code, message) => c.json({
11
+ error: {
12
+ code,
13
+ message,
14
+ },
15
+ }, status);
16
+ export const redirectWithOAuthError = (redirectUri, code, state, baseUrl) => Response.redirect(appendParams(redirectUri, {
17
+ error: code,
18
+ state,
19
+ }, baseUrl), 302);
20
+ export const redirectOrJson = (c, redirectTo) => {
21
+ const accept = c.req.header('accept') ?? '';
22
+ if (accept.includes('application/json')) {
23
+ return c.json({ redirectTo });
24
+ }
25
+ return c.redirect(redirectTo, 302);
26
+ };
27
+ //# sourceMappingURL=http-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-utils.js","sourceRoot":"","sources":["../../src/oauth/http-utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,MAAc,EACd,MAAiD,EACjD,OAAe,EACP,EAAE;IACV,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC1C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,CAAU,EACV,MAAiB,EACjB,IAAY,EACZ,OAAe,EACL,EAAE,CACZ,CAAC,CAAC,IAAI,CACJ;IACE,KAAK,EAAE;QACL,IAAI;QACJ,OAAO;KACR;CACF,EACD,MAAM,CACP,CAAC;AAEJ,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,WAAmB,EACnB,IAAY,EACZ,KAAoB,EACpB,OAAe,EACL,EAAE,CACZ,QAAQ,CAAC,QAAQ,CACf,YAAY,CACV,WAAW,EACX;IACE,KAAK,EAAE,IAAI;IACX,KAAK;CACN,EACD,OAAO,CACR,EACD,GAAG,CACJ,CAAC;AAEJ,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAU,EAAE,UAAkB,EAAY,EAAE;IACzE,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACxC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,CAAC,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AACrC,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Context } from 'hono';
2
+ import type { AuthHonoPorts } from '../ports.js';
3
+ export interface OAuthIntrospectHandlerOptions {
4
+ issuer: string;
5
+ ports: AuthHonoPorts;
6
+ }
7
+ export declare const createOAuthIntrospectHandler: (options: OAuthIntrospectHandlerOptions) => (c: Context) => Promise<Response>;
8
+ //# sourceMappingURL=introspect-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspect-handler.d.ts","sourceRoot":"","sources":["../../src/oauth/introspect-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,eAAO,MAAM,4BAA4B,YAC7B,6BAA6B,SAC7B,OAAO,KAAG,QAAQ,QAAQ,CAgCnC,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { oauthJsonError } from './http-utils.js';
2
+ import { createJwksService } from './jwks-service.js';
3
+ export const createOAuthIntrospectHandler = (options) => async (c) => {
4
+ const authenticated = await authenticateIntrospectionClient(c, options.ports);
5
+ if (authenticated instanceof Response)
6
+ return authenticated;
7
+ const form = new URLSearchParams(await c.req.text());
8
+ const token = form.get('token');
9
+ if (!token)
10
+ return c.json({ active: false });
11
+ const payload = await verifyToken(options, token);
12
+ if (!payload?.jti)
13
+ return c.json({ active: false });
14
+ const meta = await options.ports.oauthStateStore.findTokenMeta(payload.jti);
15
+ if (!meta ||
16
+ meta.expiresAt <= options.ports.clock.now() ||
17
+ (await options.ports.oauthStateStore.isTokenRevoked(payload.jti))) {
18
+ return c.json({ active: false });
19
+ }
20
+ return c.json({
21
+ active: true,
22
+ aud: meta.audience,
23
+ client_id: meta.clientId,
24
+ ...(meta.dpopJkt ? { cnf: { jkt: meta.dpopJkt } } : {}),
25
+ exp: toEpochSeconds(meta.expiresAt),
26
+ iat: typeof payload.iat === 'number' ? payload.iat : undefined,
27
+ jti: meta.jti,
28
+ scope: meta.scope,
29
+ sub: meta.userId,
30
+ token_type: meta.tokenType,
31
+ });
32
+ };
33
+ const authenticateIntrospectionClient = async (c, ports) => {
34
+ const authorization = c.req.header('authorization');
35
+ if (!authorization?.startsWith('Basic ')) {
36
+ return oauthJsonError(c, 401, 'invalid_client', 'Client authentication is required.');
37
+ }
38
+ const decoded = atob(authorization.slice('Basic '.length));
39
+ const separator = decoded.indexOf(':');
40
+ const clientId = separator >= 0 ? decoded.slice(0, separator) : decoded;
41
+ const secret = separator >= 0 ? decoded.slice(separator + 1) : '';
42
+ const client = await ports.oauthStateStore.findClient(clientId);
43
+ if (!client?.clientSecretHash || (await ports.tokens.hashSecret(secret)) !== client.clientSecretHash) {
44
+ return oauthJsonError(c, 401, 'invalid_client', 'Client authentication failed.');
45
+ }
46
+ return true;
47
+ };
48
+ const verifyToken = async (options, token) => {
49
+ try {
50
+ const jwks = createJwksService({ clock: options.ports.clock, jwksPort: options.ports.jwks });
51
+ const result = await jwks.verifyJwt(token, {
52
+ currentDate: options.ports.clock.now(),
53
+ issuer: trimTrailingSlash(options.issuer),
54
+ });
55
+ return result.payload;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ };
61
+ const toEpochSeconds = (date) => Math.floor(date.getTime() / 1000);
62
+ const trimTrailingSlash = (value) => value.replace(/\/+$/u, '');
63
+ //# sourceMappingURL=introspect-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspect-handler.js","sourceRoot":"","sources":["../../src/oauth/introspect-handler.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAOtD,MAAM,CAAC,MAAM,4BAA4B,GACvC,CAAC,OAAsC,EAAE,EAAE,CAC3C,KAAK,EAAE,CAAU,EAAqB,EAAE;IACtC,MAAM,aAAa,GAAG,MAAM,+BAA+B,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9E,IAAI,aAAa,YAAY,QAAQ;QAAE,OAAO,aAAa,CAAC;IAE5D,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAE7C,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,GAAG;QAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAEpD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5E,IACE,CAAC,IAAI;QACL,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;QAC3C,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EACjE,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,CAAC,CAAC,IAAI,CAAC;QACZ,MAAM,EAAE,IAAI;QACZ,GAAG,EAAE,IAAI,CAAC,QAAQ;QAClB,SAAS,EAAE,IAAI,CAAC,QAAQ;QACxB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,GAAG,EAAE,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC;QACnC,GAAG,EAAE,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;QAC9D,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,GAAG,EAAE,IAAI,CAAC,MAAM;QAChB,UAAU,EAAE,IAAI,CAAC,SAAS;KAC3B,CAAC,CAAC;AACL,CAAC,CAAC;AAEJ,MAAM,+BAA+B,GAAG,KAAK,EAC3C,CAAU,EACV,KAAoB,EACM,EAAE;IAC5B,MAAM,aAAa,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACpD,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,OAAO,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,oCAAoC,CAAC,CAAC;IACxF,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACxE,MAAM,MAAM,GAAG,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAChE,IAAI,CAAC,MAAM,EAAE,gBAAgB,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC,gBAAgB,EAAE,CAAC;QACrG,OAAO,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,+BAA+B,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,KAAK,EACvB,OAAsC,EACtC,KAAa,EACe,EAAE;IAC9B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,iBAAiB,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7F,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE;YACzC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;YACtC,MAAM,EAAE,iBAAiB,CAAC,OAAO,CAAC,MAAM,CAAC;SAC1C,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,IAAU,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;AAEjF,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type JWTVerifyOptions, type JWTVerifyResult, type JWTPayload } from 'jose';
2
+ import type { AuthHonoClockPort } from '../ports.js';
3
+ import type { JwksPort } from './state-store-types.js';
4
+ export interface CreateJwksServiceOptions {
5
+ clock: AuthHonoClockPort;
6
+ jwksPort: JwksPort;
7
+ }
8
+ export interface JwksSignOptions {
9
+ audience?: string | string[];
10
+ expiresAt?: Date;
11
+ issuer?: string;
12
+ jti?: string;
13
+ subject?: string;
14
+ type?: string;
15
+ }
16
+ export interface PublicJwks {
17
+ keys: Array<Record<string, unknown>>;
18
+ }
19
+ export interface JwksService {
20
+ getPublicJwks(): Promise<PublicJwks>;
21
+ signJwt(payload: JWTPayload, options?: JwksSignOptions): Promise<string>;
22
+ verifyJwt<T extends JWTPayload = JWTPayload>(jwt: string, options?: JWTVerifyOptions): Promise<JWTVerifyResult<T>>;
23
+ }
24
+ export declare const createJwksService: ({ clock, jwksPort }: CreateJwksServiceOptions) => JwksService;
25
+ //# sourceMappingURL=jwks-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwks-service.d.ts","sourceRoot":"","sources":["../../src/oauth/jwks-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IACrC,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE,SAAS,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,EACzC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,iBAAiB,wBAAyB,wBAAwB,KAAG,WA4DhF,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { decodeProtectedHeader, importJWK, jwtVerify, SignJWT, } from 'jose';
2
+ export const createJwksService = ({ clock, jwksPort }) => ({
3
+ async getPublicJwks() {
4
+ const keys = await jwksPort.listPublicKeys();
5
+ return {
6
+ keys: keys.map((key) => {
7
+ const { d: _privateExponent, ...publicJwk } = key.publicJwk;
8
+ return {
9
+ ...publicJwk,
10
+ alg: key.alg,
11
+ crv: key.crv,
12
+ kid: key.kid,
13
+ kty: publicJwk.kty,
14
+ status: key.active ? 'active' : 'rotated',
15
+ use: 'sig',
16
+ };
17
+ }),
18
+ };
19
+ },
20
+ async signJwt(payload, options = {}) {
21
+ const activeKey = await jwksPort.getActiveKey();
22
+ if (!activeKey) {
23
+ throw new Error('No active JWKS signing key is configured.');
24
+ }
25
+ if (!activeKey.privateKey) {
26
+ throw new Error(`Active JWKS signing key ${activeKey.kid} has no private key material.`);
27
+ }
28
+ let jwt = new SignJWT(payload).setProtectedHeader({
29
+ alg: activeKey.alg,
30
+ kid: activeKey.kid,
31
+ ...(options.type ? { typ: options.type } : {}),
32
+ });
33
+ jwt = jwt.setIssuedAt(toEpochSeconds(clock.now()));
34
+ if (options.audience)
35
+ jwt = jwt.setAudience(options.audience);
36
+ if (options.expiresAt)
37
+ jwt = jwt.setExpirationTime(toEpochSeconds(options.expiresAt));
38
+ if (options.issuer)
39
+ jwt = jwt.setIssuer(options.issuer);
40
+ if (options.jti)
41
+ jwt = jwt.setJti(options.jti);
42
+ if (options.subject)
43
+ jwt = jwt.setSubject(options.subject);
44
+ return jwt.sign(activeKey.privateKey);
45
+ },
46
+ async verifyJwt(jwt, options = {}) {
47
+ const protectedHeader = decodeProtectedHeader(jwt);
48
+ const kid = protectedHeader.kid;
49
+ if (!kid) {
50
+ throw new Error('JWT protected header is missing kid.');
51
+ }
52
+ const key = await jwksPort.findKeyByKid(kid);
53
+ if (!key) {
54
+ throw new Error(`Unknown JWKS kid: ${kid}`);
55
+ }
56
+ const publicKey = await importJWK(key.publicJwk, key.alg);
57
+ return jwtVerify(jwt, publicKey, options);
58
+ },
59
+ });
60
+ const toEpochSeconds = (date) => Math.floor(date.getTime() / 1000);
61
+ //# sourceMappingURL=jwks-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwks-service.js","sourceRoot":"","sources":["../../src/oauth/jwks-service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,SAAS,EACT,OAAO,GAIR,MAAM,MAAM,CAAC;AAgCd,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,EAA4B,EAAe,EAAE,CAAC,CAAC;IAChG,KAAK,CAAC,aAAa;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,cAAc,EAAE,CAAC;QAE7C,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBACrB,MAAM,EAAE,CAAC,EAAE,gBAAgB,EAAE,GAAG,SAAS,EAAE,GAAG,GAAG,CAAC,SAAS,CAAC;gBAC5D,OAAO;oBACL,GAAG,SAAS;oBACZ,GAAG,EAAE,GAAG,CAAC,GAAG;oBACZ,GAAG,EAAE,GAAG,CAAC,GAAG;oBACZ,GAAG,EAAE,GAAG,CAAC,GAAG;oBACZ,GAAG,EAAE,SAAS,CAAC,GAAG;oBAClB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;oBACzC,GAAG,EAAE,KAAK;iBACX,CAAC;YACJ,CAAC,CAAC;SACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,EAAE;QACjC,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,CAAC,GAAG,+BAA+B,CAAC,CAAC;QAC3F,CAAC;QAED,IAAI,GAAG,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,kBAAkB,CAAC;YAChD,GAAG,EAAE,SAAS,CAAC,GAAG;YAClB,GAAG,EAAE,SAAS,CAAC,GAAG;YAClB,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/C,CAAC,CAAC;QAEH,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACnD,IAAI,OAAO,CAAC,QAAQ;YAAE,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9D,IAAI,OAAO,CAAC,SAAS;YAAE,GAAG,GAAG,GAAG,CAAC,iBAAiB,CAAC,cAAc,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QACtF,IAAI,OAAO,CAAC,MAAM;YAAE,GAAG,GAAG,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACxD,IAAI,OAAO,CAAC,GAAG;YAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/C,IAAI,OAAO,CAAC,OAAO;YAAE,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE3D,OAAO,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,EAAE;QAC/B,MAAM,eAAe,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;QACnD,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC;QAChC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1D,OAAO,SAAS,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,cAAc,GAAG,CAAC,IAAU,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Context } from 'hono';
2
+ import type { AuthHonoPorts } from '../ports.js';
3
+ export interface OAuthRevokeHandlerOptions {
4
+ dpopIatSkewSeconds?: number;
5
+ ports: AuthHonoPorts;
6
+ }
7
+ export declare const createOAuthRevokeHandler: (options: OAuthRevokeHandlerOptions) => (c: Context) => Promise<Response>;
8
+ //# sourceMappingURL=revoke-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revoke-handler.d.ts","sourceRoot":"","sources":["../../src/oauth/revoke-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,eAAO,MAAM,wBAAwB,YACzB,yBAAyB,SACzB,OAAO,KAAG,QAAQ,QAAQ,CAgBnC,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { decodeJwt } from 'jose';
2
+ import { OAuthDpopProofError, verifyOAuthDpopProof } from './dpop.js';
3
+ import { oauthJsonError } from './http-utils.js';
4
+ export const createOAuthRevokeHandler = (options) => async (c) => {
5
+ const form = new URLSearchParams(await c.req.text());
6
+ const token = form.get('token');
7
+ if (!token)
8
+ return oauthJsonError(c, 400, 'invalid_request', 'token is required.');
9
+ const jti = decodeTokenJti(token);
10
+ if (!jti)
11
+ return c.json({ success: true });
12
+ const meta = await options.ports.oauthStateStore.findTokenMeta(jti);
13
+ if (meta?.dpopJkt) {
14
+ const dpop = await validateRevokeDpop(c, options, token, meta.dpopJkt);
15
+ if (dpop instanceof Response)
16
+ return dpop;
17
+ }
18
+ await options.ports.oauthStateStore.revokeToken(jti);
19
+ return c.json({ success: true });
20
+ };
21
+ const validateRevokeDpop = async (c, options, accessToken, expectedJkt) => {
22
+ const proof = c.req.header('dpop');
23
+ if (!proof)
24
+ return oauthJsonError(c, 400, 'invalid_dpop_proof', 'DPoP proof is required.');
25
+ try {
26
+ const verified = await verifyOAuthDpopProof({
27
+ accessToken,
28
+ htm: 'POST',
29
+ htu: c.req.url,
30
+ iatSkewSeconds: options.dpopIatSkewSeconds,
31
+ ports: options.ports,
32
+ proof,
33
+ });
34
+ if (verified.jkt !== expectedJkt) {
35
+ return oauthJsonError(c, 400, 'invalid_dpop_proof', 'DPoP proof key does not match the token.');
36
+ }
37
+ return null;
38
+ }
39
+ catch (error) {
40
+ if (error instanceof OAuthDpopProofError) {
41
+ return oauthJsonError(c, 400, 'invalid_dpop_proof', error.message);
42
+ }
43
+ throw error;
44
+ }
45
+ };
46
+ const decodeTokenJti = (token) => {
47
+ try {
48
+ const payload = decodeJwt(token);
49
+ return typeof payload.jti === 'string' ? payload.jti : null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ };
55
+ //# sourceMappingURL=revoke-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revoke-handler.js","sourceRoot":"","sources":["../../src/oauth/revoke-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { Hono } from 'hono';
2
+ import { type OAuthAuthorizeHandlerOptions } from './authorize-handler.js';
3
+ export interface CreateOAuthRouterOptions extends OAuthAuthorizeHandlerOptions {
4
+ authorizationCodeTtlSeconds?: number;
5
+ routePrefix?: string;
6
+ }
7
+ export declare const createOAuthRouter: (options: CreateOAuthRouterOptions) => Hono;
8
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/oauth/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAA+B,KAAK,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAUxG,MAAM,WAAW,wBAAyB,SAAQ,4BAA4B;IAC5E,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,iBAAiB,YAAa,wBAAwB,KAAG,IAcrE,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { Hono } from 'hono';
2
+ import { createOAuthAuthorizeHandler } from './authorize-handler.js';
3
+ import { createOAuthConsentDecisionHandler, createOAuthConsentDetailsHandler, } from './consent-decision-handler.js';
4
+ import { createOAuthIntrospectHandler } from './introspect-handler.js';
5
+ import { createOAuthRevokeHandler } from './revoke-handler.js';
6
+ import { createOAuthTokenHandler } from './token-handler.js';
7
+ import { createOAuthUserInfoHandler } from './userinfo-handler.js';
8
+ export const createOAuthRouter = (options) => {
9
+ const router = new Hono();
10
+ const prefix = normalizeRoutePrefix(options.routePrefix ?? '/oauth');
11
+ router.get(joinRoutePath(prefix, '/authorize'), createOAuthAuthorizeHandler(options));
12
+ router.get(joinRoutePath(prefix, '/consent'), createOAuthConsentDetailsHandler(options));
13
+ router.post(joinRoutePath(prefix, '/consent/decision'), createOAuthConsentDecisionHandler(options));
14
+ router.post(joinRoutePath(prefix, '/token'), createOAuthTokenHandler(options));
15
+ router.get(joinRoutePath(prefix, '/userinfo'), createOAuthUserInfoHandler(options));
16
+ router.post(joinRoutePath(prefix, '/userinfo'), createOAuthUserInfoHandler(options));
17
+ router.post(joinRoutePath(prefix, '/revoke'), createOAuthRevokeHandler(options));
18
+ router.post(joinRoutePath(prefix, '/introspect'), createOAuthIntrospectHandler(options));
19
+ return router;
20
+ };
21
+ const normalizeRoutePrefix = (prefix) => {
22
+ if (!prefix || prefix === '/')
23
+ return '';
24
+ return `/${prefix.replace(/^\/+|\/+$/g, '')}`;
25
+ };
26
+ const joinRoutePath = (prefix, path) => {
27
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
28
+ return `${prefix}${normalizedPath}`;
29
+ };
30
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/oauth/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,2BAA2B,EAAqC,MAAM,wBAAwB,CAAC;AACxG,OAAO,EACL,iCAAiC,EACjC,gCAAgC,GACjC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAOnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAiC,EAAQ,EAAE;IAC3E,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,oBAAoB,CAAC,OAAO,CAAC,WAAW,IAAI,QAAQ,CAAC,CAAC;IAErE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAC;IACtF,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,gCAAgC,CAAC,OAAO,CAAC,CAAC,CAAC;IACzF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAAE,iCAAiC,CAAC,OAAO,CAAC,CAAC,CAAC;IACpG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/E,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC;IACpF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC;IACrF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC;IACjF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,EAAE,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC;IAEzF,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,CAAC,MAAc,EAAU,EAAE;IACtD,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,CAAC;IACzC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC;AAChD,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,MAAc,EAAE,IAAY,EAAU,EAAE;IAC7D,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IAChE,OAAO,GAAG,MAAM,GAAG,cAAc,EAAE,CAAC;AACtC,CAAC,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { AuthHonoClockPort } from '../ports.js';
3
+ import type { JwksPort, OauthStateStorePort } from './state-store-types.js';
4
+ /**
5
+ * Narrow port set for resource-server verification (BR39d-D6). Resource servers
6
+ * must not construct users/credentials/sessions/email ports just to verify a
7
+ * bearer or DPoP-bound access token.
8
+ */
9
+ export interface ServiceAuthPorts {
10
+ clock: AuthHonoClockPort;
11
+ jwks: JwksPort;
12
+ dpopReplay?: Pick<OauthStateStorePort, 'recordDpopJti'>;
13
+ }
14
+ export interface ServiceAuthContext {
15
+ clientId: string;
16
+ scopes: string[];
17
+ jkt: string | null;
18
+ }
19
+ export interface CreateRequireServiceAuthOptions {
20
+ issuer: string;
21
+ requiredScopes?: string[];
22
+ resource: string;
23
+ ports: ServiceAuthPorts;
24
+ /** DPoP proof iat acceptance window in seconds (default 60). */
25
+ dpopIatSkewSeconds?: number;
26
+ /** Context key the verified service-client context is stored under (default 'serviceClient'). */
27
+ contextKey?: string;
28
+ }
29
+ export declare const createRequireServiceAuth: (options: CreateRequireServiceAuthOptions) => MiddlewareHandler;
30
+ //# sourceMappingURL=service-auth-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-auth-middleware.d.ts","sourceRoot":"","sources":["../../src/oauth/service-auth-middleware.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE5E;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,iBAAiB,CAAC;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,IAAI,CAAC,mBAAmB,EAAE,eAAe,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,gBAAgB,CAAC;IACxB,gEAAgE;IAChE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAcD,eAAO,MAAM,wBAAwB,YAC1B,+BAA+B,KACvC,iBA6BF,CAAC"}
@@ -0,0 +1,170 @@
1
+ import { calculateJwkThumbprint, decodeProtectedHeader, importJWK, jwtVerify, } from 'jose';
2
+ import { sha256Base64url } from './crypto-utils.js';
3
+ class ServiceAuthError extends Error {
4
+ status;
5
+ code;
6
+ scheme;
7
+ constructor(status, code, message, scheme = 'Bearer') {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.scheme = scheme;
12
+ this.name = 'ServiceAuthError';
13
+ }
14
+ }
15
+ export const createRequireServiceAuth = (options) => {
16
+ const issuer = trimTrailingSlash(options.issuer);
17
+ const requiredScopes = options.requiredScopes ?? [];
18
+ const contextKey = options.contextKey ?? 'serviceClient';
19
+ return async (c, next) => {
20
+ try {
21
+ const { scheme, token } = parseAuthorization(c.req.header('authorization'));
22
+ const payload = await verifyAccessToken(token, options.ports, issuer, options.resource);
23
+ const scopes = parseScopes(payload.scope);
24
+ assertScopes(scopes, requiredScopes);
25
+ const jkt = await enforceDpop(c, payload, token, scheme, options);
26
+ const serviceContext = {
27
+ clientId: typeof payload.client_id === 'string' ? payload.client_id : String(payload.sub ?? ''),
28
+ jkt,
29
+ scopes,
30
+ };
31
+ c.set(contextKey, serviceContext);
32
+ await next();
33
+ }
34
+ catch (error) {
35
+ if (error instanceof ServiceAuthError) {
36
+ return serviceAuthErrorResponse(c, error);
37
+ }
38
+ throw error;
39
+ }
40
+ };
41
+ };
42
+ const parseAuthorization = (header) => {
43
+ if (!header) {
44
+ throw new ServiceAuthError(401, 'invalid_token', 'Authorization header is required.');
45
+ }
46
+ const [scheme, token] = header.split(/\s+/, 2);
47
+ if (!token) {
48
+ throw new ServiceAuthError(401, 'invalid_token', 'Authorization header is malformed.');
49
+ }
50
+ if (scheme === 'Bearer')
51
+ return { scheme: 'Bearer', token };
52
+ if (scheme === 'DPoP')
53
+ return { scheme: 'DPoP', token };
54
+ throw new ServiceAuthError(401, 'invalid_token', 'Unsupported authorization scheme.');
55
+ };
56
+ const verifyAccessToken = async (token, ports, issuer, resource) => {
57
+ let kid;
58
+ try {
59
+ kid = decodeProtectedHeader(token).kid;
60
+ }
61
+ catch {
62
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token header is invalid.');
63
+ }
64
+ if (!kid) {
65
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token is missing a key id.');
66
+ }
67
+ const key = await ports.jwks.findKeyByKid(kid);
68
+ if (!key) {
69
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token signing key is unknown.');
70
+ }
71
+ const publicKey = await importJWK(key.publicJwk, key.alg);
72
+ const currentDate = ports.clock.now();
73
+ try {
74
+ const { payload } = await jwtVerify(token, publicKey, {
75
+ audience: resource,
76
+ currentDate,
77
+ issuer,
78
+ });
79
+ return payload;
80
+ }
81
+ catch {
82
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token is invalid, expired, or has the wrong audience.');
83
+ }
84
+ };
85
+ const parseScopes = (scope) => typeof scope === 'string' ? scope.split(/\s+/).filter(Boolean) : [];
86
+ const assertScopes = (scopes, requiredScopes) => {
87
+ const granted = new Set(scopes);
88
+ const missing = requiredScopes.filter((scope) => !granted.has(scope));
89
+ if (missing.length > 0) {
90
+ throw new ServiceAuthError(403, 'insufficient_scope', `Missing required scope: ${missing.join(' ')}.`);
91
+ }
92
+ };
93
+ const enforceDpop = async (c, payload, accessToken, scheme, options) => {
94
+ const boundJkt = payload.cnf?.jkt;
95
+ if (!boundJkt)
96
+ return null;
97
+ if (scheme !== 'DPoP') {
98
+ throw new ServiceAuthError(401, 'invalid_token', 'DPoP-bound token requires the DPoP authorization scheme.', 'DPoP');
99
+ }
100
+ const proof = c.req.header('dpop');
101
+ if (!proof) {
102
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof is required.', 'DPoP');
103
+ }
104
+ const verifiedJkt = await verifyServiceDpopProof({
105
+ accessToken,
106
+ htm: c.req.method,
107
+ htu: c.req.url,
108
+ iatSkewSeconds: options.dpopIatSkewSeconds,
109
+ ports: options.ports,
110
+ proof,
111
+ });
112
+ if (verifiedJkt !== boundJkt) {
113
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof key does not match the bound token.', 'DPoP');
114
+ }
115
+ return verifiedJkt;
116
+ };
117
+ const verifyServiceDpopProof = async (options) => {
118
+ const header = decodeProtectedHeader(options.proof);
119
+ const publicJwk = header.jwk;
120
+ if (!publicJwk || !header.alg || header.typ !== 'dpop+jwt') {
121
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof header is invalid.', 'DPoP');
122
+ }
123
+ const key = await importJWK(publicJwk, header.alg);
124
+ let payload;
125
+ try {
126
+ ({ payload } = await jwtVerify(options.proof, key));
127
+ }
128
+ catch {
129
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof signature is invalid.', 'DPoP');
130
+ }
131
+ const skew = options.iatSkewSeconds ?? 60;
132
+ if (payload.htm !== options.htm.toUpperCase()) {
133
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP htm claim does not match the request method.', 'DPoP');
134
+ }
135
+ if (payload.htu !== options.htu) {
136
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP htu claim does not match the request URL.', 'DPoP');
137
+ }
138
+ if (!payload.jti || typeof payload.jti !== 'string') {
139
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP jti claim is required.', 'DPoP');
140
+ }
141
+ if (typeof payload.iat !== 'number') {
142
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP iat claim is required.', 'DPoP');
143
+ }
144
+ const nowSeconds = Math.floor(options.ports.clock.now().getTime() / 1000);
145
+ if (Math.abs(payload.iat - nowSeconds) > skew) {
146
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP iat claim is outside the allowed skew.', 'DPoP');
147
+ }
148
+ // RFC 9449 §4.3: bind the proof to the access token (BR39d-D7).
149
+ if (payload.ath !== (await sha256Base64url(options.accessToken))) {
150
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP ath claim does not match the access token.', 'DPoP');
151
+ }
152
+ if (options.ports.dpopReplay) {
153
+ const expiresAt = options.ports.clock.addSeconds(options.ports.clock.now(), skew);
154
+ const recorded = await options.ports.dpopReplay.recordDpopJti(payload.jti, expiresAt);
155
+ if (!recorded) {
156
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof jti was already used.', 'DPoP');
157
+ }
158
+ }
159
+ return calculateJwkThumbprint(publicJwk);
160
+ };
161
+ const serviceAuthErrorResponse = (c, error) => {
162
+ c.header('WWW-Authenticate', buildWwwAuthenticate(error));
163
+ return c.json({ error: { code: error.code, message: error.message } }, error.status);
164
+ };
165
+ const buildWwwAuthenticate = (error) => {
166
+ const params = [`error="${error.code}"`, `error_description="${error.message}"`];
167
+ return `${error.scheme} ${params.join(', ')}`;
168
+ };
169
+ const trimTrailingSlash = (value) => value.replace(/\/+$/u, '');
170
+ //# sourceMappingURL=service-auth-middleware.js.map