@rdlabo/workers-hono-kit 0.10.1 → 0.10.2
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 +16 -3
- package/dist/firebase/jose-firebase-verifier.d.ts +12 -0
- package/dist/firebase/jose-firebase-verifier.js +20 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/middleware/auth.d.ts +37 -3
- package/dist/middleware/auth.js +36 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -481,8 +481,10 @@ Repos with a custom DB error classifier (e.g. odss-mobile) pass `classify` to
|
|
|
481
481
|
### Auth middleware
|
|
482
482
|
|
|
483
483
|
Encodes the shared skeleton (read token header → verify → `getAppInfo` → resolve user id →
|
|
484
|
-
set context, with
|
|
485
|
-
|
|
484
|
+
set context, with configurable reporting and response hooks). Inject your own verify/resolver,
|
|
485
|
+
context-variable names, and failure mode. By default a missing header remains backward compatible
|
|
486
|
+
and calls `verify('')`; set `rejectMissingToken: true` to reject missing/blank input first with
|
|
487
|
+
`AuthTokenMissingError`.
|
|
486
488
|
|
|
487
489
|
`createAuthMiddleware<Env, Verified, Id>` is generic over your Hono `Env`, so `c.set(...)` in
|
|
488
490
|
`setContext` is type-checked against your `Variables`.
|
|
@@ -492,6 +494,7 @@ import { createAuthMiddleware, createIdentityAuthFailureBody } from '@rdlabo/wor
|
|
|
492
494
|
|
|
493
495
|
// AuthGuard: verify + resolve (and provision) the DB user id.
|
|
494
496
|
const userAuth = createAuthMiddleware<AppEnv, UserRecord, number>({
|
|
497
|
+
rejectMissingToken: true,
|
|
495
498
|
verify: (token) => container.firebase.verifyIdToken(token),
|
|
496
499
|
resolveUserId: (record, _c, appInfo) =>
|
|
497
500
|
container.auth.getUserIdFromFirebase(record, appInfo).catch(() => container.auth.createUser(record)),
|
|
@@ -500,18 +503,28 @@ const userAuth = createAuthMiddleware<AppEnv, UserRecord, number>({
|
|
|
500
503
|
c.set('userId', userId);
|
|
501
504
|
c.set('appInfo', appInfo);
|
|
502
505
|
},
|
|
503
|
-
|
|
506
|
+
reportFailure: (error, context, { stage, tokenPresent }) => {
|
|
507
|
+
// Suppress expected credential rejection; report dependency/internal failures without tokens.
|
|
508
|
+
},
|
|
509
|
+
onFailure: (_error, context, { stage }) =>
|
|
504
510
|
context.json(createIdentityAuthFailureBody(), 401),
|
|
505
511
|
});
|
|
506
512
|
|
|
507
513
|
// TokenGuard (login): verify only — omit resolveUserId. Override the failure if needed.
|
|
508
514
|
const tokenAuth = createAuthMiddleware<AppEnv, UserRecord>({
|
|
515
|
+
rejectMissingToken: true,
|
|
509
516
|
verify: (token) => container.firebase.verifyIdToken(token),
|
|
510
517
|
setContext: (c, { verified }) => c.set('userRecord', verified),
|
|
511
518
|
onFailure: (_e, c) => c.json(createIdentityAuthFailureBody(), 401),
|
|
512
519
|
});
|
|
513
520
|
```
|
|
514
521
|
|
|
522
|
+
`reportFailure(error, context, details)` receives only the stage (`token`, `verify`, `appInfo`,
|
|
523
|
+
`resolveUserId`, or `setContext`) and a `tokenPresent` boolean; raw token data is never included in
|
|
524
|
+
`details`. If the hook is omitted, the historical `console.error(error)` behavior remains. A reporting
|
|
525
|
+
hook failure is logged but cannot change the authentication response. `onFailure` receives the same
|
|
526
|
+
details as its third argument and may be asynchronous.
|
|
527
|
+
|
|
515
528
|
Authentication failures use three explicit scopes. Only `identity` permits a client to purge its
|
|
516
529
|
global authenticated session, offline replica boundary, and outbox. `reauthentication` means the
|
|
517
530
|
identity remains valid but a recent sign-in is required; `credential` belongs to a domain feature
|
|
@@ -22,6 +22,18 @@ type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array | JWTVerifyGetKey;
|
|
|
22
22
|
* be verified against Google's rotating public keys.
|
|
23
23
|
*/
|
|
24
24
|
export declare const SECURETOKEN_JWK_URL = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
|
|
25
|
+
/** Expected rejection for Firebase-specific claims that passed JOSE signature/claim verification. */
|
|
26
|
+
export declare class FirebaseIdTokenValidationError extends Error {
|
|
27
|
+
readonly claim: 'subject' | 'exp' | 'iat' | 'auth_time';
|
|
28
|
+
/** Stable machine-readable code for authentication failure classifiers. */
|
|
29
|
+
readonly code = "ERR_FIREBASE_ID_TOKEN_INVALID";
|
|
30
|
+
/**
|
|
31
|
+
* Create a Firebase ID-token validation rejection.
|
|
32
|
+
*
|
|
33
|
+
* @param claim - Firebase-specific claim which failed validation.
|
|
34
|
+
*/
|
|
35
|
+
constructor(claim: 'subject' | 'exp' | 'iat' | 'auth_time');
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
38
|
* Verifies Firebase ID tokens with `jose` RS256 against Google's securetoken JWKS, and
|
|
27
39
|
* optionally looks up or deletes users via the Google Identity Toolkit REST API.
|
|
@@ -8,6 +8,22 @@ import { jwtVerify } from 'jose';
|
|
|
8
8
|
* be verified against Google's rotating public keys.
|
|
9
9
|
*/
|
|
10
10
|
export const SECURETOKEN_JWK_URL = 'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com';
|
|
11
|
+
/** Expected rejection for Firebase-specific claims that passed JOSE signature/claim verification. */
|
|
12
|
+
export class FirebaseIdTokenValidationError extends Error {
|
|
13
|
+
claim;
|
|
14
|
+
/** Stable machine-readable code for authentication failure classifiers. */
|
|
15
|
+
code = 'ERR_FIREBASE_ID_TOKEN_INVALID';
|
|
16
|
+
/**
|
|
17
|
+
* Create a Firebase ID-token validation rejection.
|
|
18
|
+
*
|
|
19
|
+
* @param claim - Firebase-specific claim which failed validation.
|
|
20
|
+
*/
|
|
21
|
+
constructor(claim) {
|
|
22
|
+
super(`Firebase ID token has an invalid ${claim}`);
|
|
23
|
+
this.claim = claim;
|
|
24
|
+
this.name = 'FirebaseIdTokenValidationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
/**
|
|
12
28
|
* Verifies Firebase ID tokens with `jose` RS256 against Google's securetoken JWKS, and
|
|
13
29
|
* optionally looks up or deletes users via the Google Identity Toolkit REST API.
|
|
@@ -74,18 +90,18 @@ export class JoseFirebaseVerifier {
|
|
|
74
90
|
const { payload } = typeof key === 'function' ? await jwtVerify(idToken, key, options) : await jwtVerify(idToken, key, options);
|
|
75
91
|
// Apply Firebase's documented checks beyond signature/iss/aud/exp.
|
|
76
92
|
if (!payload.sub || typeof payload.sub !== 'string' || payload.sub.length > 128) {
|
|
77
|
-
throw new
|
|
93
|
+
throw new FirebaseIdTokenValidationError('subject');
|
|
78
94
|
}
|
|
79
95
|
if (!Number.isFinite(payload.exp)) {
|
|
80
|
-
throw new
|
|
96
|
+
throw new FirebaseIdTokenValidationError('exp');
|
|
81
97
|
}
|
|
82
98
|
const issuedAt = payload.iat;
|
|
83
99
|
if (typeof issuedAt !== 'number' || !Number.isFinite(issuedAt) || issuedAt > now) {
|
|
84
|
-
throw new
|
|
100
|
+
throw new FirebaseIdTokenValidationError('iat');
|
|
85
101
|
}
|
|
86
102
|
const authTime = payload.auth_time;
|
|
87
103
|
if (typeof authTime !== 'number' || !Number.isFinite(authTime) || authTime > now) {
|
|
88
|
-
throw new
|
|
104
|
+
throw new FirebaseIdTokenValidationError('auth_time');
|
|
89
105
|
}
|
|
90
106
|
return { ...payload, uid: payload.sub, email: payload.email };
|
|
91
107
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export { validate, createValidate } from './middleware/validation.js';
|
|
|
14
14
|
export { createSentryValidate } from './middleware/validation.js';
|
|
15
15
|
export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, SentryScopeLike, } from './middleware/validation.js';
|
|
16
16
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
17
|
-
export { createAuthMiddleware } from './middleware/auth.js';
|
|
18
|
-
export type { AuthMiddlewareOptions } from './middleware/auth.js';
|
|
17
|
+
export { AuthTokenMissingError, createAuthMiddleware } from './middleware/auth.js';
|
|
18
|
+
export type { AuthMiddlewareFailureDetails, AuthMiddlewareFailureStage, AuthMiddlewareOptions, } from './middleware/auth.js';
|
|
19
19
|
export { perfLog } from './middleware/perf-log.js';
|
|
20
20
|
export type { PerfLogOptions, AnalyticsEngineDatasetLike } from './middleware/perf-log.js';
|
|
21
21
|
export { createMaintenanceMiddleware, createMaintenanceWaitHandler, isMaintenanceEnabled, MAINTENANCE_BODY, MAINTENANCE_CODE, MAINTENANCE_WAIT_PATH, } from './middleware/maintenance.js';
|
|
@@ -90,7 +90,7 @@ export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
|
|
|
90
90
|
export { getTemporaryCredentials } from './aws/sts.js';
|
|
91
91
|
export type { GetTemporaryCredentialsOptions, StsCredentials } from './aws/sts.js';
|
|
92
92
|
export type { DecodedIdToken, FirebaseVerifier } from './firebase/firebase-verifier.js';
|
|
93
|
-
export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
|
|
93
|
+
export { FirebaseIdTokenValidationError, JoseFirebaseVerifier, SECURETOKEN_JWK_URL, } from './firebase/jose-firebase-verifier.js';
|
|
94
94
|
export { IdentityToolkit } from './firebase/identity-toolkit.js';
|
|
95
95
|
export type { ServiceAccount } from './firebase/identity-toolkit.js';
|
|
96
96
|
export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier.js';
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { validate, createValidate } from './middleware/validation.js';
|
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional public re-export
|
|
17
17
|
export { createSentryValidate } from './middleware/validation.js';
|
|
18
18
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
19
|
-
export { createAuthMiddleware } from './middleware/auth.js';
|
|
19
|
+
export { AuthTokenMissingError, createAuthMiddleware } from './middleware/auth.js';
|
|
20
20
|
export { perfLog } from './middleware/perf-log.js';
|
|
21
21
|
export { createMaintenanceMiddleware, createMaintenanceWaitHandler, isMaintenanceEnabled, MAINTENANCE_BODY, MAINTENANCE_CODE, MAINTENANCE_WAIT_PATH, } from './middleware/maintenance.js';
|
|
22
22
|
export { createIsolateMemo } from './container/isolate-memo.js';
|
|
@@ -67,6 +67,6 @@ export { createAiGatewayProvider } from './ai/gateway.js';
|
|
|
67
67
|
export { getAuthenticationSecret } from './aws/secrets-manager.js';
|
|
68
68
|
export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
|
|
69
69
|
export { getTemporaryCredentials } from './aws/sts.js';
|
|
70
|
-
export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
|
|
70
|
+
export { FirebaseIdTokenValidationError, JoseFirebaseVerifier, SECURETOKEN_JWK_URL, } from './firebase/jose-firebase-verifier.js';
|
|
71
71
|
export { IdentityToolkit } from './firebase/identity-toolkit.js';
|
|
72
72
|
export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier.js';
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { Context, Env, MiddlewareHandler } from 'hono';
|
|
2
2
|
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
3
|
import type { AppInfo } from '../http/app-info.js';
|
|
4
|
+
/** Processing stage at which authentication middleware failed. */
|
|
5
|
+
export type AuthMiddlewareFailureStage = 'token' | 'verify' | 'appInfo' | 'resolveUserId' | 'setContext';
|
|
6
|
+
/** Safe metadata describing an authentication middleware failure without including the token. */
|
|
7
|
+
export interface AuthMiddlewareFailureDetails {
|
|
8
|
+
/** Stage which rejected or failed. */
|
|
9
|
+
stage: AuthMiddlewareFailureStage;
|
|
10
|
+
/** Whether the configured token header contained a non-blank value. */
|
|
11
|
+
tokenPresent: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Expected rejection raised when the configured authentication header is absent or blank. */
|
|
14
|
+
export declare class AuthTokenMissingError extends Error {
|
|
15
|
+
/** Stable machine-readable code for application classifiers. */
|
|
16
|
+
readonly code = "AUTH_TOKEN_MISSING";
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
4
19
|
/**
|
|
5
20
|
* Configuration for {@link createAuthMiddleware}.
|
|
6
21
|
*
|
|
@@ -11,10 +26,19 @@ import type { AppInfo } from '../http/app-info.js';
|
|
|
11
26
|
export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
12
27
|
/** Header carrying the ID token. Defaults to `'x-amz-security-token'`. */
|
|
13
28
|
tokenHeader?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Reject an absent or blank token before calling {@link AuthMiddlewareOptions.verify}.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Defaults to `false` for backward compatibility: existing consumers historically receive an
|
|
34
|
+
* empty string in `verify` when the header is absent. Enable this when the application wants a
|
|
35
|
+
* typed {@link AuthTokenMissingError} and does not use an empty token as custom input.
|
|
36
|
+
*/
|
|
37
|
+
rejectMissingToken?: boolean;
|
|
14
38
|
/**
|
|
15
39
|
* Verify the raw token and return the decoded value or user record.
|
|
16
40
|
*
|
|
17
|
-
* @param token - The raw token
|
|
41
|
+
* @param token - The raw token, or an empty string when absent unless `rejectMissingToken` is enabled.
|
|
18
42
|
* @param c - The current Hono context.
|
|
19
43
|
* @returns The verified value passed to {@link AuthMiddlewareOptions.resolveUserId}/{@link AuthMiddlewareOptions.setContext}.
|
|
20
44
|
* @throws If the token is invalid; rejecting/throwing triggers the failure path.
|
|
@@ -59,7 +83,17 @@ export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
|
59
83
|
* @param c - The current Hono context.
|
|
60
84
|
* @returns The failure response to send.
|
|
61
85
|
*/
|
|
62
|
-
onFailure?: (err: unknown, c: Context<E
|
|
86
|
+
onFailure?: (err: unknown, c: Context<E>, details: AuthMiddlewareFailureDetails) => Response | Promise<Response>;
|
|
87
|
+
/**
|
|
88
|
+
* Report a failed authentication attempt.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* When omitted, the historical behavior (`console.error(err)`) is preserved. Applications should
|
|
92
|
+
* provide this hook to suppress expected credential rejections while reporting dependency and
|
|
93
|
+
* internal failures through their normal observability path. The hook must never include raw
|
|
94
|
+
* authentication tokens in logs or telemetry.
|
|
95
|
+
*/
|
|
96
|
+
reportFailure?: (err: unknown, c: Context<E>, details: AuthMiddlewareFailureDetails) => void | Promise<void>;
|
|
63
97
|
/** Status used by the default `onFailure`. Defaults to `403`. */
|
|
64
98
|
failureStatus?: ContentfulStatusCode;
|
|
65
99
|
/** Message used by the default `onFailure`. Defaults to `'Forbidden resource'`. */
|
|
@@ -69,7 +103,7 @@ export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
|
69
103
|
* Create an authentication middleware equivalent to a NestJS `AuthGuard` / `TokenGuard`.
|
|
70
104
|
*
|
|
71
105
|
* The middleware runs a fixed skeleton — read the token header, `verify`, `getAppInfo`,
|
|
72
|
-
* `resolveUserId`, `setContext`, and on error `
|
|
106
|
+
* `resolveUserId`, `setContext`, and on error `reportFailure` then `onFailure` — while the
|
|
73
107
|
* application injects the variable parts (token verification, user-id resolution, context variable
|
|
74
108
|
* names, and the failure response). Omitting {@link AuthMiddlewareOptions.resolveUserId} yields a
|
|
75
109
|
* token-only middleware.
|
package/dist/middleware/auth.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { HTTPException } from 'hono/http-exception';
|
|
2
2
|
import { getAppInfo } from '../http/app-info.js';
|
|
3
|
+
/** Expected rejection raised when the configured authentication header is absent or blank. */
|
|
4
|
+
export class AuthTokenMissingError extends Error {
|
|
5
|
+
/** Stable machine-readable code for application classifiers. */
|
|
6
|
+
code = 'AUTH_TOKEN_MISSING';
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Authentication token is missing');
|
|
9
|
+
this.name = 'AuthTokenMissingError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* Create an authentication middleware equivalent to a NestJS `AuthGuard` / `TokenGuard`.
|
|
5
14
|
*
|
|
6
15
|
* The middleware runs a fixed skeleton — read the token header, `verify`, `getAppInfo`,
|
|
7
|
-
* `resolveUserId`, `setContext`, and on error `
|
|
16
|
+
* `resolveUserId`, `setContext`, and on error `reportFailure` then `onFailure` — while the
|
|
8
17
|
* application injects the variable parts (token verification, user-id resolution, context variable
|
|
9
18
|
* names, and the failure response). Omitting {@link AuthMiddlewareOptions.resolveUserId} yields a
|
|
10
19
|
* token-only middleware.
|
|
@@ -30,22 +39,42 @@ import { getAppInfo } from '../http/app-info.js';
|
|
|
30
39
|
* ```
|
|
31
40
|
*/
|
|
32
41
|
export function createAuthMiddleware(options) {
|
|
33
|
-
const { tokenHeader = 'x-amz-security-token', verify, resolveUserId, setContext, onFailure, failureStatus = 403, failureMessage = 'Forbidden resource', } = options;
|
|
42
|
+
const { tokenHeader = 'x-amz-security-token', rejectMissingToken = false, verify, resolveUserId, setContext, onFailure, reportFailure, failureStatus = 403, failureMessage = 'Forbidden resource', } = options;
|
|
34
43
|
return async (c, next) => {
|
|
44
|
+
let stage = 'token';
|
|
45
|
+
let tokenPresent = false;
|
|
35
46
|
try {
|
|
36
47
|
const token = c.req.header(tokenHeader) ?? '';
|
|
48
|
+
tokenPresent = token.trim().length > 0;
|
|
49
|
+
if (!tokenPresent && rejectMissingToken) {
|
|
50
|
+
throw new AuthTokenMissingError();
|
|
51
|
+
}
|
|
52
|
+
stage = 'verify';
|
|
37
53
|
const verified = await verify(token, c);
|
|
54
|
+
stage = 'appInfo';
|
|
38
55
|
const appInfo = getAppInfo(c);
|
|
56
|
+
stage = 'resolveUserId';
|
|
39
57
|
const userId = resolveUserId ? await resolveUserId(verified, c, appInfo) : undefined;
|
|
58
|
+
stage = 'setContext';
|
|
40
59
|
setContext(c, { verified, appInfo, userId });
|
|
41
60
|
}
|
|
42
61
|
catch (e) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
const details = { stage, tokenPresent };
|
|
63
|
+
if (reportFailure) {
|
|
64
|
+
try {
|
|
65
|
+
await reportFailure(e, c, details);
|
|
66
|
+
}
|
|
67
|
+
catch (reportingError) {
|
|
68
|
+
// Observability must never alter the authentication response.
|
|
69
|
+
console.error(reportingError);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Preserve the historical default for consumers that have not adopted classified reporting.
|
|
74
|
+
console.error(e);
|
|
75
|
+
}
|
|
47
76
|
if (onFailure) {
|
|
48
|
-
return onFailure(e, c);
|
|
77
|
+
return onFailure(e, c, details);
|
|
49
78
|
}
|
|
50
79
|
throw new HTTPException(failureStatus, { message: failureMessage });
|
|
51
80
|
}
|