Skip to content

Commit 161fc97

Browse files
committed
added revoke function
1 parent ac80e34 commit 161fc97

File tree

7 files changed

+1522
-40
lines changed

7 files changed

+1522
-40
lines changed

src/auth-api-requests.ts

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,38 @@ import { ApiSettings } from './api-requests';
22
import { BaseClient } from './client';
33
import type { EmulatorEnv } from './emulator';
44
import { AuthClientErrorCode, FirebaseAuthError } from './errors';
5-
import { isNonEmptyString, isNumber } from './validator';
5+
import { UserRecord } from './user-record';
6+
import { isNonEmptyString, isNumber, isObject, isUid } from './validator';
67

78
/** Minimum allowed session cookie duration in seconds (5 minutes). */
89
const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60;
910

1011
/** Maximum allowed session cookie duration in seconds (2 weeks). */
1112
const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60;
1213

14+
/** List of reserved claims which cannot be provided when creating a custom token. */
15+
const RESERVED_CLAIMS = [
16+
'acr',
17+
'amr',
18+
'at_hash',
19+
'aud',
20+
'auth_time',
21+
'azp',
22+
'cnf',
23+
'c_hash',
24+
'exp',
25+
'iat',
26+
'iss',
27+
'jti',
28+
'nbf',
29+
'nonce',
30+
'sub',
31+
'firebase',
32+
];
33+
34+
/** Maximum allowed number of characters in the custom claims payload. */
35+
const MAX_CLAIMS_PAYLOAD_SIZE = 1000;
36+
1337
/**
1438
* Instantiates the createSessionCookie endpoint settings.
1539
*
@@ -39,6 +63,131 @@ export const FIREBASE_AUTH_CREATE_SESSION_COOKIE = new ApiSettings('v1', ':creat
3963
}
4064
});
4165

66+
interface GetAccountInfoRequest {
67+
localId?: string[];
68+
email?: string[];
69+
phoneNumber?: string[];
70+
federatedUserId?: Array<{
71+
providerId: string;
72+
rawId: string;
73+
}>;
74+
}
75+
76+
/**
77+
* Instantiates the getAccountInfo endpoint settings.
78+
*
79+
* @internal
80+
*/
81+
export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('v1', '/accounts:lookup', 'POST')
82+
// Set request validator.
83+
.setRequestValidator((request: GetAccountInfoRequest) => {
84+
if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) {
85+
throw new FirebaseAuthError(
86+
AuthClientErrorCode.INTERNAL_ERROR,
87+
'INTERNAL ASSERT FAILED: Server request is missing user identifier'
88+
);
89+
}
90+
})
91+
// Set response validator.
92+
.setResponseValidator((response: any) => {
93+
if (!response.users || !response.users.length) {
94+
throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND);
95+
}
96+
});
97+
98+
/**
99+
* Instantiates the revokeRefreshTokens endpoint settings for updating existing accounts.
100+
*
101+
* @internal
102+
* @link https://github.com/firebase/firebase-admin-node/blob/9955bca47249301aa970679ae99fe01d54adf6a8/src/auth/auth-api-request.ts#L746
103+
*/
104+
export const FIREBASE_AUTH_REVOKE_REFRESH_TOKENS = new ApiSettings('v1', '/accounts:update', 'POST')
105+
// Set request validator.
106+
.setRequestValidator((request: any) => {
107+
// localId is a required parameter.
108+
if (typeof request.localId === 'undefined') {
109+
throw new FirebaseAuthError(
110+
AuthClientErrorCode.INTERNAL_ERROR,
111+
'INTERNAL ASSERT FAILED: Server request is missing user identifier'
112+
);
113+
}
114+
// validSince should be a number.
115+
if (typeof request.validSince !== 'undefined' && !isNumber(request.validSince)) {
116+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME);
117+
}
118+
})
119+
// Set response validator.
120+
.setResponseValidator((response: any) => {
121+
// If the localId is not returned, then the request failed.
122+
if (!response.localId) {
123+
throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND);
124+
}
125+
});
126+
127+
/**
128+
* Instantiates the setCustomUserClaims endpoint settings for updating existing accounts.
129+
*
130+
* @internal
131+
* @link https://github.com/firebase/firebase-admin-node/blob/9955bca47249301aa970679ae99fe01d54adf6a8/src/auth/auth-api-request.ts#L746
132+
*/
133+
export const FIREBASE_AUTH_SET_CUSTOM_USER_CLAIMS = new ApiSettings('v1', '/accounts:update', 'POST')
134+
// Set request validator.
135+
.setRequestValidator((request: any) => {
136+
// localId is a required parameter.
137+
if (typeof request.localId === 'undefined') {
138+
throw new FirebaseAuthError(
139+
AuthClientErrorCode.INTERNAL_ERROR,
140+
'INTERNAL ASSERT FAILED: Server request is missing user identifier'
141+
);
142+
}
143+
// customAttributes should be stringified JSON with no blacklisted claims.
144+
// The payload should not exceed 1KB.
145+
if (typeof request.customAttributes !== 'undefined') {
146+
let developerClaims: object;
147+
try {
148+
developerClaims = JSON.parse(request.customAttributes);
149+
} catch (error) {
150+
if (error instanceof Error) {
151+
// JSON parsing error. This should never happen as we stringify the claims internally.
152+
// However, we still need to check since setAccountInfo via edit requests could pass
153+
// this field.
154+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CLAIMS, error.message);
155+
}
156+
throw error;
157+
}
158+
const invalidClaims: string[] = [];
159+
// Check for any invalid claims.
160+
RESERVED_CLAIMS.forEach(blacklistedClaim => {
161+
if (Object.prototype.hasOwnProperty.call(developerClaims, blacklistedClaim)) {
162+
invalidClaims.push(blacklistedClaim);
163+
}
164+
});
165+
// Throw an error if an invalid claim is detected.
166+
if (invalidClaims.length > 0) {
167+
throw new FirebaseAuthError(
168+
AuthClientErrorCode.FORBIDDEN_CLAIM,
169+
invalidClaims.length > 1
170+
? `Developer claims "${invalidClaims.join('", "')}" are reserved and cannot be specified.`
171+
: `Developer claim "${invalidClaims[0]}" is reserved and cannot be specified.`
172+
);
173+
}
174+
// Check claims payload does not exceed maxmimum size.
175+
if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) {
176+
throw new FirebaseAuthError(
177+
AuthClientErrorCode.CLAIMS_TOO_LARGE,
178+
`Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`
179+
);
180+
}
181+
}
182+
})
183+
// Set response validator.
184+
.setResponseValidator((response: any) => {
185+
// If the localId is not returned, then the request failed.
186+
if (!response.localId) {
187+
throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND);
188+
}
189+
});
190+
42191
export class AuthApiClient extends BaseClient {
43192
/**
44193
* Creates a new Firebase session cookie with the specified duration that can be used for
@@ -47,7 +196,7 @@ export class AuthApiClient extends BaseClient {
47196
*
48197
* @param idToken - The Firebase ID token to exchange for a session cookie.
49198
* @param expiresIn - The session cookie duration in milliseconds.
50-
* @param - An optional parameter specifying the environment in which the function is running.
199+
* @param env - An optional parameter specifying the environment in which the function is running.
51200
* If the function is running in an emulator environment, this should be set to `EmulatorEnv`.
52201
* If not specified, the function will assume it is running in a production environment.
53202
*
@@ -62,4 +211,90 @@ export class AuthApiClient extends BaseClient {
62211
const res = await this.fetch<{ sessionCookie: string }>(FIREBASE_AUTH_CREATE_SESSION_COOKIE, request, env);
63212
return res.sessionCookie;
64213
}
214+
215+
/**
216+
* Looks up a user by uid.
217+
*
218+
* @param uid - The uid of the user to lookup.
219+
* @param env - An optional parameter specifying the environment in which the function is running.
220+
* If the function is running in an emulator environment, this should be set to `EmulatorEnv`.
221+
* If not specified, the function will assume it is running in a production environment.
222+
* @returns A promise that resolves with the user information.
223+
*/
224+
public async getAccountInfoByUid(uid: string, env?: EmulatorEnv): Promise<UserRecord> {
225+
if (!isUid(uid)) {
226+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID);
227+
}
228+
229+
const request = {
230+
localId: [uid],
231+
};
232+
const res = await this.fetch<object>(FIREBASE_AUTH_GET_ACCOUNT_INFO, request, env);
233+
// Returns the user record populated with server response.
234+
return new UserRecord((res as any).users[0]);
235+
}
236+
237+
/**
238+
* Revokes all refresh tokens for the specified user identified by the uid provided.
239+
* In addition to revoking all refresh tokens for a user, all ID tokens issued
240+
* before revocation will also be revoked on the Auth backend. Any request with an
241+
* ID token generated before revocation will be rejected with a token expired error.
242+
* Note that due to the fact that the timestamp is stored in seconds, any tokens minted in
243+
* the same second as the revocation will still be valid. If there is a chance that a token
244+
* was minted in the last second, delay for 1 second before revoking.
245+
*
246+
* @param uid - The user whose tokens are to be revoked.
247+
* @param env - An optional parameter specifying the environment in which the function is running.
248+
* If the function is running in an emulator environment, this should be set to `EmulatorEnv`.
249+
* If not specified, the function will assume it is running in a production environment.
250+
* @returns A promise that resolves when the operation completes
251+
* successfully with the user id of the corresponding user.
252+
*/
253+
public async revokeRefreshTokens(uid: string, env?: EmulatorEnv): Promise<string> {
254+
// Validate user UID.
255+
if (!isUid(uid)) {
256+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID);
257+
}
258+
const request: any = {
259+
localId: uid,
260+
// validSince is in UTC seconds.
261+
validSince: Math.floor(new Date().getTime() / 1000),
262+
};
263+
const res = await this.fetch<{ localId: string }>(FIREBASE_AUTH_REVOKE_REFRESH_TOKENS, request, env);
264+
return res.localId;
265+
}
266+
267+
/**
268+
* Sets additional developer claims on an existing user identified by provided UID.
269+
*
270+
* @param uid - The user to edit.
271+
* @param customUserClaims - The developer claims to set.
272+
* @param env - An optional parameter specifying the environment in which the function is running.
273+
* If the function is running in an emulator environment, this should be set to `EmulatorEnv`.
274+
* If not specified, the function will assume it is running in a production environment.
275+
* @returns A promise that resolves when the operation completes
276+
* with the user id that was edited.
277+
*/
278+
public async setCustomUserClaims(uid: string, customUserClaims: object | null, env?: EmulatorEnv): Promise<string> {
279+
// Validate user UID.
280+
if (!isUid(uid)) {
281+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID);
282+
} else if (!isObject(customUserClaims)) {
283+
throw new FirebaseAuthError(
284+
AuthClientErrorCode.INVALID_ARGUMENT,
285+
'CustomUserClaims argument must be an object or null.'
286+
);
287+
}
288+
// Delete operation. Replace null with an empty object.
289+
if (customUserClaims === null) {
290+
customUserClaims = {};
291+
}
292+
// Construct custom user attribute editting request.
293+
const request: any = {
294+
localId: uid,
295+
customAttributes: JSON.stringify(customUserClaims),
296+
};
297+
const res = await this.fetch<{ localId: string }>(FIREBASE_AUTH_SET_CUSTOM_USER_CLAIMS, request, env);
298+
return res.localId;
299+
}
65300
}

0 commit comments

Comments
 (0)