@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,250 @@
1
+ import {
2
+ calculateJwkThumbprint,
3
+ decodeProtectedHeader,
4
+ importJWK,
5
+ jwtVerify,
6
+ type JWK,
7
+ type JWTPayload,
8
+ } from 'jose';
9
+ import type { Context, MiddlewareHandler } from 'hono';
10
+
11
+ import type { AuthHonoClockPort } from '../ports.js';
12
+ import { sha256Base64url } from './crypto-utils.js';
13
+ import type { JwksPort, OauthStateStorePort } from './state-store-types.js';
14
+
15
+ /**
16
+ * Narrow port set for resource-server verification (BR39d-D6). Resource servers
17
+ * must not construct users/credentials/sessions/email ports just to verify a
18
+ * bearer or DPoP-bound access token.
19
+ */
20
+ export interface ServiceAuthPorts {
21
+ clock: AuthHonoClockPort;
22
+ jwks: JwksPort;
23
+ dpopReplay?: Pick<OauthStateStorePort, 'recordDpopJti'>;
24
+ }
25
+
26
+ export interface ServiceAuthContext {
27
+ clientId: string;
28
+ scopes: string[];
29
+ jkt: string | null;
30
+ }
31
+
32
+ export interface CreateRequireServiceAuthOptions {
33
+ issuer: string;
34
+ requiredScopes?: string[];
35
+ resource: string;
36
+ ports: ServiceAuthPorts;
37
+ /** DPoP proof iat acceptance window in seconds (default 60). */
38
+ dpopIatSkewSeconds?: number;
39
+ /** Context key the verified service-client context is stored under (default 'serviceClient'). */
40
+ contextKey?: string;
41
+ }
42
+
43
+ class ServiceAuthError extends Error {
44
+ constructor(
45
+ readonly status: 401 | 403,
46
+ readonly code: string,
47
+ message: string,
48
+ readonly scheme: 'Bearer' | 'DPoP' = 'Bearer'
49
+ ) {
50
+ super(message);
51
+ this.name = 'ServiceAuthError';
52
+ }
53
+ }
54
+
55
+ export const createRequireServiceAuth = (
56
+ options: CreateRequireServiceAuthOptions
57
+ ): MiddlewareHandler => {
58
+ const issuer = trimTrailingSlash(options.issuer);
59
+ const requiredScopes = options.requiredScopes ?? [];
60
+ const contextKey = options.contextKey ?? 'serviceClient';
61
+
62
+ return async (c, next) => {
63
+ try {
64
+ const { scheme, token } = parseAuthorization(c.req.header('authorization'));
65
+ const payload = await verifyAccessToken(token, options.ports, issuer, options.resource);
66
+ const scopes = parseScopes(payload.scope);
67
+ assertScopes(scopes, requiredScopes);
68
+
69
+ const jkt = await enforceDpop(c, payload, token, scheme, options);
70
+
71
+ const serviceContext: ServiceAuthContext = {
72
+ clientId: typeof payload.client_id === 'string' ? payload.client_id : String(payload.sub ?? ''),
73
+ jkt,
74
+ scopes,
75
+ };
76
+ c.set(contextKey, serviceContext);
77
+
78
+ await next();
79
+ } catch (error) {
80
+ if (error instanceof ServiceAuthError) {
81
+ return serviceAuthErrorResponse(c, error);
82
+ }
83
+ throw error;
84
+ }
85
+ };
86
+ };
87
+
88
+ const parseAuthorization = (header: string | undefined): { scheme: 'Bearer' | 'DPoP'; token: string } => {
89
+ if (!header) {
90
+ throw new ServiceAuthError(401, 'invalid_token', 'Authorization header is required.');
91
+ }
92
+ const [scheme, token] = header.split(/\s+/, 2);
93
+ if (!token) {
94
+ throw new ServiceAuthError(401, 'invalid_token', 'Authorization header is malformed.');
95
+ }
96
+ if (scheme === 'Bearer') return { scheme: 'Bearer', token };
97
+ if (scheme === 'DPoP') return { scheme: 'DPoP', token };
98
+ throw new ServiceAuthError(401, 'invalid_token', 'Unsupported authorization scheme.');
99
+ };
100
+
101
+ const verifyAccessToken = async (
102
+ token: string,
103
+ ports: ServiceAuthPorts,
104
+ issuer: string,
105
+ resource: string
106
+ ): Promise<JWTPayload & { scope?: unknown; client_id?: unknown; cnf?: { jkt?: string } }> => {
107
+ let kid: string | undefined;
108
+ try {
109
+ kid = decodeProtectedHeader(token).kid;
110
+ } catch {
111
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token header is invalid.');
112
+ }
113
+ if (!kid) {
114
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token is missing a key id.');
115
+ }
116
+
117
+ const key = await ports.jwks.findKeyByKid(kid);
118
+ if (!key) {
119
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token signing key is unknown.');
120
+ }
121
+
122
+ const publicKey = await importJWK(key.publicJwk, key.alg);
123
+ const currentDate = ports.clock.now();
124
+ try {
125
+ const { payload } = await jwtVerify(token, publicKey, {
126
+ audience: resource,
127
+ currentDate,
128
+ issuer,
129
+ });
130
+ return payload;
131
+ } catch {
132
+ throw new ServiceAuthError(401, 'invalid_token', 'Access token is invalid, expired, or has the wrong audience.');
133
+ }
134
+ };
135
+
136
+ const parseScopes = (scope: unknown): string[] =>
137
+ typeof scope === 'string' ? scope.split(/\s+/).filter(Boolean) : [];
138
+
139
+ const assertScopes = (scopes: string[], requiredScopes: string[]): void => {
140
+ const granted = new Set(scopes);
141
+ const missing = requiredScopes.filter((scope) => !granted.has(scope));
142
+ if (missing.length > 0) {
143
+ throw new ServiceAuthError(403, 'insufficient_scope', `Missing required scope: ${missing.join(' ')}.`);
144
+ }
145
+ };
146
+
147
+ const enforceDpop = async (
148
+ c: Context,
149
+ payload: { cnf?: { jkt?: string } },
150
+ accessToken: string,
151
+ scheme: 'Bearer' | 'DPoP',
152
+ options: CreateRequireServiceAuthOptions
153
+ ): Promise<string | null> => {
154
+ const boundJkt = payload.cnf?.jkt;
155
+ if (!boundJkt) return null;
156
+
157
+ if (scheme !== 'DPoP') {
158
+ throw new ServiceAuthError(401, 'invalid_token', 'DPoP-bound token requires the DPoP authorization scheme.', 'DPoP');
159
+ }
160
+
161
+ const proof = c.req.header('dpop');
162
+ if (!proof) {
163
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof is required.', 'DPoP');
164
+ }
165
+
166
+ const verifiedJkt = await verifyServiceDpopProof({
167
+ accessToken,
168
+ htm: c.req.method,
169
+ htu: c.req.url,
170
+ iatSkewSeconds: options.dpopIatSkewSeconds,
171
+ ports: options.ports,
172
+ proof,
173
+ });
174
+
175
+ if (verifiedJkt !== boundJkt) {
176
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof key does not match the bound token.', 'DPoP');
177
+ }
178
+
179
+ return verifiedJkt;
180
+ };
181
+
182
+ interface VerifyServiceDpopProofOptions {
183
+ accessToken: string;
184
+ htm: string;
185
+ htu: string;
186
+ iatSkewSeconds?: number;
187
+ ports: ServiceAuthPorts;
188
+ proof: string;
189
+ }
190
+
191
+ const verifyServiceDpopProof = async (options: VerifyServiceDpopProofOptions): Promise<string> => {
192
+ const header = decodeProtectedHeader(options.proof);
193
+ const publicJwk = header.jwk as JWK | undefined;
194
+ if (!publicJwk || !header.alg || header.typ !== 'dpop+jwt') {
195
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof header is invalid.', 'DPoP');
196
+ }
197
+
198
+ const key = await importJWK(publicJwk, header.alg);
199
+ let payload: JWTPayload;
200
+ try {
201
+ ({ payload } = await jwtVerify(options.proof, key));
202
+ } catch {
203
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof signature is invalid.', 'DPoP');
204
+ }
205
+
206
+ const skew = options.iatSkewSeconds ?? 60;
207
+ if (payload.htm !== options.htm.toUpperCase()) {
208
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP htm claim does not match the request method.', 'DPoP');
209
+ }
210
+ if (payload.htu !== options.htu) {
211
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP htu claim does not match the request URL.', 'DPoP');
212
+ }
213
+ if (!payload.jti || typeof payload.jti !== 'string') {
214
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP jti claim is required.', 'DPoP');
215
+ }
216
+ if (typeof payload.iat !== 'number') {
217
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP iat claim is required.', 'DPoP');
218
+ }
219
+ const nowSeconds = Math.floor(options.ports.clock.now().getTime() / 1000);
220
+ if (Math.abs(payload.iat - nowSeconds) > skew) {
221
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP iat claim is outside the allowed skew.', 'DPoP');
222
+ }
223
+
224
+ // RFC 9449 §4.3: bind the proof to the access token (BR39d-D7).
225
+ if (payload.ath !== (await sha256Base64url(options.accessToken))) {
226
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP ath claim does not match the access token.', 'DPoP');
227
+ }
228
+
229
+ if (options.ports.dpopReplay) {
230
+ const expiresAt = options.ports.clock.addSeconds(options.ports.clock.now(), skew);
231
+ const recorded = await options.ports.dpopReplay.recordDpopJti(payload.jti, expiresAt);
232
+ if (!recorded) {
233
+ throw new ServiceAuthError(401, 'invalid_dpop_proof', 'DPoP proof jti was already used.', 'DPoP');
234
+ }
235
+ }
236
+
237
+ return calculateJwkThumbprint(publicJwk);
238
+ };
239
+
240
+ const serviceAuthErrorResponse = (c: Context, error: ServiceAuthError): Response => {
241
+ c.header('WWW-Authenticate', buildWwwAuthenticate(error));
242
+ return c.json({ error: { code: error.code, message: error.message } }, error.status);
243
+ };
244
+
245
+ const buildWwwAuthenticate = (error: ServiceAuthError): string => {
246
+ const params = [`error="${error.code}"`, `error_description="${error.message}"`];
247
+ return `${error.scheme} ${params.join(', ')}`;
248
+ };
249
+
250
+ const trimTrailingSlash = (value: string): string => value.replace(/\/+$/u, '');
@@ -0,0 +1,48 @@
1
+ import type {
2
+ AuthHonoPorts,
3
+ AuthHonoSessionClaims,
4
+ AuthHonoSessionRecord,
5
+ AuthHonoUserRecord,
6
+ } from '../ports.js';
7
+
8
+ export interface OAuthResolvedSession {
9
+ claims: AuthHonoSessionClaims;
10
+ sessionRecord: AuthHonoSessionRecord;
11
+ user: AuthHonoUserRecord;
12
+ }
13
+
14
+ export const resolveOAuthSession = async (
15
+ request: Request,
16
+ ports: AuthHonoPorts
17
+ ): Promise<OAuthResolvedSession | null> => {
18
+ const token = ports.cookies.readSessionToken(request);
19
+ if (!token) return null;
20
+
21
+ const claims = await ports.tokens.verifySessionToken(token);
22
+ if (!claims) return null;
23
+
24
+ const tokenHash = await ports.tokens.hashSecret(token);
25
+ const sessionRecord = await ports.sessions.findByTokenHash(tokenHash);
26
+ const now = ports.clock.now();
27
+ if (
28
+ !sessionRecord ||
29
+ sessionRecord.id !== claims.sessionId ||
30
+ sessionRecord.userId !== claims.userId ||
31
+ sessionRecord.revokedAt ||
32
+ sessionRecord.expiresAt <= now
33
+ ) {
34
+ return null;
35
+ }
36
+
37
+ const user = await ports.users.findById(claims.userId);
38
+ if (!user) return null;
39
+
40
+ const decision = await ports.accountPolicy.canAuthenticate(user, now);
41
+ if (!decision.allowed) return null;
42
+
43
+ await ports.sessions.touch(sessionRecord.id, now);
44
+ return { claims, sessionRecord, user };
45
+ };
46
+
47
+ export const resolveOAuthAcr = (session: AuthHonoSessionRecord): string =>
48
+ session.mfaVerified ? 'urn:sentropic:loa:passkey-fresh' : 'urn:sentropic:loa:bearer';
@@ -0,0 +1,98 @@
1
+ export interface OAuthContinuationState {
2
+ acr?: string;
3
+ authTime?: string;
4
+ clientId: string;
5
+ codeChallenge: string;
6
+ codeChallengeMethod: 'S256';
7
+ createdAt: string;
8
+ dpopJkt: string | null;
9
+ expiresAt: string;
10
+ nonce: string | null;
11
+ redirectUri: string;
12
+ scope: string;
13
+ state: string | null;
14
+ tenantId: string | null;
15
+ userId?: string;
16
+ }
17
+
18
+ export interface OAuthContinuationCodec {
19
+ seal(payload: OAuthContinuationState): Promise<string> | string;
20
+ unseal(token: string): Promise<OAuthContinuationState | null> | OAuthContinuationState | null;
21
+ }
22
+
23
+ export interface CreateOAuthHmacStateCodecOptions {
24
+ secret: string;
25
+ }
26
+
27
+ export const createOAuthHmacStateCodec = ({
28
+ secret,
29
+ }: CreateOAuthHmacStateCodecOptions): OAuthContinuationCodec => {
30
+ if (!secret) {
31
+ throw new Error('OAuth state codec secret is required.');
32
+ }
33
+
34
+ return {
35
+ async seal(payload) {
36
+ const body = base64urlEncode(textEncoder.encode(JSON.stringify(payload)));
37
+ return `${body}.${await sign(body, secret)}`;
38
+ },
39
+
40
+ async unseal(token) {
41
+ const [body, signature, extra] = token.split('.');
42
+ if (!body || !signature || extra !== undefined) return null;
43
+
44
+ const expected = await sign(body, secret);
45
+ const actualBytes = base64urlDecode(signature);
46
+ const expectedBytes = base64urlDecode(expected);
47
+ if (!timingSafeEqual(actualBytes, expectedBytes)) return null;
48
+
49
+ try {
50
+ return JSON.parse(textDecoder.decode(base64urlDecode(body))) as OAuthContinuationState;
51
+ } catch {
52
+ return null;
53
+ }
54
+ },
55
+ };
56
+ };
57
+
58
+ const textEncoder = new TextEncoder();
59
+ const textDecoder = new TextDecoder();
60
+
61
+ const sign = async (body: string, secret: string): Promise<string> => {
62
+ const key = await crypto.subtle.importKey(
63
+ 'raw',
64
+ textEncoder.encode(secret),
65
+ { hash: 'SHA-256', name: 'HMAC' },
66
+ false,
67
+ ['sign']
68
+ );
69
+ const signature = await crypto.subtle.sign('HMAC', key, textEncoder.encode(body));
70
+ return base64urlEncode(new Uint8Array(signature));
71
+ };
72
+
73
+ const timingSafeEqual = (actual: Uint8Array, expected: Uint8Array): boolean => {
74
+ if (actual.byteLength !== expected.byteLength) return false;
75
+ let diff = 0;
76
+ for (let index = 0; index < actual.byteLength; index += 1) {
77
+ diff |= actual[index] ^ expected[index];
78
+ }
79
+ return diff === 0;
80
+ };
81
+
82
+ const base64urlEncode = (bytes: Uint8Array): string => {
83
+ let binary = '';
84
+ for (const byte of bytes) {
85
+ binary += String.fromCharCode(byte);
86
+ }
87
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
88
+ };
89
+
90
+ const base64urlDecode = (value: string): Uint8Array => {
91
+ const base64 = value.replaceAll('-', '+').replaceAll('_', '/').padEnd(Math.ceil(value.length / 4) * 4, '=');
92
+ const binary = atob(base64);
93
+ const bytes = new Uint8Array(binary.length);
94
+ for (let index = 0; index < binary.length; index += 1) {
95
+ bytes[index] = binary.charCodeAt(index);
96
+ }
97
+ return bytes;
98
+ };
@@ -0,0 +1,109 @@
1
+ import type { JWK, KeyLike } from 'jose';
2
+
3
+ export type OauthTokenType = 'access_token' | 'id_token';
4
+
5
+ export interface OauthClientRecord {
6
+ id: string;
7
+ clientId: string;
8
+ clientSecretHash: string | null;
9
+ name: string;
10
+ redirectUris: string[];
11
+ allowedScopes: string[];
12
+ grantTypes: string[];
13
+ responseTypes: string[];
14
+ tokenEndpointAuthMethod: 'client_secret_basic' | 'none' | (string & {});
15
+ dpopBoundAccessTokens: boolean;
16
+ requirePkce: boolean;
17
+ tenantId: string | null;
18
+ ownerUserId: string | null;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ }
22
+
23
+ export interface AuthCodePayload {
24
+ clientId: string;
25
+ userId: string;
26
+ tenantId: string | null;
27
+ redirectUri: string;
28
+ scope: string;
29
+ codeChallenge: string;
30
+ codeChallengeMethod: 'S256';
31
+ dpopJkt: string | null;
32
+ nonce: string | null;
33
+ acr: string;
34
+ authTime: Date;
35
+ expiresAt: Date;
36
+ createdAt: Date;
37
+ }
38
+
39
+ export interface TokenMeta {
40
+ jti: string;
41
+ tokenType: OauthTokenType;
42
+ clientId: string;
43
+ userId: string;
44
+ tenantId: string | null;
45
+ scope: string;
46
+ audience: string;
47
+ dpopJkt: string | null;
48
+ expiresAt: Date;
49
+ createdAt: Date;
50
+ }
51
+
52
+ export interface DpopProofRecord {
53
+ jti: string;
54
+ expiresAt: Date;
55
+ createdAt: Date;
56
+ }
57
+
58
+ export interface ServiceClientRecord {
59
+ id: string;
60
+ clientId: string;
61
+ clientSecretHash: string;
62
+ displayName: string | null;
63
+ allowedScopes: string[];
64
+ resourceIndicators: string[];
65
+ dpopBoundAccessTokens: boolean;
66
+ tenantId: string | null;
67
+ secretRotatedAt: Date | null;
68
+ createdAt: Date;
69
+ revokedAt: Date | null;
70
+ }
71
+
72
+ export interface OauthStateStorePort {
73
+ findClient(clientId: string): Promise<OauthClientRecord | null>;
74
+ findServiceClient?(clientId: string): Promise<ServiceClientRecord | null>;
75
+ saveAuthCode(code: string, payload: AuthCodePayload, ttlSec: number): Promise<void>;
76
+ consumeAuthCode(code: string): Promise<AuthCodePayload | null>;
77
+ saveTokenMeta(jti: string, meta: TokenMeta, ttlSec: number): Promise<void>;
78
+ findTokenMeta(jti: string): Promise<TokenMeta | null>;
79
+ revokeToken(jti: string): Promise<boolean>;
80
+ isTokenRevoked(jti: string): Promise<boolean>;
81
+ recordDpopJti(jti: string, expiresAt: Date): Promise<boolean>;
82
+ purgeExpired(): Promise<number>;
83
+ }
84
+
85
+ export type JwksPublicJwk = JWK & {
86
+ alg?: 'EdDSA' | (string & {});
87
+ crv: 'Ed25519' | (string & {});
88
+ kid?: string;
89
+ kty: 'OKP' | (string & {});
90
+ use?: 'sig' | (string & {});
91
+ x: string;
92
+ };
93
+
94
+ export interface JwksKeyRecord {
95
+ kid: string;
96
+ alg: 'EdDSA' | (string & {});
97
+ crv: 'Ed25519' | (string & {});
98
+ publicJwk: JwksPublicJwk;
99
+ privateKey?: KeyLike | Uint8Array;
100
+ active: boolean;
101
+ createdAt: Date;
102
+ rotatedAt: Date | null;
103
+ }
104
+
105
+ export interface JwksPort {
106
+ getActiveKey(): Promise<JwksKeyRecord | null>;
107
+ findKeyByKid(kid: string): Promise<JwksKeyRecord | null>;
108
+ listPublicKeys(): Promise<JwksKeyRecord[]>;
109
+ }