Skip to content

Commit

Permalink
Implement Passkey unenrollment (#7768)
Browse files Browse the repository at this point in the history
  • Loading branch information
renkelvin committed Apr 9, 2024
1 parent 97931f3 commit 015d8ad
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 6 deletions.
10 changes: 10 additions & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@ export interface ParsedToken {
'sub'?: string;
}

// @public
export interface PasskeyInfo {
readonly credentialId: string;
readonly name?: string;
}

// @public
export interface PasswordPolicy {
readonly allowedNonAlphanumericCharacters: string;
Expand Down Expand Up @@ -839,6 +845,9 @@ export class TwitterAuthProvider extends BaseOAuthProvider {
static readonly TWITTER_SIGN_IN_METHOD: 'twitter.com';
}

// @public
export function unenrollPasskey(user: User, credentialId: string): Promise<void>;

// @public
export function unlink(user: User, providerId: string): Promise<User>;

Expand Down Expand Up @@ -869,6 +878,7 @@ export function useDeviceLanguage(auth: Auth): void;
export interface User extends UserInfo {
delete(): Promise<void>;
readonly emailVerified: boolean;
readonly enrolledPasskeys: PasskeyInfo[];
getIdToken(forceRefresh?: boolean): Promise<string>;
getIdTokenResult(forceRefresh?: boolean): Promise<IdTokenResult>;
readonly isAnonymous: boolean;
Expand Down
26 changes: 26 additions & 0 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Firebase Authentication
| [reauthenticateWithRedirect(user, provider, resolver)](./auth.md#reauthenticatewithredirect) | Reauthenticates the current user with the specified [OAuthProvider](./auth.oauthprovider.md#oauthprovider_class) using a full-page redirect flow. |
| [reload(user)](./auth.md#reload) | Reloads user account data, if signed in. |
| [sendEmailVerification(user, actionCodeSettings)](./auth.md#sendemailverification) | Sends a verification email to a user. |
| [unenrollPasskey(user, credentialId)](./auth.md#unenrollpasskey) | Unenrolls the passkey corresponding to the specified credentialId. |
| [unlink(user, providerId)](./auth.md#unlink) | Unlinks a provider from a user account. |
| [updateEmail(user, newEmail)](./auth.md#updateemail) | Updates the user's email address. |
| [updatePassword(user, newPassword)](./auth.md#updatepassword) | Updates the user's password. |
Expand Down Expand Up @@ -127,6 +128,7 @@ Firebase Authentication
| [MultiFactorUser](./auth.multifactoruser.md#multifactoruser_interface) | An interface that defines the multi-factor related properties and operations pertaining to a [User](./auth.user.md#user_interface)<!-- -->. |
| [OAuthCredentialOptions](./auth.oauthcredentialoptions.md#oauthcredentialoptions_interface) | Defines the options for initializing an [OAuthCredential](./auth.oauthcredential.md#oauthcredential_class)<!-- -->. |
| [ParsedToken](./auth.parsedtoken.md#parsedtoken_interface) | Interface representing a parsed ID token. |
| [PasskeyInfo](./auth.passkeyinfo.md#passkeyinfo_interface) | Represents information about a passkey. |
| [PasswordPolicy](./auth.passwordpolicy.md#passwordpolicy_interface) | A structure specifying password policy requirements. |
| [PasswordValidationStatus](./auth.passwordvalidationstatus.md#passwordvalidationstatus_interface) | A structure indicating which password policy requirements were met or violated and what the requirements are. |
| [Persistence](./auth.persistence.md#persistence_interface) | An interface covering the possible persistence mechanism types. |
Expand Down Expand Up @@ -1672,6 +1674,30 @@ await applyActionCode(auth, code);
```

### unlink(user, providerId) {:#unlink_f289a14}
## unenrollPasskey()

Unenrolls the passkey corresponding to the specified credentialId.

<b>Signature:</b>

```typescript
export declare function unenrollPasskey(user: User, credentialId: string): Promise<void>;
```

### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| user | [User](./auth.user.md#user_interface) | The user to unenroll the passkey for. |
| credentialId | string | The ID of the passkey to unenroll. |

<b>Returns:</b>

Promise&lt;void&gt;

A promise that resolves when the passkey is successfully unenrolled.

## unlink()

Unlinks a provider from a user account.

Expand Down
46 changes: 46 additions & 0 deletions docs-devsite/auth.passkeyinfo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# PasskeyInfo interface
Represents information about a passkey.

<b>Signature:</b>

```typescript
export interface PasskeyInfo
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [credentialId](./auth.passkeyinfo.md#passkeyinfocredentialid) | string | The credential ID of the passkey. |
| [name](./auth.passkeyinfo.md#passkeyinfoname) | string | The name associated with the passkey. |

## PasskeyInfo.credentialId

The credential ID of the passkey.

<b>Signature:</b>

```typescript
readonly credentialId: string;
```

## PasskeyInfo.name

The name associated with the passkey.

<b>Signature:</b>

```typescript
readonly name?: string;
```
13 changes: 12 additions & 1 deletion docs-devsite/auth.user.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export interface User extends UserInfo
| Property | Type | Description |
| --- | --- | --- |
| [emailVerified](./auth.user.md#useremailverified) | boolean | Whether the email has been verified with [sendEmailVerification()](./auth.md#sendemailverification_6a885d6) and [applyActionCode()](./auth.md#applyactioncode_d2ae15a)<!-- -->. |
| [emailVerified](./auth.user.md#useremailverified) | boolean | Whether the email has been verified with [sendEmailVerification()](./auth.md#sendemailverification) and [applyActionCode()](./auth.md#applyactioncode)<!-- -->. |
| [enrolledPasskeys](./auth.user.md#userenrolledpasskeys) | [PasskeyInfo](./auth.passkeyinfo.md#passkeyinfo_interface)<!-- -->\[\] | An array of PasskeyInfo objects representing the passkeys that the user has enrolled. |
| [isAnonymous](./auth.user.md#userisanonymous) | boolean | Whether the user is authenticated using the [ProviderId](./auth.md#providerid)<!-- -->.ANONYMOUS provider. |
| [metadata](./auth.user.md#usermetadata) | [UserMetadata](./auth.usermetadata.md#usermetadata_interface) | Additional metadata around user creation and sign-in times. |
| [providerData](./auth.user.md#userproviderdata) | [UserInfo](./auth.userinfo.md#userinfo_interface)<!-- -->\[\] | Additional per provider such as displayName and profile information. |
Expand All @@ -50,6 +51,16 @@ Whether the email has been verified with [sendEmailVerification()](./auth.md#sen
readonly emailVerified: boolean;
```
## User.enrolledPasskeys
An array of PasskeyInfo objects representing the passkeys that the user has enrolled.
<b>Signature:</b>
```typescript
readonly enrolledPasskeys: PasskeyInfo[];
```
## User.isAnonymous
Whether the user is authenticated using the [ProviderId](./auth.md#providerid)<!-- -->.ANONYMOUS provider.
Expand Down
3 changes: 3 additions & 0 deletions packages/auth-compat/src/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export class User implements compat.User, Compat<exp.User> {
get providerData(): Array<compat.UserInfo | null> {
return this._delegate.providerData;
}
get enrolledPasskeys(): compat.PasskeyInfo[] {
return this._delegate.enrolledPasskeys;
}
get refreshToken(): string {
return this._delegate.refreshToken;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/auth-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface User extends UserInfo {
multiFactor: MultiFactorUser;
phoneNumber: string | null;
providerData: (UserInfo | null)[];
enrolledPasskeys: PasskeyInfo[];
reauthenticateAndRetrieveDataWithCredential(
credential: AuthCredential
): Promise<UserCredential>;
Expand Down Expand Up @@ -469,6 +470,11 @@ export class FirebaseAuth {
verifyPasswordResetCode(code: string): Promise<string>;
}

export interface PasskeyInfo {
readonly credentialId: string;
readonly name?: string;
}

declare module '@firebase/app-types' {
interface FirebaseNamespace {
auth?: {
Expand Down
7 changes: 7 additions & 0 deletions packages/auth/demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,13 @@
id="enroll-passkey">
Enroll Passkey
</button>
<button class="btn btn-block btn-primary" id="get-enrolled-passkey">
Get Enrolled Passkeys
</button>
<input type="test" name="unenroll-passkey-credential-id" id="unenroll-passkey-credential-id" class="form-control" placeholder="Passkey Credential ID" />
<button class="btn btn-block btn-primary" id="unenroll-passkey">
Unenroll Passkey
</button>
</form>

<!-- Linking/Unlinking -->
Expand Down
16 changes: 15 additions & 1 deletion packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ import {
validatePassword,
revokeAccessToken,
enrollPasskey,
signInWithPasskey
signInWithPasskey,
unenrollPasskey
} from '@firebase/auth';

import { config } from './config';
Expand Down Expand Up @@ -535,6 +536,17 @@ function onEnrollPasskey() {
);
}

function onGetEnrolledPasskeys() {
console.log('Getting enrolled passkeys');
const passkeys = activeUser().enrolledPasskeys;
console.log(passkeys);
}

function onUnenrollPasskey() {
const credId = $('#unenroll-passkey-credential-id').val();
unenrollPasskey(activeUser(), credId);
}

function onSignInWithPasskey() {
const name = $('#signin-passkey-name').val();
signInWithPasskey(auth, name).then(onAuthSuccess, onAuthError);
Expand Down Expand Up @@ -2323,6 +2335,8 @@ function initApp() {

$('#get-provider-data').click(onGetProviderData);
$('#enroll-passkey').click(onEnrollPasskey);
$('#get-enrolled-passkey').click(onGetEnrolledPasskeys);
$('#unenroll-passkey').click(onUnenrollPasskey);
$('#link-with-email-and-password').click(onLinkWithEmailAndPassword);
$('#link-with-generic-idp-credential').click(onLinkWithGenericIdPCredential);
$('#unlink-provider').click(onUnlinkProvider);
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/api/account_management/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import { Endpoint, HttpMethod, _performApiRequest } from '../index';
import { MfaEnrollment } from './mfa';
import { PasskeyInfo } from './passkey';
import { Auth } from '../../model/public_types';

export interface DeleteAccountRequest {
Expand Down Expand Up @@ -76,6 +77,7 @@ export interface APIUserInfo {
passwordHash?: string;
providerUserInfo?: ProviderUserInfo[];
mfaInfo?: MfaEnrollment[];
passkeyInfo?: PasskeyInfo[];
}

export interface GetAccountInfoRequest {
Expand Down
24 changes: 24 additions & 0 deletions packages/auth/src/api/account_management/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export function publicKeyCredentialToJSON(
return result as PublicKeyCredentialJSON;
}

export interface PasskeyInfo {
credentialId: string;
name?: string;
}

// Enrollment types.
export interface StartPasskeyEnrollmentRequest {
idToken?: string;
Expand Down Expand Up @@ -198,3 +203,22 @@ export async function finalizePasskeySignIn(
_addTidIfNecessary(auth, request)
);
}

export interface PasskeyUnenrollRequest {
idToken?: string;
deletePasskey: string[];
}

export interface PasskeyUnenrollResponse {}

export async function passkeyUnenroll(
auth: Auth,
request: PasskeyUnenrollRequest
): Promise<PasskeyUnenrollResponse> {
return _performApiRequest<PasskeyUnenrollRequest, PasskeyUnenrollResponse>(
auth,
HttpMethod.POST,
Endpoint.SET_ACCOUNT_INFO,
request
);
}
6 changes: 5 additions & 1 deletion packages/auth/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,11 @@ export {
sendEmailVerification,
verifyBeforeUpdateEmail
} from './strategies/email';
export { signInWithPasskey, enrollPasskey } from './strategies/passkey';
export {
signInWithPasskey,
enrollPasskey,
unenrollPasskey
} from './strategies/passkey';

// core
export { ActionCodeURL, parseActionCodeURL } from './action_code_url';
Expand Down
26 changes: 25 additions & 1 deletion packages/auth/src/core/strategies/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import {
finalizePasskeySignIn,
FinalizePasskeySignInRequest,
FinalizePasskeySignInResponse,
publicKeyCredentialToJSON
publicKeyCredentialToJSON,
PasskeyUnenrollRequest,
passkeyUnenroll
} from '../../api/account_management/passkey';
import { UserInternal } from '../../model/user';
import { _castAuth } from '../auth/auth_impl';
Expand Down Expand Up @@ -164,6 +166,28 @@ export async function enrollPasskey(
}
}

/**
* Unenrolls the passkey corresponding to the specified credentialId.
* @param user - The user to unenroll the passkey for.
* @param credentialId - The ID of the passkey to unenroll.
* @returns A promise that resolves when the passkey is successfully unenrolled.
* @public
*/
export async function unenrollPasskey(
user: User,
credentialId: string
): Promise<void> {
const userInternal = getModularInstance(user) as UserInternal;
const authInternal = _castAuth(userInternal.auth);

const idToken = await userInternal.getIdToken();
const request: PasskeyUnenrollRequest = {
idToken,
deletePasskey: [credentialId]
};
await passkeyUnenroll(authInternal, request);
}

// Converts an array of credential IDs of `excludeCredentials` field to an array of `PublicKeyCredentialDescriptor` objects.
function convertExcludeCredentials(
options:
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/core/user/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export async function _reloadWithoutSaving(user: UserInternal): Promise<void> {
tenantId: coreAccount.tenantId || null,
providerData,
metadata: new UserMetadata(coreAccount.createdAt, coreAccount.lastLoginAt),
isAnonymous
isAnonymous,
enrolledPasskeys: coreAccount.passkeyInfo || []
};

Object.assign(user, updates);
Expand Down
9 changes: 8 additions & 1 deletion packages/auth/src/core/user/user_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

import { IdTokenResult, UserInfo } from '../../model/public_types';
import { IdTokenResult, UserInfo, PasskeyInfo } from '../../model/public_types';
import { NextFn } from '@firebase/util';
import {
APIUserInfo,
Expand Down Expand Up @@ -70,6 +70,7 @@ export class UserImpl implements UserInternal {
tenantId: string | null;
readonly metadata: UserMetadata;
providerData: MutableUserInfo[];
enrolledPasskeys: PasskeyInfo[];

// Optional fields from UserInfo
displayName: string | null;
Expand All @@ -93,6 +94,9 @@ export class UserImpl implements UserInternal {
this.isAnonymous = opt.isAnonymous || false;
this.tenantId = opt.tenantId || null;
this.providerData = opt.providerData ? [...opt.providerData] : [];
this.enrolledPasskeys = opt.enrolledPasskeys
? [...opt.enrolledPasskeys]
: [];
this.metadata = new UserMetadata(
opt.createdAt || undefined,
opt.lastLoginAt || undefined
Expand Down Expand Up @@ -139,6 +143,9 @@ export class UserImpl implements UserInternal {
this.isAnonymous = user.isAnonymous;
this.tenantId = user.tenantId;
this.providerData = user.providerData.map(userInfo => ({ ...userInfo }));
this.enrolledPasskeys = user.enrolledPasskeys.map(passkeyInfo => ({
...passkeyInfo
}));
this.metadata._copy(user.metadata);
this.stsTokenManager._assign(user.stsTokenManager);
}
Expand Down

0 comments on commit 015d8ad

Please sign in to comment.