pyric-admin 0.1.0-alpha.32 → 0.1.0-alpha.34

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.
@@ -1,42 +1,86 @@
1
1
  /**
2
- * Stateless ID token verification for `pyric-admin/auth`.
3
- * Routes token parsing across recognized decoder strategies.
2
+ * `verifyIdToken` for `pyric-admin/auth`, shared by the in-process and remote
3
+ * arms. Each arm passes the project the token must be scoped to and the
4
+ * current time on its own clock.
4
5
  */
6
+ import type { AuthAccountStanding } from 'pyric/sandbox/internal';
7
+ import { PrefixedFirebaseError } from '../app/firebase-error.js';
5
8
  import type { DecodedIdToken } from './types.js';
6
- import { SANDBOX_TOKEN_PREFIX, TOKEN_DECODERS } from './token-decoders.js';
9
+ import { checkClientIdToken, type IdTokenFailureCode } from './token-decoders.js';
10
+
11
+ /** The codes `checkRevoked` and `revokeRefreshTokens` add to a token check's. */
12
+ type AccountFailureCode = 'user-not-found' | 'user-disabled' | 'id-token-revoked' | 'invalid-uid';
13
+
14
+ /** firebase-admin's FirebaseAuthError: a PrefixedFirebaseError with prefix `auth`. */
15
+ export class FirebaseAuthError extends PrefixedFirebaseError {
16
+ constructor(code: IdTokenFailureCode | AccountFailureCode, message: string) {
17
+ super('auth', code, message);
18
+ }
19
+ }
20
+
21
+ /** What one verification checks against. */
22
+ export interface IdTokenExpectation {
23
+ /** The project the token's `aud` and `iss` must name. */
24
+ projectId: string;
25
+ /** The current time, in epoch milliseconds, that `nbf` and `exp` are read against. */
26
+ nowMs: number;
27
+ }
28
+
29
+ const NOT_A_STRING = 'First argument to verifyIdToken() must be a Firebase ID token string.';
7
30
 
8
31
  /**
9
- * Parse a token minted by `createCustomToken` or a client ID token
10
- * minted by `pyric/auth`'s `getIdToken()`. Routes to the appropriate
11
- * modular token decoder based on token prefix.
12
- *
13
- * Throws on any token that doesn't match a recognized sandbox prefix —
14
- * including real JWTs that another verifier would parse. The sandbox
15
- * backends are intentionally not drop-ins for production verification.
32
+ * Throws `auth/argument-error` synchronously for a token that is not a string,
33
+ * as firebase-admin's `verifyJWT` does, so an arm that reads its clock before
34
+ * verifying can check the argument first.
16
35
  */
17
- export function verifySandboxIdToken(idToken: string): Promise<DecodedIdToken> {
36
+ export function assertIdTokenArgument(idToken: unknown): void {
18
37
  const isInvalidTokenArgument = typeof idToken !== 'string';
19
- if (isInvalidTokenArgument) {
20
- return Promise.reject(
21
- new Error('pyric-admin/auth: verifyIdToken expects a string argument.'),
22
- );
23
- }
24
- const nowSec = Math.floor(Date.now() / 1000);
25
- for (const decoder of TOKEN_DECODERS) {
26
- const matchesDecoderPrefix = idToken.startsWith(decoder.prefix);
27
- if (matchesDecoderPrefix) {
28
- try {
29
- return Promise.resolve(decoder.decode(idToken, nowSec));
30
- } catch (error) {
31
- return Promise.reject(error);
32
- }
33
- }
34
- }
35
- return Promise.reject(
36
- new Error(
37
- 'pyric-admin/auth: verifyIdToken on the sandbox backend only ' +
38
- 'accepts tokens minted by this sandbox\'s createCustomToken or client getIdToken(). ' +
39
- `Token must begin with "${SANDBOX_TOKEN_PREFIX}:" or "sandbox-id-token-".`,
40
- ),
41
- );
38
+ if (isInvalidTokenArgument) throw new FirebaseAuthError('argument-error', NOT_A_STRING);
39
+ }
40
+
41
+ /**
42
+ * Verifies an ID token minted by pyric's Auth for `expected.projectId`.
43
+ *
44
+ * A token that is not a string throws `auth/argument-error` synchronously, as
45
+ * firebase-admin's `verifyJWT` does. Every other failure rejects with
46
+ * `auth/argument-error`, or `auth/id-token-expired` for an expired token.
47
+ */
48
+ export function verifySandboxIdToken(idToken: string, expected: IdTokenExpectation): Promise<DecodedIdToken> {
49
+ assertIdTokenArgument(idToken);
50
+ const nowSec = Math.floor(expected.nowMs / 1000);
51
+ return checkClientIdToken(idToken, expected.projectId, nowSec).then((verdict) => {
52
+ if (!verdict.ok) throw new FirebaseAuthError(verdict.code, verdict.message);
53
+ return verdict.token;
54
+ });
55
+ }
56
+
57
+ /**
58
+ * firebase-admin's answer for a uid that is not a non-empty string of at most
59
+ * 128 characters (13.10.0 `validator.isUid`), checked before any request.
60
+ */
61
+ export function invalidUid(uid: unknown): FirebaseAuthError | null {
62
+ const isUid = typeof uid === 'string' && uid.length > 0 && uid.length <= 128;
63
+ if (isUid) return null;
64
+ return new FirebaseAuthError('invalid-uid', 'The uid must be a non-empty string with at most 128 characters.');
65
+ }
66
+
67
+ /** firebase-admin's `getUser` answer for a uid with no record. */
68
+ export function userNotFound(): FirebaseAuthError {
69
+ return new FirebaseAuthError('user-not-found', 'There is no user record corresponding to the provided identifier.');
70
+ }
71
+
72
+ /**
73
+ * firebase-admin's `verifyDecodedJWTNotRevokedOrDisabled` (13.10.0
74
+ * `lib/auth/base-auth.js`), the `checkRevoked` step: the account must exist,
75
+ * be enabled, and have no revocation after the token's `auth_time`, compared
76
+ * in whole seconds as production stores it.
77
+ */
78
+ export function assertNotRevokedOrDisabled(decoded: DecodedIdToken, standing: AuthAccountStanding): DecodedIdToken {
79
+ const hasNoRecord = standing.kind === 'deleted' || standing.kind === 'unrecorded';
80
+ if (hasNoRecord) throw userNotFound();
81
+ if (standing.kind === 'disabled') throw new FirebaseAuthError('user-disabled', 'The user record is disabled.');
82
+ const validAfterMs = standing.tokensValidAfterMs;
83
+ const revoked = validAfterMs !== null && decoded.auth_time * 1000 < validAfterMs;
84
+ if (revoked) throw new FirebaseAuthError('id-token-revoked', 'The Firebase ID token has been revoked.');
85
+ return decoded;
42
86
  }