Skip to content

Commit 041bf17

Browse files
user record changes for getAccountInfo() (#2341)
* user record changes for getAccountInfo() * lint and api-extractor fixes * Apply suggestions from code review Co-authored-by: Kevin Cheung <[email protected]> * remove `[key: string]: unknown;` field from `PasskeyInfoResponse` * add undefined displayName case * name and credentialId are not optional * add `rpId` to update --------- Co-authored-by: Kevin Cheung <[email protected]>
1 parent f9d35c3 commit 041bf17

9 files changed

+248
-64
lines changed

etc/firebase-admin.auth.api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,14 @@ export interface PasskeyConfigRequest {
365365
expectedOrigins?: string[];
366366
}
367367

368+
// @public
369+
export class PasskeyInfo {
370+
readonly credentialId: string;
371+
readonly displayName?: string;
372+
readonly name: string;
373+
toJSON(): object;
374+
}
375+
368376
// @public
369377
export interface PasswordPolicyConfig {
370378
constraints?: CustomStrengthOptionsConfig;
@@ -664,6 +672,7 @@ export class UserRecord {
664672
readonly emailVerified: boolean;
665673
readonly metadata: UserMetadata;
666674
readonly multiFactor?: MultiFactorSettings;
675+
readonly passkeyInfo?: PasskeyInfo[];
667676
readonly passwordHash?: string;
668677
readonly passwordSalt?: string;
669678
readonly phoneNumber?: string;

src/auth/auth-api-request.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,9 +1612,9 @@ export abstract class AbstractAuthRequestHandler {
16121612
public getEmailActionLink(
16131613
requestType: string, email: string,
16141614
actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise<string> {
1615-
let request = {
1616-
requestType,
1617-
email,
1615+
let request = {
1616+
requestType,
1617+
email,
16181618
returnOobLink: true,
16191619
...(typeof newEmail !== 'undefined') && { newEmail },
16201620
};
@@ -2297,20 +2297,20 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
22972297
}
22982298

22992299
public getPasskeyConfig(tenantId?: string): Promise<PasskeyConfigServerResponse> {
2300-
return this.invokeRequestHandler(this.authResourceUrlBuilder,
2300+
return this.invokeRequestHandler(this.authResourceUrlBuilder,
23012301
tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {})
23022302
.then((response: any) => {
23032303
return response as PasskeyConfigServerResponse;
23042304
});
23052305
}
23062306

23072307
public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string,
2308-
options?: PasskeyConfigRequest, rpId?: string): Promise<PasskeyConfigServerResponse> {
2308+
options?: PasskeyConfigRequest): Promise<PasskeyConfigServerResponse> {
23092309
try {
2310-
const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId);
2310+
const request = PasskeyConfig.buildServerRequest(isCreateRequest, options);
23112311
const updateMask = utils.generateUpdateMask(request);
23122312
return this.invokeRequestHandler(
2313-
this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG,
2313+
this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG,
23142314
request, { updateMask: updateMask.join(',') })
23152315
.then((response: any) => {
23162316
return response as PasskeyConfigServerResponse;

src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,5 @@ export {
172172
UserInfo,
173173
UserMetadata,
174174
UserRecord,
175+
PasskeyInfo,
175176
} from './user-record';

src/auth/passkey-config-manager.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,22 @@ import { App } from '../app';
1717
import {
1818
AuthRequestHandler,
1919
} from './auth-api-request';
20-
import {
21-
PasskeyConfig,
22-
PasskeyConfigClientRequest,
23-
PasskeyConfigRequest,
24-
PasskeyConfigServerResponse
20+
import {
21+
PasskeyConfig,
22+
PasskeyConfigClientRequest,
23+
PasskeyConfigRequest,
24+
PasskeyConfigServerResponse
2525
} from './passkey-config';
2626

2727
/**
2828
* Manages Passkey configuration for a Firebase app.
2929
*/
3030
export class PasskeyConfigManager {
3131
private readonly authRequestHandler: AuthRequestHandler;
32-
32+
3333
/**
3434
* Initializes a PasskeyConfigManager instance for a specified FirebaseApp.
35-
*
35+
*
3636
* @param app - The Firebase app associated with this PasskeyConfigManager instance.
3737
*
3838
* @constructor
@@ -43,8 +43,8 @@ export class PasskeyConfigManager {
4343
}
4444

4545
/**
46-
* Retrieves the Passkey configuration.
47-
*
46+
* Retrieves the Passkey Configuration.
47+
*
4848
* @param tenantId - (optional) The tenant ID if querying passkeys on a specific tenant.
4949
* @returns A promise fulfilled with the passkey configuration.
5050
*/
@@ -57,23 +57,21 @@ export class PasskeyConfigManager {
5757

5858
/**
5959
* Creates a new passkey configuration.
60-
*
61-
* @param rpId - The relying party ID.
60+
*
6261
* @param passkeyConfigRequest - Configuration details for the passkey.
6362
* @param tenantId - (optional) The tenant ID for which the passkey config is created.
6463
* @returns A promise fulfilled with the newly created passkey configuration.
6564
*/
66-
public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest,
67-
tenantId?: string): Promise<PasskeyConfig> {
68-
return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId)
65+
public createPasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise<PasskeyConfig> {
66+
return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest)
6967
.then((response: PasskeyConfigClientRequest) => {
7068
return new PasskeyConfig(response);
7169
});
7270
}
7371

7472
/**
7573
* Updates an existing passkey configuration.
76-
*
74+
*
7775
* @param passkeyConfigRequest - Updated configuration details for the passkey.
7876
* @param tenantId - (optional) The tenant ID for which the passkey config is updated.
7977
* @returns A promise fulfilled with the updated passkey configuration.

src/auth/passkey-config.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { deepCopy } from '../utils/deep-copy';
2121
* Interface representing the properties to update in a passkey config.
2222
*/
2323
export interface PasskeyConfigRequest {
24+
rpId?: string;
2425
/**
2526
* An array of website or app origins. Only challenges signed
2627
* from these origins will be allowed for signing in with passkeys.
@@ -73,28 +74,29 @@ export class PasskeyConfig {
7374
*
7475
* @internal
7576
*/
76-
private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): void {
77+
private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest): void {
7778
// Validation for creating a new PasskeyConfig.
78-
if (isCreateRequest && !validator.isNonEmptyString(rpId)) {
79+
if (isCreateRequest && !validator.isNonEmptyString(passkeyConfigRequest?.rpId)) {
7980
throw new FirebaseAuthError(
8081
AuthClientErrorCode.INVALID_ARGUMENT,
8182
"'rpId' must be a non-empty string.",
8283
);
8384
}
84-
// Validation for updating an existing PasskeyConfig.
85-
if (!isCreateRequest && typeof rpId !== 'undefined') {
86-
throw new FirebaseAuthError(
87-
AuthClientErrorCode.INVALID_ARGUMENT,
88-
"'rpId' cannot be changed once created.",
89-
);
90-
}
85+
// // Validation for updating an existing PasskeyConfig.
86+
// if (!isCreateRequest && typeof rpId !== 'undefined') {
87+
// throw new FirebaseAuthError(
88+
// AuthClientErrorCode.INVALID_ARGUMENT,
89+
// "'rpId' cannot be changed once created.",
90+
// );
91+
// }
9192
if (!validator.isNonNullObject(passkeyConfigRequest)) {
9293
throw new FirebaseAuthError(
9394
AuthClientErrorCode.INVALID_ARGUMENT,
9495
"'passkeyConfigRequest' must not be null.",
9596
);
9697
}
9798
const validKeys = {
99+
rpId: true,
98100
expectedOrigins: true,
99101
};
100102
// Check for unsupported top-level attributes.
@@ -126,18 +128,17 @@ export class PasskeyConfig {
126128
* Build a server request for a Passkey Config object.
127129
* @param isCreateRequest - A boolean stating if it's a create request.
128130
* @param passkeyConfigRequest - Passkey config to be updated.
129-
* @param rpId - (optional) Relying party ID for the request if it's a create request.
130131
* @returns The equivalent server request.
131132
* @throws FirebaseAuthError - If validation fails.
132133
*
133134
* @internal
134135
*/
135-
public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest,
136-
rpId?: string): PasskeyConfigClientRequest {
137-
PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId);
136+
public static buildServerRequest(isCreateRequest: boolean,
137+
passkeyConfigRequest?: PasskeyConfigRequest): PasskeyConfigClientRequest {
138+
PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest);
138139
const request: PasskeyConfigClientRequest = {};
139-
if (isCreateRequest && typeof rpId !== 'undefined') {
140-
request.rpId = rpId;
140+
if (typeof passkeyConfigRequest?.rpId !== 'undefined') {
141+
request.rpId = passkeyConfigRequest.rpId;
141142
}
142143
if (typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') {
143144
request.expectedOrigins = passkeyConfigRequest.expectedOrigins;

src/auth/user-record.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export interface TotpInfoResponse {
5656
[key: string]: unknown;
5757
}
5858

59+
export interface PasskeyInfoResponse {
60+
name: string;
61+
credentialId: string;
62+
displayName?: string;
63+
}
64+
5965
export interface ProviderUserInfoResponse {
6066
rawId: string;
6167
displayName?: string;
@@ -81,6 +87,7 @@ export interface GetAccountInfoUserResponse {
8187
tenantId?: string;
8288
providerUserInfo?: ProviderUserInfoResponse[];
8389
mfaInfo?: MultiFactorInfoResponse[];
90+
passkeyInfo?: PasskeyInfoResponse[];
8491
createdAt?: string;
8592
lastLoginAt?: string;
8693
lastRefreshAt?: string;
@@ -357,6 +364,55 @@ export class MultiFactorSettings {
357364
}
358365
}
359366

367+
/**
368+
* Interface representing a user-enrolled passkey.
369+
*/
370+
export class PasskeyInfo {
371+
/**
372+
* The name of the user.
373+
*/
374+
public readonly name: string;
375+
/**
376+
* Identifier for the registered credential.
377+
*/
378+
public readonly credentialId: string;
379+
/**
380+
* The human-readable name of the user, intended for display.
381+
*/
382+
public readonly displayName?: string;
383+
384+
/**
385+
* Initializes the PasskeyInfo object using the server side response.
386+
*
387+
* @param response - The server side response.
388+
* @constructor
389+
* @internal
390+
*/
391+
constructor(response: PasskeyInfoResponse) {
392+
if (!isNonNullObject(response)) {
393+
throw new FirebaseAuthError(
394+
AuthClientErrorCode.INTERNAL_ERROR,
395+
'INTERNAL ASSERT FAILED: Invalid passkey info response');
396+
}
397+
utils.addReadonlyGetter(this, 'name', response.name);
398+
utils.addReadonlyGetter(this, 'credentialId', response.credentialId);
399+
utils.addReadonlyGetter(this, 'displayName', response.displayName);
400+
}
401+
402+
/**
403+
* Returns a JSON-serializable representation of this passkey info object.
404+
*
405+
* @returns A JSON-serializable representation of this passkey info object.
406+
*/
407+
public toJSON(): object {
408+
return {
409+
name: this.name,
410+
credentialId: this.credentialId,
411+
displayName: this.displayName,
412+
};
413+
}
414+
}
415+
360416
/**
361417
* Represents a user's metadata.
362418
*/
@@ -582,6 +638,11 @@ export class UserRecord {
582638
*/
583639
public readonly multiFactor?: MultiFactorSettings;
584640

641+
/**
642+
* Passkey-related properties for the current user, if available.
643+
*/
644+
public readonly passkeyInfo?: PasskeyInfo[];
645+
585646
/**
586647
* @param response - The server side response returned from the getAccountInfo
587648
* endpoint.
@@ -637,6 +698,15 @@ export class UserRecord {
637698
if (multiFactor.enrolledFactors.length > 0) {
638699
utils.addReadonlyGetter(this, 'multiFactor', multiFactor);
639700
}
701+
if (response.passkeyInfo) {
702+
const passkeys: PasskeyInfo[] = [];
703+
response.passkeyInfo.forEach((passkey) => {
704+
passkeys.push(new PasskeyInfo(passkey));
705+
});
706+
if (passkeys.length > 0) {
707+
utils.addReadonlyGetter(this, 'passkeyInfo', passkeys);
708+
}
709+
}
640710
}
641711

642712
/**
@@ -664,6 +734,12 @@ export class UserRecord {
664734
if (this.multiFactor) {
665735
json.multiFactor = this.multiFactor.toJSON();
666736
}
737+
if (this.passkeyInfo) {
738+
json.passkeyInfo = [];
739+
this.passkeyInfo.forEach((passkey) => {
740+
json.passkeyInfo.push(passkey.toJSON());
741+
})
742+
}
667743
json.providerData = [];
668744
for (const entry of this.providerData) {
669745
// Convert each provider data to json.

test/unit/auth/passkey-config-manager.spec.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ describe('PasskeyConfigManager', () => {
124124
const rpId = 'project-id.firebaseapp.com';
125125
const expectedOrigins: string[] = ['app1', 'example.com']
126126
const passkeyConfigRequest: PasskeyConfigRequest = {
127+
rpId: rpId,
127128
expectedOrigins: expectedOrigins ,
128129
};
129130
const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE);
@@ -140,19 +141,19 @@ describe('PasskeyConfigManager', () => {
140141
return (passkeyConfigManager as any).createPasskeyConfig(null as unknown as PasskeyConfigRequest)
141142
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
142143
});
143-
144+
144145
it('should be rejected given an app which returns null access tokens', () => {
145-
return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
146+
return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
146147
.should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential');
147148
});
148-
149+
149150
it('should be rejected given an app which returns invalid access tokens', () => {
150-
return malformedAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
151+
return malformedAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
151152
.should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential');
152153
});
153-
154+
154155
it('should be rejected given an app which fails to generate access tokens', () => {
155-
return rejectedPromiseAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
156+
return rejectedPromiseAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
156157
.should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential');
157158
});
158159

@@ -161,10 +162,10 @@ describe('PasskeyConfigManager', () => {
161162
const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig')
162163
.returns(Promise.resolve(GET_CONFIG_RESPONSE));
163164
stubs.push(stub);
164-
return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
165+
return passkeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
165166
.then((actualPasskeyConfig) => {
166167
// Confirm underlying API called with expected parameters.
167-
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId);
168+
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest);
168169
// Confirm expected Passkey Config object returned.
169170
expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig);
170171
});
@@ -175,12 +176,12 @@ describe('PasskeyConfigManager', () => {
175176
const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig')
176177
.returns(Promise.reject(expectedError));
177178
stubs.push(stub);
178-
return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
179+
return passkeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
179180
.then(() => {
180181
throw new Error('Unexpected success');
181182
}, (error) => {
182183
// Confirm underlying API called with expected parameters.
183-
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId);
184+
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest);
184185
// Confirm expected error returned.
185186
expect(error).to.equal(expectedError);
186187
});

0 commit comments

Comments
 (0)