Skip to content

user record changes for getAccountInfo() #2341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 3, 2024
9 changes: 9 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
@@ -365,6 +365,14 @@ export interface PasskeyConfigRequest {
expectedOrigins?: string[];
}

// @public
export class PasskeyInfo {
readonly credentialId: string;
readonly displayName?: string;
readonly name: string;
toJSON(): object;
}

// @public
export interface PasswordPolicyConfig {
constraints?: CustomStrengthOptionsConfig;
@@ -664,6 +672,7 @@ export class UserRecord {
readonly emailVerified: boolean;
readonly metadata: UserMetadata;
readonly multiFactor?: MultiFactorSettings;
readonly passkeyInfo?: PasskeyInfo[];
readonly passwordHash?: string;
readonly passwordSalt?: string;
readonly phoneNumber?: string;
14 changes: 7 additions & 7 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
@@ -1612,9 +1612,9 @@ export abstract class AbstractAuthRequestHandler {
public getEmailActionLink(
requestType: string, email: string,
actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise<string> {
let request = {
requestType,
email,
let request = {
requestType,
email,
returnOobLink: true,
...(typeof newEmail !== 'undefined') && { newEmail },
};
@@ -2297,20 +2297,20 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
}

public getPasskeyConfig(tenantId?: string): Promise<PasskeyConfigServerResponse> {
return this.invokeRequestHandler(this.authResourceUrlBuilder,
return this.invokeRequestHandler(this.authResourceUrlBuilder,
tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {})
.then((response: any) => {
return response as PasskeyConfigServerResponse;
});
}

public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string,
options?: PasskeyConfigRequest, rpId?: string): Promise<PasskeyConfigServerResponse> {
options?: PasskeyConfigRequest): Promise<PasskeyConfigServerResponse> {
try {
const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId);
const request = PasskeyConfig.buildServerRequest(isCreateRequest, options);
const updateMask = utils.generateUpdateMask(request);
return this.invokeRequestHandler(
this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG,
this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG,
request, { updateMask: updateMask.join(',') })
.then((response: any) => {
return response as PasskeyConfigServerResponse;
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -172,4 +172,5 @@ export {
UserInfo,
UserMetadata,
UserRecord,
PasskeyInfo,
} from './user-record';
28 changes: 13 additions & 15 deletions src/auth/passkey-config-manager.ts
Original file line number Diff line number Diff line change
@@ -17,22 +17,22 @@ import { App } from '../app';
import {
AuthRequestHandler,
} from './auth-api-request';
import {
PasskeyConfig,
PasskeyConfigClientRequest,
PasskeyConfigRequest,
PasskeyConfigServerResponse
import {
PasskeyConfig,
PasskeyConfigClientRequest,
PasskeyConfigRequest,
PasskeyConfigServerResponse
} from './passkey-config';

/**
* Manages Passkey configuration for a Firebase app.
*/
export class PasskeyConfigManager {
private readonly authRequestHandler: AuthRequestHandler;

/**
* Initializes a PasskeyConfigManager instance for a specified FirebaseApp.
*
*
* @param app - The Firebase app associated with this PasskeyConfigManager instance.
*
* @constructor
@@ -43,8 +43,8 @@ export class PasskeyConfigManager {
}

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

/**
* Creates a new passkey configuration.
*
* @param rpId - The relying party ID.
*
* @param passkeyConfigRequest - Configuration details for the passkey.
* @param tenantId - (optional) The tenant ID for which the passkey config is created.
* @returns A promise fulfilled with the newly created passkey configuration.
*/
public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest,
tenantId?: string): Promise<PasskeyConfig> {
return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId)
public createPasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise<PasskeyConfig> {
return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest)
.then((response: PasskeyConfigClientRequest) => {
return new PasskeyConfig(response);
});
}

/**
* Updates an existing passkey configuration.
*
*
* @param passkeyConfigRequest - Updated configuration details for the passkey.
* @param tenantId - (optional) The tenant ID for which the passkey config is updated.
* @returns A promise fulfilled with the updated passkey configuration.
31 changes: 16 additions & 15 deletions src/auth/passkey-config.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import { deepCopy } from '../utils/deep-copy';
* Interface representing the properties to update in a passkey config.
*/
export interface PasskeyConfigRequest {
rpId?: string;
/**
* An array of website or app origins. Only challenges signed
* from these origins will be allowed for signing in with passkeys.
@@ -73,28 +74,29 @@ export class PasskeyConfig {
*
* @internal
*/
private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): void {
private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest): void {
// Validation for creating a new PasskeyConfig.
if (isCreateRequest && !validator.isNonEmptyString(rpId)) {
if (isCreateRequest && !validator.isNonEmptyString(passkeyConfigRequest?.rpId)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
"'rpId' must be a non-empty string.",
);
}
// Validation for updating an existing PasskeyConfig.
if (!isCreateRequest && typeof rpId !== 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
"'rpId' cannot be changed once created.",
);
}
// // Validation for updating an existing PasskeyConfig.
// if (!isCreateRequest && typeof rpId !== 'undefined') {
// throw new FirebaseAuthError(
// AuthClientErrorCode.INVALID_ARGUMENT,
// "'rpId' cannot be changed once created.",
// );
// }
if (!validator.isNonNullObject(passkeyConfigRequest)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
"'passkeyConfigRequest' must not be null.",
);
}
const validKeys = {
rpId: true,
expectedOrigins: true,
};
// Check for unsupported top-level attributes.
@@ -126,18 +128,17 @@ export class PasskeyConfig {
* Build a server request for a Passkey Config object.
* @param isCreateRequest - A boolean stating if it's a create request.
* @param passkeyConfigRequest - Passkey config to be updated.
* @param rpId - (optional) Relying party ID for the request if it's a create request.
* @returns The equivalent server request.
* @throws FirebaseAuthError - If validation fails.
*
* @internal
*/
public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest,
rpId?: string): PasskeyConfigClientRequest {
PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId);
public static buildServerRequest(isCreateRequest: boolean,
passkeyConfigRequest?: PasskeyConfigRequest): PasskeyConfigClientRequest {
PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest);
const request: PasskeyConfigClientRequest = {};
if (isCreateRequest && typeof rpId !== 'undefined') {
request.rpId = rpId;
if (typeof passkeyConfigRequest?.rpId !== 'undefined') {
request.rpId = passkeyConfigRequest.rpId;
}
if (typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') {
request.expectedOrigins = passkeyConfigRequest.expectedOrigins;
76 changes: 76 additions & 0 deletions src/auth/user-record.ts
Original file line number Diff line number Diff line change
@@ -56,6 +56,12 @@ export interface TotpInfoResponse {
[key: string]: unknown;
}

export interface PasskeyInfoResponse {
name: string;
credentialId: string;
displayName?: string;
}

export interface ProviderUserInfoResponse {
rawId: string;
displayName?: string;
@@ -81,6 +87,7 @@ export interface GetAccountInfoUserResponse {
tenantId?: string;
providerUserInfo?: ProviderUserInfoResponse[];
mfaInfo?: MultiFactorInfoResponse[];
passkeyInfo?: PasskeyInfoResponse[];
createdAt?: string;
lastLoginAt?: string;
lastRefreshAt?: string;
@@ -357,6 +364,55 @@ export class MultiFactorSettings {
}
}

/**
* Interface representing a user-enrolled passkey.
*/
export class PasskeyInfo {
/**
* The name of the user.
*/
public readonly name: string;
/**
* Identifier for the registered credential.
*/
public readonly credentialId: string;
/**
* The human-readable name of the user, intended for display.
*/
public readonly displayName?: string;

/**
* Initializes the PasskeyInfo object using the server side response.
*
* @param response - The server side response.
* @constructor
* @internal
*/
constructor(response: PasskeyInfoResponse) {
if (!isNonNullObject(response)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid passkey info response');
}
utils.addReadonlyGetter(this, 'name', response.name);
utils.addReadonlyGetter(this, 'credentialId', response.credentialId);
utils.addReadonlyGetter(this, 'displayName', response.displayName);
}

/**
* Returns a JSON-serializable representation of this passkey info object.
*
* @returns A JSON-serializable representation of this passkey info object.
*/
public toJSON(): object {
return {
name: this.name,
credentialId: this.credentialId,
displayName: this.displayName,
};
}
}

/**
* Represents a user's metadata.
*/
@@ -582,6 +638,11 @@ export class UserRecord {
*/
public readonly multiFactor?: MultiFactorSettings;

/**
* Passkey-related properties for the current user, if available.
*/
public readonly passkeyInfo?: PasskeyInfo[];

/**
* @param response - The server side response returned from the getAccountInfo
* endpoint.
@@ -637,6 +698,15 @@ export class UserRecord {
if (multiFactor.enrolledFactors.length > 0) {
utils.addReadonlyGetter(this, 'multiFactor', multiFactor);
}
if (response.passkeyInfo) {
const passkeys: PasskeyInfo[] = [];
response.passkeyInfo.forEach((passkey) => {
passkeys.push(new PasskeyInfo(passkey));
});
if (passkeys.length > 0) {
utils.addReadonlyGetter(this, 'passkeyInfo', passkeys);
}
}
}

/**
@@ -664,6 +734,12 @@ export class UserRecord {
if (this.multiFactor) {
json.multiFactor = this.multiFactor.toJSON();
}
if (this.passkeyInfo) {
json.passkeyInfo = [];
this.passkeyInfo.forEach((passkey) => {
json.passkeyInfo.push(passkey.toJSON());
})
}
json.providerData = [];
for (const entry of this.providerData) {
// Convert each provider data to json.
21 changes: 11 additions & 10 deletions test/unit/auth/passkey-config-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -124,6 +124,7 @@ describe('PasskeyConfigManager', () => {
const rpId = 'project-id.firebaseapp.com';
const expectedOrigins: string[] = ['app1', 'example.com']
const passkeyConfigRequest: PasskeyConfigRequest = {
rpId: rpId,
expectedOrigins: expectedOrigins ,
};
const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE);
@@ -140,19 +141,19 @@ describe('PasskeyConfigManager', () => {
return (passkeyConfigManager as any).createPasskeyConfig(null as unknown as PasskeyConfigRequest)
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
});

it('should be rejected given an app which returns null access tokens', () => {
return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
.should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential');
});

it('should be rejected given an app which returns invalid access tokens', () => {
return malformedAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
return malformedAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
.should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential');
});

it('should be rejected given an app which fails to generate access tokens', () => {
return rejectedPromiseAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
return rejectedPromiseAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
.should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential');
});

@@ -161,10 +162,10 @@ describe('PasskeyConfigManager', () => {
const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig')
.returns(Promise.resolve(GET_CONFIG_RESPONSE));
stubs.push(stub);
return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
return passkeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
.then((actualPasskeyConfig) => {
// Confirm underlying API called with expected parameters.
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId);
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest);
// Confirm expected Passkey Config object returned.
expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig);
});
@@ -175,12 +176,12 @@ describe('PasskeyConfigManager', () => {
const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig')
.returns(Promise.reject(expectedError));
stubs.push(stub);
return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest)
return passkeyConfigManager.createPasskeyConfig(passkeyConfigRequest)
.then(() => {
throw new Error('Unexpected success');
}, (error) => {
// Confirm underlying API called with expected parameters.
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId);
expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest);
// Confirm expected error returned.
expect(error).to.equal(expectedError);
});
Loading