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.
- package/dist/auth/index.d.ts +5 -4
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +127 -53
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/token-decoders.d.ts +13 -26
- package/dist/auth/token-decoders.d.ts.map +1 -1
- package/dist/auth/token-decoders.js +102 -122
- package/dist/auth/token-decoders.js.map +1 -1
- package/dist/auth/verify-token.d.ts +45 -9
- package/dist/auth/verify-token.d.ts.map +1 -1
- package/dist/auth/verify-token.js +63 -27
- package/dist/auth/verify-token.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/auth.test.ts +221 -57
- package/src/auth/custom-token-exchange.test.ts +100 -0
- package/src/auth/index.ts +132 -53
- package/src/auth/oracle-admin-answers.test.ts +40 -4
- package/src/auth/token-decoders.ts +98 -141
- package/src/auth/verify-token.ts +78 -34
|
@@ -1,130 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `verifyIdToken`. Exported as a constant so tests can lock the shape.
|
|
2
|
+
* The ID token check behind `pyric-admin/auth` `verifyIdToken`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* NOT a JWT. NOT signed. Do not use this token format to talk to any
|
|
14
|
-
* real Firebase service — it only round-trips through this same
|
|
15
|
-
* sandbox backend.
|
|
4
|
+
* The only token that verifies is the JWT pyric's Auth mints
|
|
5
|
+
* (`pyric/sandbox/internal` `mintIdToken`), checked by the shared
|
|
6
|
+
* {@link checkIdToken} and then by firebase-admin 13.10.0's rules, in its
|
|
7
|
+
* order (lib/auth/token-verifier.js `verifyContent` and `verifySignature`,
|
|
8
|
+
* lib/utils/jwt.js, and jsonwebtoken's `nbf` and `exp`). A plain string, or
|
|
9
|
+
* the client token shape from before #1836, fails to decode, and a custom
|
|
10
|
+
* token is refused as one, as in production (#1949, #1952).
|
|
16
11
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
import { checkIdToken, CUSTOM_TOKEN_AUDIENCE } from 'pyric/sandbox/internal';
|
|
13
|
+
const ID_TOKEN_ISSUER = 'https://securetoken.google.com/';
|
|
14
|
+
const MAX_SUBJECT_LENGTH = 128;
|
|
15
|
+
const DOCS = ' See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.';
|
|
16
|
+
const PROJECT_MATCH = ' Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK.';
|
|
17
|
+
const MESSAGES = {
|
|
18
|
+
decode: `Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token.${DOCS}`,
|
|
19
|
+
customToken: `verifyIdToken() expects an ID token, but was given a custom token.${DOCS}`,
|
|
20
|
+
legacyCustomToken: `verifyIdToken() expects an ID token, but was given a legacy custom token.${DOCS}`,
|
|
21
|
+
noKid: `Firebase ID token has no "kid" claim.${DOCS}`,
|
|
22
|
+
algorithm: (alg) => `Firebase ID token has incorrect algorithm. Expected "RS256" but got "${String(alg)}".${DOCS}`,
|
|
23
|
+
audience: (projectId, aud) => `Firebase ID token has incorrect "aud" (audience) claim. Expected "${projectId}" but got "${String(aud)}".${PROJECT_MATCH}${DOCS}`,
|
|
24
|
+
issuer: (projectId, iss) => `Firebase ID token has incorrect "iss" (issuer) claim. Expected "${ID_TOKEN_ISSUER}${projectId}" but got "${String(iss)}".${PROJECT_MATCH}${DOCS}`,
|
|
25
|
+
noSub: `Firebase ID token has no "sub" (subject) claim.${DOCS}`,
|
|
26
|
+
emptySub: `Firebase ID token has an empty "sub" (subject) claim.${DOCS}`,
|
|
27
|
+
longSub: `Firebase ID token has a "sub" (subject) claim longer than 128 characters.${DOCS}`,
|
|
28
|
+
unknownKid: 'Firebase ID token has "kid" claim which does not correspond to a known public key. Most likely the ID token is expired, so get a fresh token from your client app and try again.',
|
|
29
|
+
signature: `Firebase ID token has invalid signature.${DOCS}`,
|
|
30
|
+
expired: `Firebase ID token has expired. Get a fresh ID token from your client app and try again (auth/id-token-expired).${DOCS}`,
|
|
31
|
+
};
|
|
32
|
+
function refused(message) {
|
|
33
|
+
return { ok: false, code: 'argument-error', message };
|
|
22
34
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
/** token-verifier.js: a missing `kid` names a custom token, a legacy custom token, or neither. */
|
|
36
|
+
function missingKidMessage(header, claims) {
|
|
37
|
+
if (claims.aud === CUSTOM_TOKEN_AUDIENCE)
|
|
38
|
+
return MESSAGES.customToken;
|
|
39
|
+
const legacyData = claims.d;
|
|
40
|
+
const isLegacyCustomToken = header.alg === 'HS256' && claims.v === 0 && typeof legacyData === 'object' && legacyData !== null && 'uid' in legacyData;
|
|
41
|
+
if (isLegacyCustomToken)
|
|
42
|
+
return MESSAGES.legacyCustomToken;
|
|
43
|
+
return MESSAGES.noKid;
|
|
27
44
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (hasExplicitSubjectClaim) {
|
|
32
|
-
return rawSub;
|
|
33
|
-
}
|
|
34
|
-
const prefixSegment = idToken.slice('sandbox-id-token-'.length, firstColon);
|
|
35
|
-
const lastHyphen = prefixSegment.lastIndexOf('-');
|
|
36
|
-
const hasHyphenatedSerialSuffix = lastHyphen > 0;
|
|
37
|
-
if (hasHyphenatedSerialSuffix) {
|
|
38
|
-
return prefixSegment.slice(0, lastHyphen);
|
|
39
|
-
}
|
|
40
|
-
return prefixSegment;
|
|
45
|
+
/** Auth blocking events with no user record, whose `sub` firebase-admin leaves unchecked. */
|
|
46
|
+
function skipsSubjectCheck(claims) {
|
|
47
|
+
return claims.event_type === 'beforeSendSms' || claims.event_type === 'beforeSendEmail';
|
|
41
48
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
function subjectMessage(sub) {
|
|
50
|
+
if (typeof sub !== 'string')
|
|
51
|
+
return MESSAGES.noSub;
|
|
52
|
+
if (sub === '')
|
|
53
|
+
return MESSAGES.emptySub;
|
|
54
|
+
if (sub.length > MAX_SUBJECT_LENGTH)
|
|
55
|
+
return MESSAGES.longSub;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/** token-verifier.js `verifyContent`: `kid`, `alg`, `aud`, `iss`, then `sub`; the first failure wins. */
|
|
59
|
+
function contentMessage(header, claims, projectId) {
|
|
60
|
+
if (header.kid === undefined)
|
|
61
|
+
return missingKidMessage(header, claims);
|
|
62
|
+
if (header.alg !== 'RS256')
|
|
63
|
+
return MESSAGES.algorithm(header.alg);
|
|
64
|
+
if (claims.aud !== projectId)
|
|
65
|
+
return MESSAGES.audience(projectId, claims.aud);
|
|
66
|
+
if (claims.iss !== `${ID_TOKEN_ISSUER}${projectId}`)
|
|
67
|
+
return MESSAGES.issuer(projectId, claims.iss);
|
|
68
|
+
if (skipsSubjectCheck(claims))
|
|
69
|
+
return undefined;
|
|
70
|
+
return subjectMessage(claims.sub);
|
|
71
|
+
}
|
|
72
|
+
/** jsonwebtoken's `nbf` rule, which runs before `exp`; firebase-admin reports a failure as an invalid signature. */
|
|
73
|
+
function isNotYetValid(nbf, nowSec) {
|
|
74
|
+
if (nbf === undefined)
|
|
75
|
+
return false;
|
|
76
|
+
return typeof nbf !== 'number' || nbf > nowSec;
|
|
77
|
+
}
|
|
78
|
+
/** jsonwebtoken's `exp` rule: an absent `exp` never expires, and a non-number is an invalid signature. */
|
|
79
|
+
function expiryVerdict(exp, nowSec) {
|
|
80
|
+
if (exp === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (typeof exp !== 'number')
|
|
83
|
+
return refused(MESSAGES.signature);
|
|
84
|
+
const hasExpired = nowSec >= exp;
|
|
85
|
+
if (hasExpired)
|
|
86
|
+
return { ok: false, code: 'id-token-expired', message: MESSAGES.expired };
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks an ID token for `projectId` at `nowSec`, in firebase-admin's order:
|
|
91
|
+
* decoding, the content rules, the key and signature, then `nbf` and `exp`.
|
|
92
|
+
*/
|
|
93
|
+
export async function checkClientIdToken(idToken, projectId, nowSec) {
|
|
94
|
+
const check = await checkIdToken(idToken);
|
|
95
|
+
if (!check.ok && check.reason === 'malformed')
|
|
96
|
+
return refused(MESSAGES.decode);
|
|
97
|
+
const contentFailure = contentMessage(check.header, check.claims, projectId);
|
|
98
|
+
if (contentFailure !== undefined)
|
|
99
|
+
return refused(contentFailure);
|
|
100
|
+
if (!check.ok)
|
|
101
|
+
return refused(check.reason === 'kid' ? MESSAGES.unknownKid : MESSAGES.signature);
|
|
102
|
+
if (isNotYetValid(check.claims.nbf, nowSec))
|
|
103
|
+
return refused(MESSAGES.signature);
|
|
104
|
+
const expired = expiryVerdict(check.claims.exp, nowSec);
|
|
105
|
+
if (expired !== undefined)
|
|
106
|
+
return expired;
|
|
107
|
+
const token = { ...check.claims, uid: check.claims.sub };
|
|
108
|
+
return { ok: true, token };
|
|
56
109
|
}
|
|
57
|
-
const customTokenDecoder = {
|
|
58
|
-
name: 'custom-token',
|
|
59
|
-
prefix: `${SANDBOX_TOKEN_PREFIX}:`,
|
|
60
|
-
decode(idToken, nowSec) {
|
|
61
|
-
const firstColon = idToken.indexOf(':');
|
|
62
|
-
const secondColon = idToken.indexOf(':', firstColon + 1);
|
|
63
|
-
const isMissingClaimsSegment = secondColon < 0;
|
|
64
|
-
if (isMissingClaimsSegment) {
|
|
65
|
-
throw new Error(`pyric-admin/auth: verifyIdToken received a malformed sandbox token (missing claims segment): ${idToken}`);
|
|
66
|
-
}
|
|
67
|
-
const uid = idToken.slice(firstColon + 1, secondColon);
|
|
68
|
-
const jsonClaims = idToken.slice(secondColon + 1);
|
|
69
|
-
let claims;
|
|
70
|
-
try {
|
|
71
|
-
claims = JSON.parse(jsonClaims);
|
|
72
|
-
}
|
|
73
|
-
catch (e) {
|
|
74
|
-
throw new Error(`pyric-admin/auth: verifyIdToken failed to parse sandbox token claims as JSON: ${e.message}`);
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
aud: 'pyric-sandbox',
|
|
78
|
-
auth_time: nowSec,
|
|
79
|
-
exp: nowSec + 3600,
|
|
80
|
-
firebase: {
|
|
81
|
-
identities: {},
|
|
82
|
-
sign_in_provider: 'custom',
|
|
83
|
-
},
|
|
84
|
-
iat: nowSec,
|
|
85
|
-
iss: 'pyric-sandbox',
|
|
86
|
-
sub: uid,
|
|
87
|
-
uid,
|
|
88
|
-
...claims,
|
|
89
|
-
};
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
const clientIdTokenDecoder = {
|
|
93
|
-
name: 'client-id-token',
|
|
94
|
-
prefix: 'sandbox-id-token-',
|
|
95
|
-
decode(idToken, nowSec) {
|
|
96
|
-
const firstColon = idToken.indexOf(':');
|
|
97
|
-
const isMissingClaimsSegment = firstColon < 0;
|
|
98
|
-
if (isMissingClaimsSegment) {
|
|
99
|
-
throw new Error(`pyric-admin/auth: verifyIdToken received a malformed client ID token (missing claims segment after colon): ${idToken}`);
|
|
100
|
-
}
|
|
101
|
-
const jsonClaims = idToken.slice(firstColon + 1);
|
|
102
|
-
let claims;
|
|
103
|
-
try {
|
|
104
|
-
claims = JSON.parse(jsonClaims);
|
|
105
|
-
}
|
|
106
|
-
catch (e) {
|
|
107
|
-
throw new Error(`pyric-admin/auth: verifyIdToken failed to parse client ID token claims as JSON: ${e.message}`);
|
|
108
|
-
}
|
|
109
|
-
const uid = resolveUid(idToken, firstColon, claims);
|
|
110
|
-
const aud = resolveStringClaim(claims, 'aud', 'pyric-sandbox');
|
|
111
|
-
const iss = resolveStringClaim(claims, 'iss', 'https://sandbox.pyric.dev');
|
|
112
|
-
const exp = resolveNumberClaim(claims, 'exp', nowSec + 3600);
|
|
113
|
-
const iat = resolveNumberClaim(claims, 'iat', nowSec);
|
|
114
|
-
const authTime = resolveNumberClaim(claims, 'auth_time', nowSec);
|
|
115
|
-
const firebase = resolveFirebaseClaim(claims);
|
|
116
|
-
return {
|
|
117
|
-
...claims,
|
|
118
|
-
aud,
|
|
119
|
-
auth_time: authTime,
|
|
120
|
-
exp,
|
|
121
|
-
firebase,
|
|
122
|
-
iat,
|
|
123
|
-
iss,
|
|
124
|
-
sub: uid,
|
|
125
|
-
uid,
|
|
126
|
-
};
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
export const TOKEN_DECODERS = [customTokenDecoder, clientIdTokenDecoder];
|
|
130
110
|
//# sourceMappingURL=token-decoders.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-decoders.js","sourceRoot":"","sources":["../../src/auth/token-decoders.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token-decoders.js","sourceRoot":"","sources":["../../src/auth/token-decoders.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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';
|
|
9
|
+
import { type IdTokenFailureCode } from './token-decoders.js';
|
|
10
|
+
/** The codes `checkRevoked` and `revokeRefreshTokens` add to a token check's. */
|
|
11
|
+
type AccountFailureCode = 'user-not-found' | 'user-disabled' | 'id-token-revoked' | 'invalid-uid';
|
|
12
|
+
/** firebase-admin's FirebaseAuthError: a PrefixedFirebaseError with prefix `auth`. */
|
|
13
|
+
export declare class FirebaseAuthError extends PrefixedFirebaseError {
|
|
14
|
+
constructor(code: IdTokenFailureCode | AccountFailureCode, message: string);
|
|
15
|
+
}
|
|
16
|
+
/** What one verification checks against. */
|
|
17
|
+
export interface IdTokenExpectation {
|
|
18
|
+
/** The project the token's `aud` and `iss` must name. */
|
|
19
|
+
projectId: string;
|
|
20
|
+
/** The current time, in epoch milliseconds, that `nbf` and `exp` are read against. */
|
|
21
|
+
nowMs: number;
|
|
22
|
+
}
|
|
6
23
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
24
|
+
* Throws `auth/argument-error` synchronously for a token that is not a string,
|
|
25
|
+
* as firebase-admin's `verifyJWT` does, so an arm that reads its clock before
|
|
26
|
+
* verifying can check the argument first.
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertIdTokenArgument(idToken: unknown): void;
|
|
29
|
+
/**
|
|
30
|
+
* Verifies an ID token minted by pyric's Auth for `expected.projectId`.
|
|
10
31
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
32
|
+
* A token that is not a string throws `auth/argument-error` synchronously, as
|
|
33
|
+
* firebase-admin's `verifyJWT` does. Every other failure rejects with
|
|
34
|
+
* `auth/argument-error`, or `auth/id-token-expired` for an expired token.
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifySandboxIdToken(idToken: string, expected: IdTokenExpectation): Promise<DecodedIdToken>;
|
|
37
|
+
/**
|
|
38
|
+
* firebase-admin's answer for a uid that is not a non-empty string of at most
|
|
39
|
+
* 128 characters (13.10.0 `validator.isUid`), checked before any request.
|
|
40
|
+
*/
|
|
41
|
+
export declare function invalidUid(uid: unknown): FirebaseAuthError | null;
|
|
42
|
+
/** firebase-admin's `getUser` answer for a uid with no record. */
|
|
43
|
+
export declare function userNotFound(): FirebaseAuthError;
|
|
44
|
+
/**
|
|
45
|
+
* firebase-admin's `verifyDecodedJWTNotRevokedOrDisabled` (13.10.0
|
|
46
|
+
* `lib/auth/base-auth.js`), the `checkRevoked` step: the account must exist,
|
|
47
|
+
* be enabled, and have no revocation after the token's `auth_time`, compared
|
|
48
|
+
* in whole seconds as production stores it.
|
|
14
49
|
*/
|
|
15
|
-
export declare function
|
|
50
|
+
export declare function assertNotRevokedOrDisabled(decoded: DecodedIdToken, standing: AuthAccountStanding): DecodedIdToken;
|
|
51
|
+
export {};
|
|
16
52
|
//# sourceMappingURL=verify-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-token.d.ts","sourceRoot":"","sources":["../../src/auth/verify-token.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"verify-token.d.ts","sourceRoot":"","sources":["../../src/auth/verify-token.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAElF,iFAAiF;AACjF,KAAK,kBAAkB,GAAG,gBAAgB,GAAG,eAAe,GAAG,kBAAkB,GAAG,aAAa,CAAC;AAElG,sFAAsF;AACtF,qBAAa,iBAAkB,SAAQ,qBAAqB;gBAC9C,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,EAAE,OAAO,EAAE,MAAM;CAG3E;AAED,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAG5D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAO3G;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAIjE;AAED,kEAAkE;AAClE,wBAAgB,YAAY,IAAI,iBAAiB,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,mBAAmB,GAAG,cAAc,CAQjH"}
|
|
@@ -1,32 +1,68 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrefixedFirebaseError } from '../app/firebase-error.js';
|
|
2
|
+
import { checkClientIdToken } from './token-decoders.js';
|
|
3
|
+
/** firebase-admin's FirebaseAuthError: a PrefixedFirebaseError with prefix `auth`. */
|
|
4
|
+
export class FirebaseAuthError extends PrefixedFirebaseError {
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super('auth', code, message);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const NOT_A_STRING = 'First argument to verifyIdToken() must be a Firebase ID token string.';
|
|
2
10
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Throws on any token that doesn't match a recognized sandbox prefix —
|
|
8
|
-
* including real JWTs that another verifier would parse. The sandbox
|
|
9
|
-
* backends are intentionally not drop-ins for production verification.
|
|
11
|
+
* Throws `auth/argument-error` synchronously for a token that is not a string,
|
|
12
|
+
* as firebase-admin's `verifyJWT` does, so an arm that reads its clock before
|
|
13
|
+
* verifying can check the argument first.
|
|
10
14
|
*/
|
|
11
|
-
export function
|
|
15
|
+
export function assertIdTokenArgument(idToken) {
|
|
12
16
|
const isInvalidTokenArgument = typeof idToken !== 'string';
|
|
13
|
-
if (isInvalidTokenArgument)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
if (isInvalidTokenArgument)
|
|
18
|
+
throw new FirebaseAuthError('argument-error', NOT_A_STRING);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Verifies an ID token minted by pyric's Auth for `expected.projectId`.
|
|
22
|
+
*
|
|
23
|
+
* A token that is not a string throws `auth/argument-error` synchronously, as
|
|
24
|
+
* firebase-admin's `verifyJWT` does. Every other failure rejects with
|
|
25
|
+
* `auth/argument-error`, or `auth/id-token-expired` for an expired token.
|
|
26
|
+
*/
|
|
27
|
+
export function verifySandboxIdToken(idToken, expected) {
|
|
28
|
+
assertIdTokenArgument(idToken);
|
|
29
|
+
const nowSec = Math.floor(expected.nowMs / 1000);
|
|
30
|
+
return checkClientIdToken(idToken, expected.projectId, nowSec).then((verdict) => {
|
|
31
|
+
if (!verdict.ok)
|
|
32
|
+
throw new FirebaseAuthError(verdict.code, verdict.message);
|
|
33
|
+
return verdict.token;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* firebase-admin's answer for a uid that is not a non-empty string of at most
|
|
38
|
+
* 128 characters (13.10.0 `validator.isUid`), checked before any request.
|
|
39
|
+
*/
|
|
40
|
+
export function invalidUid(uid) {
|
|
41
|
+
const isUid = typeof uid === 'string' && uid.length > 0 && uid.length <= 128;
|
|
42
|
+
if (isUid)
|
|
43
|
+
return null;
|
|
44
|
+
return new FirebaseAuthError('invalid-uid', 'The uid must be a non-empty string with at most 128 characters.');
|
|
45
|
+
}
|
|
46
|
+
/** firebase-admin's `getUser` answer for a uid with no record. */
|
|
47
|
+
export function userNotFound() {
|
|
48
|
+
return new FirebaseAuthError('user-not-found', 'There is no user record corresponding to the provided identifier.');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* firebase-admin's `verifyDecodedJWTNotRevokedOrDisabled` (13.10.0
|
|
52
|
+
* `lib/auth/base-auth.js`), the `checkRevoked` step: the account must exist,
|
|
53
|
+
* be enabled, and have no revocation after the token's `auth_time`, compared
|
|
54
|
+
* in whole seconds as production stores it.
|
|
55
|
+
*/
|
|
56
|
+
export function assertNotRevokedOrDisabled(decoded, standing) {
|
|
57
|
+
const hasNoRecord = standing.kind === 'deleted' || standing.kind === 'unrecorded';
|
|
58
|
+
if (hasNoRecord)
|
|
59
|
+
throw userNotFound();
|
|
60
|
+
if (standing.kind === 'disabled')
|
|
61
|
+
throw new FirebaseAuthError('user-disabled', 'The user record is disabled.');
|
|
62
|
+
const validAfterMs = standing.tokensValidAfterMs;
|
|
63
|
+
const revoked = validAfterMs !== null && decoded.auth_time * 1000 < validAfterMs;
|
|
64
|
+
if (revoked)
|
|
65
|
+
throw new FirebaseAuthError('id-token-revoked', 'The Firebase ID token has been revoked.');
|
|
66
|
+
return decoded;
|
|
31
67
|
}
|
|
32
68
|
//# sourceMappingURL=verify-token.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-token.js","sourceRoot":"","sources":["../../src/auth/verify-token.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify-token.js","sourceRoot":"","sources":["../../src/auth/verify-token.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAA2B,MAAM,qBAAqB,CAAC;AAKlF,sFAAsF;AACtF,MAAM,OAAO,iBAAkB,SAAQ,qBAAqB;IAC1D,YAAY,IAA6C,EAAE,OAAe;QACxE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF;AAUD,MAAM,YAAY,GAAG,uEAAuE,CAAC;AAE7F;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,MAAM,sBAAsB,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC;IAC3D,IAAI,sBAAsB;QAAE,MAAM,IAAI,iBAAiB,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;AAC1F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,QAA4B;IAChF,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACjD,OAAO,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9E,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,IAAI,iBAAiB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5E,OAAO,OAAO,CAAC,KAAK,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,GAAY;IACrC,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC;IAC7E,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,IAAI,iBAAiB,CAAC,aAAa,EAAE,iEAAiE,CAAC,CAAC;AACjH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,iBAAiB,CAAC,gBAAgB,EAAE,mEAAmE,CAAC,CAAC;AACtH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAAuB,EAAE,QAA6B;IAC/F,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,CAAC;IAClF,IAAI,WAAW;QAAE,MAAM,YAAY,EAAE,CAAC;IACtC,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU;QAAE,MAAM,IAAI,iBAAiB,CAAC,eAAe,EAAE,8BAA8B,CAAC,CAAC;IAC/G,MAAM,YAAY,GAAG,QAAQ,CAAC,kBAAkB,CAAC;IACjD,MAAM,OAAO,GAAG,YAAY,KAAK,IAAI,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,GAAG,YAAY,CAAC;IACjF,IAAI,OAAO;QAAE,MAAM,IAAI,iBAAiB,CAAC,kBAAkB,EAAE,yCAAyC,CAAC,CAAC;IACxG,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyric-admin",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.34",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://pyric.dev",
|
|
6
6
|
"repository": {
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"typecheck": "bun x tsc -p tsconfig.json --noEmit"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"pyric": "^0.1.0-alpha.
|
|
109
|
+
"pyric": "^0.1.0-alpha.34"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@types/bun": "1.3.14",
|