@sentropic/auth-hono 0.4.0 → 0.7.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.
- package/README.md +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth/authorize-handler.d.ts +1 -0
- package/dist/oauth/authorize-handler.d.ts.map +1 -1
- package/dist/oauth/authorize-handler.js +85 -7
- package/dist/oauth/authorize-handler.js.map +1 -1
- package/dist/oauth/consent-decision-handler.d.ts.map +1 -1
- package/dist/oauth/consent-decision-handler.js +7 -18
- package/dist/oauth/consent-decision-handler.js.map +1 -1
- package/dist/oauth/dpop.d.ts +7 -4
- package/dist/oauth/dpop.d.ts.map +1 -1
- package/dist/oauth/dpop.js +23 -44
- package/dist/oauth/dpop.js.map +1 -1
- package/dist/oauth/issue-authorized-code.d.ts +15 -0
- package/dist/oauth/issue-authorized-code.d.ts.map +1 -0
- package/dist/oauth/issue-authorized-code.js +29 -0
- package/dist/oauth/issue-authorized-code.js.map +1 -0
- package/dist/oauth/jwks-service.d.ts.map +1 -1
- package/dist/oauth/jwks-service.js +6 -8
- package/dist/oauth/jwks-service.js.map +1 -1
- package/dist/oauth/service-auth-middleware.d.ts.map +1 -1
- package/dist/oauth/service-auth-middleware.js +46 -64
- package/dist/oauth/service-auth-middleware.js.map +1 -1
- package/dist/oauth/state-codec.d.ts +2 -0
- package/dist/oauth/state-codec.d.ts.map +1 -1
- package/dist/oauth/state-codec.js.map +1 -1
- package/dist/oauth/state-store-types.d.ts +12 -0
- package/dist/oauth/state-store-types.d.ts.map +1 -1
- package/dist/oauth/token-handler.d.ts.map +1 -1
- package/dist/oauth/token-handler.js +38 -1
- package/dist/oauth/token-handler.js.map +1 -1
- package/dist/oauth/wellknown-handler.js +1 -1
- package/dist/oauth/wellknown-handler.js.map +1 -1
- package/dist/ports.d.ts +32 -0
- package/dist/ports.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/index.ts +11 -0
- package/src/oauth/authorize-handler.ts +101 -8
- package/src/oauth/consent-decision-handler.ts +11 -25
- package/src/oauth/dpop.ts +30 -67
- package/src/oauth/issue-authorized-code.ts +50 -0
- package/src/oauth/jwks-service.ts +5 -9
- package/src/oauth/service-auth-middleware.ts +59 -80
- package/src/oauth/state-codec.ts +2 -0
- package/src/oauth/state-store-types.ts +12 -0
- package/src/oauth/token-handler.ts +44 -1
- package/src/oauth/wellknown-handler.ts +1 -1
- package/src/ports.ts +35 -0
|
@@ -59,6 +59,9 @@ export const createOAuthTokenHandler =
|
|
|
59
59
|
return oauthJsonError(c, 400, 'invalid_grant', 'PKCE verification failed.');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const resourceError = validateTokenResource(c, form, codePayload);
|
|
63
|
+
if (resourceError) return resourceError;
|
|
64
|
+
|
|
62
65
|
const dpopJkt = await resolveDpopJkt(c, options, auth.client, codePayload);
|
|
63
66
|
if (dpopJkt instanceof Response) return dpopJkt;
|
|
64
67
|
|
|
@@ -117,6 +120,30 @@ const parseClientCredentials = (
|
|
|
117
120
|
};
|
|
118
121
|
};
|
|
119
122
|
|
|
123
|
+
/**
|
|
124
|
+
* RFC 8707 resource validation on the token leg of the `authorization_code` flow (BR-39l Lot 2).
|
|
125
|
+
* - C1 single-aud: more than one `resource` value ⇒ `invalid_target`.
|
|
126
|
+
* - C3 sealing: if the client re-sends `resource` on the token request (the MCP draft requires it
|
|
127
|
+
* on both legs), it MUST equal the value sealed at authorize time, else `invalid_grant`
|
|
128
|
+
* (audience downgrade/upgrade rejection). The `aud` is derived ONLY from the sealed value.
|
|
129
|
+
* No `resource` on the token leg is accepted (the sealed value still governs `aud`).
|
|
130
|
+
*/
|
|
131
|
+
const validateTokenResource = (
|
|
132
|
+
c: Context,
|
|
133
|
+
form: URLSearchParams,
|
|
134
|
+
codePayload: AuthCodePayload
|
|
135
|
+
): Response | null => {
|
|
136
|
+
const requested = form.getAll('resource').filter((value) => value.length > 0);
|
|
137
|
+
if (requested.length === 0) return null;
|
|
138
|
+
if (requested.length > 1) {
|
|
139
|
+
return oauthJsonError(c, 400, 'invalid_target', 'Only a single resource indicator is supported.');
|
|
140
|
+
}
|
|
141
|
+
if (requested[0] !== (codePayload.resource ?? null)) {
|
|
142
|
+
return oauthJsonError(c, 400, 'invalid_grant', 'resource does not match the authorization request.');
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
|
|
120
147
|
const resolveDpopJkt = async (
|
|
121
148
|
c: Context,
|
|
122
149
|
options: OAuthTokenHandlerOptions,
|
|
@@ -316,15 +343,30 @@ const issueTokens = async (
|
|
|
316
343
|
const idExpiresAt = options.ports.clock.addSeconds(now, idTokenTtlSeconds);
|
|
317
344
|
const scopes = codePayload.scope.split(/\s+/).filter(Boolean);
|
|
318
345
|
const cnf = dpopJkt ? { jkt: dpopJkt } : undefined;
|
|
346
|
+
|
|
347
|
+
// BR-39e: bind the tenant claim to a STILL-`approved` membership at token time (lifecycle
|
|
348
|
+
// gate). If the membership was suspended/revoked between authorize and token exchange, drop
|
|
349
|
+
// the claim so no tenant-scoped token is issued for a non-member.
|
|
350
|
+
let boundTenantId: string | null = codePayload.tenantId;
|
|
351
|
+
if (boundTenantId && options.ports.tenant) {
|
|
352
|
+
const stillApproved = await options.ports.tenant.isApprovedMember(codePayload.userId, boundTenantId);
|
|
353
|
+
if (!stillApproved) boundTenantId = null;
|
|
354
|
+
}
|
|
355
|
+
|
|
319
356
|
const jwks = createJwksService({ clock: options.ports.clock, jwksPort: options.ports.jwks });
|
|
320
357
|
const accessJti = options.ports.random.uuid();
|
|
321
|
-
|
|
358
|
+
// BR-39l Lot 2 (C3/C4): variable RFC 8707 audience. The access-token `aud` is the resource
|
|
359
|
+
// sealed at authorize time (single string), or the userinfo URL by default — byte-identical to
|
|
360
|
+
// auth-hono 0.5.0 when no `resource` was requested. The id_token `aud` (client_id) is untouched.
|
|
361
|
+
const accessAudience =
|
|
362
|
+
codePayload.resource ?? `${trimTrailingSlash(options.issuer)}/api/v1/auth/oauth/userinfo`;
|
|
322
363
|
const accessToken = await jwks.signJwt(
|
|
323
364
|
{
|
|
324
365
|
acr: codePayload.acr,
|
|
325
366
|
auth_time: toEpochSeconds(codePayload.authTime),
|
|
326
367
|
client_id: client.clientId,
|
|
327
368
|
...(cnf ? { cnf } : {}),
|
|
369
|
+
...(boundTenantId ? { tid: boundTenantId } : {}),
|
|
328
370
|
scope: codePayload.scope,
|
|
329
371
|
},
|
|
330
372
|
{
|
|
@@ -368,6 +410,7 @@ const issueTokens = async (
|
|
|
368
410
|
...(scopes.includes('email') ? { email: user.email, email_verified: user.emailVerified } : {}),
|
|
369
411
|
...(scopes.includes('profile') ? { name: user.displayName } : {}),
|
|
370
412
|
...(codePayload.nonce ? { nonce: codePayload.nonce } : {}),
|
|
413
|
+
...(boundTenantId ? { tid: boundTenantId } : {}),
|
|
371
414
|
},
|
|
372
415
|
{
|
|
373
416
|
audience: client.clientId,
|
|
@@ -17,7 +17,7 @@ export const createWellKnownRouter = (options: CreateWellKnownRouterOptions): Ho
|
|
|
17
17
|
router.get('/openid-configuration', (c) =>
|
|
18
18
|
c.json({
|
|
19
19
|
authorization_endpoint: `${issuer}${oauthPrefix}/authorize`,
|
|
20
|
-
claims_supported: ['sub', 'aud', 'iss', 'exp', 'iat', 'nonce', 'auth_time', 'acr', 'email', 'email_verified', 'name'],
|
|
20
|
+
claims_supported: ['sub', 'aud', 'iss', 'exp', 'iat', 'nonce', 'auth_time', 'acr', 'email', 'email_verified', 'name', 'tid'],
|
|
21
21
|
code_challenge_methods_supported: ['S256'],
|
|
22
22
|
dpop_signing_alg_values_supported: ['EdDSA'],
|
|
23
23
|
grant_types_supported: ['authorization_code', 'client_credentials'],
|
package/src/ports.ts
CHANGED
|
@@ -287,6 +287,37 @@ export interface AuthHonoAccountPolicyPort {
|
|
|
287
287
|
afterUserCreated?(user: AuthHonoUserRecord): Promise<void> | void;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* BR-39e: tenancy spine. OPTIONAL — when absent, auth-hono keeps the legacy behavior of
|
|
292
|
+
* sourcing the auth-code `tenantId` from the client. When present, the tenant claim (`tid`)
|
|
293
|
+
* is derived ONLY from a VALIDATED `approved` membership (never a raw request parameter),
|
|
294
|
+
* and re-checked at token time (lifecycle gate).
|
|
295
|
+
*/
|
|
296
|
+
export interface AuthHonoTenantPort {
|
|
297
|
+
/** Tenant ids the user is an `approved` member of, for selection + claim derivation. */
|
|
298
|
+
listApprovedTenantIds(userId: string): Promise<string[]>;
|
|
299
|
+
/** True iff (userId, tenantId) is currently an `approved` membership (binding re-check). */
|
|
300
|
+
isApprovedMember(userId: string, tenantId: string): Promise<boolean>;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Consent persistence. OPTIONAL — when absent, auth-hono keeps the legacy behavior of
|
|
305
|
+
* always re-showing the consent screen on every `/authorize`. When present, an approved
|
|
306
|
+
* grant per exact `(userId, clientId)` lets the authorize handler skip consent and issue
|
|
307
|
+
* the auth code directly, provided the stored grant's scopes are a SUPERSET of the
|
|
308
|
+
* requested scopes (scope-escalation re-consents) and `prompt !== 'consent'`.
|
|
309
|
+
*/
|
|
310
|
+
export interface AuthHonoConsentGrant {
|
|
311
|
+
scopes: string[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface AuthHonoConsentStorePort {
|
|
315
|
+
/** The user's currently granted scopes for this client, or `null` if no grant exists. */
|
|
316
|
+
getGrant(userId: string, clientId: string): Promise<AuthHonoConsentGrant | null>;
|
|
317
|
+
/** Upsert the grant for `(userId, clientId)`, unioning `scopes` with any prior grant. */
|
|
318
|
+
saveGrant(userId: string, clientId: string, scopes: string[]): Promise<void>;
|
|
319
|
+
}
|
|
320
|
+
|
|
290
321
|
export interface AuthHonoPorts {
|
|
291
322
|
users: AuthHonoUserPort;
|
|
292
323
|
credentials: AuthHonoCredentialPort;
|
|
@@ -303,4 +334,8 @@ export interface AuthHonoPorts {
|
|
|
303
334
|
accountPolicy: AuthHonoAccountPolicyPort;
|
|
304
335
|
oauthStateStore: OauthStateStorePort;
|
|
305
336
|
jwks: JwksPort;
|
|
337
|
+
/** BR-39e tenancy spine (optional; legacy behavior when absent). */
|
|
338
|
+
tenant?: AuthHonoTenantPort;
|
|
339
|
+
/** Consent persistence (optional; always-consent legacy behavior when absent). */
|
|
340
|
+
consentStore?: AuthHonoConsentStorePort;
|
|
306
341
|
}
|