Skip to content
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

user record changes for getAccountInfo() #2341

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
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,5 @@ export {
UserInfo,
UserMetadata,
UserRecord,
PasskeyInfo,
} from './user-record';
76 changes: 76 additions & 0 deletions src/auth/user-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export interface TotpInfoResponse {
[key: string]: unknown;
}

export interface PasskeyInfoResponse {
name?: string;
credentialId?: string;
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
displayName?: string;
}

export interface ProviderUserInfoResponse {
rawId: string;
displayName?: string;
Expand All @@ -81,6 +87,7 @@ export interface GetAccountInfoUserResponse {
tenantId?: string;
providerUserInfo?: ProviderUserInfoResponse[];
mfaInfo?: MultiFactorInfoResponse[];
passkeyInfo?: PasskeyInfoResponse[];
createdAt?: string;
lastLoginAt?: string;
lastRefreshAt?: string;
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
}

/**
Expand Down Expand Up @@ -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.
Expand Down
86 changes: 85 additions & 1 deletion test/unit/auth/user-record.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as chaiAsPromised from 'chai-as-promised';

import { deepCopy } from '../../../src/utils/deep-copy';
import {
GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo,
GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo, PasskeyInfo,
} from '../../../src/auth/user-record';
import {
UserInfo, UserMetadata, UserRecord, MultiFactorSettings, MultiFactorInfo, PhoneMultiFactorInfo,
Expand Down Expand Up @@ -100,6 +100,18 @@ function getValidUserResponse(tenantId?: string): GetAccountInfoUserResponse {
phoneInfo: '+16505556789',
},
],
passkeyInfo: [
{
name: '[email protected]',
credentialId: 'credentialId1',
displayName: 'passkey1',
},
{
name: '[email protected]',
credentialId: 'credentialId2',
displayName: 'passkey2',
}
]
};
if (typeof tenantId !== 'undefined') {
response.tenantId = tenantId;
Expand Down Expand Up @@ -185,6 +197,18 @@ function getUserJSON(tenantId?: string): object {
},
],
},
passkeyInfo: [
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
{
name: '[email protected]',
credentialId: 'credentialId1',
displayName: 'passkey1',
},
{
name: '[email protected]',
credentialId: 'credentialId2',
displayName: 'passkey2',
}
]
};
}

Expand Down Expand Up @@ -663,6 +687,66 @@ describe('MultiFactorSettings', () => {
});
});

describe('PasskeyInfo', () => {
const passkeyInfoData = {
name: 'John Doe',
credentialId: 'credential123',
displayName: '[email protected]',
};
const passkeyInfo = new PasskeyInfo(passkeyInfoData);

describe('constructor', () => {
it('should create a PasskeyInfo object with valid data', () => {
expect(passkeyInfo).to.be.an.instanceOf(PasskeyInfo);
});

it('should throw when missing required fields', () => {
expect(() => {
return new PasskeyInfo(null as any);
}).to.throw('INTERNAL ASSERT FAILED: Invalid passkey info response');
});
});

describe('getters', () => {
it('should return the expected name', () => {
expect(passkeyInfo.name).to.equal(passkeyInfoData.name);
});

it('should throw when modifying readonly name property', () => {
expect(() => {
(passkeyInfo as any).name = 'Modified Name';
}).to.throw(Error);
});

it('should return the expected credentialId', () => {
expect(passkeyInfo.credentialId).to.equal(passkeyInfoData.credentialId);
});

it('should throw when modifying readonly credentialId property', () => {
expect(() => {
(passkeyInfo as any).credentialId = 'modifiedCredential';
}).to.throw(Error);
});

it('should return the expected displayName', () => {
expect(passkeyInfo.displayName).to.equal(passkeyInfoData.displayName);
});

it('should throw when modifying readonly displayName property', () => {
expect(() => {
(passkeyInfo as any).displayName = 'modifiedDisplayName';
}).to.throw(Error);
});
});

describe('toJSON', () => {
it('should return the expected JSON object', () => {
expect(passkeyInfo.toJSON()).to.deep.equal(passkeyInfoData);
});
});
});


describe('UserInfo', () => {
describe('constructor', () => {
it('should throw when an empty object is provided', () => {
Expand Down
Loading