Skip to content

Commit

Permalink
feat(auth): adding associateWebAuthnCredential API
Browse files Browse the repository at this point in the history
  • Loading branch information
jjarvisp committed Oct 1, 2024
1 parent 1cbb174 commit 7b860ef
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export {
CodeDeliveryDetails,
UserAttributeKey,
VerifiableUserAttributeKey,
associateWebAuthnCredential,
} from './providers/cognito';

export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Amplify, fetchAuthSession } from '@aws-amplify/core';
import {
AuthAction,
assertTokenProviderConfig,
} from '@aws-amplify/core/internals/utils';

import { assertAuthTokens } from '../utils/types';
import { createCognitoUserPoolEndpointResolver } from '../factories';
import { getRegionFromUserPoolId } from '../../../foundation/parsers';
import { getAuthUserAgentValue, registerPasskey } from '../../../utils';
import {
createGetWebAuthnRegistrationOptionsClient,
createVerifyWebAuthnRegistrationResultClient,
} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider';
import {
PasskeyErrorCode,
assertPasskeyError,
} from '../../../utils/passkey/errors';
import { AssociateWebAuthnCredentialOutput } from '../types/outputs';

/**
* Registers a new passkey for an authenticated user
* @returns Promise<AssociateWebAuthnCredentialOutput>
* @throws - {@link PasskeyError} Thrown when intermediate state is invalid
* @throws - {@link AuthError} Thrown when user is unauthenticated
*/

export async function associateWebAuthnCredential(): Promise<AssociateWebAuthnCredentialOutput> {
const authConfig = Amplify.getConfig().Auth?.Cognito;

assertTokenProviderConfig(authConfig);

const { userPoolEndpoint, userPoolId } = authConfig;

const { tokens } = await fetchAuthSession({ forceRefresh: false });

assertAuthTokens(tokens);

const getWebAuthnRegistrationOptions =
createGetWebAuthnRegistrationOptionsClient({
endpointResolver: createCognitoUserPoolEndpointResolver({
endpointOverride: userPoolEndpoint,
}),
});

const { CredentialCreationOptions: credentialCreationOptions } =
await getWebAuthnRegistrationOptions(
{
region: getRegionFromUserPoolId(userPoolId),
userAgentValue: getAuthUserAgentValue(
AuthAction.GetWebAuthnRegistrationOptions,
),
},
{
AccessToken: tokens.accessToken.toString(),
},
);

assertPasskeyError(
!!credentialCreationOptions,
PasskeyErrorCode.InvalidCredentialCreationOptions,
);

const cred = await registerPasskey(JSON.parse(credentialCreationOptions));

const verifyWebAuthnRegistrationResult =
createVerifyWebAuthnRegistrationResultClient({
endpointResolver: createCognitoUserPoolEndpointResolver({
endpointOverride: userPoolEndpoint,
}),
});

const { CredentialId: credentialId } = await verifyWebAuthnRegistrationResult(
{
region: getRegionFromUserPoolId(userPoolId),
userAgentValue: getAuthUserAgentValue(
AuthAction.VerifyWebAuthnRegistrationResult,
),
},
{
AccessToken: tokens.accessToken.toString(),
Credential: JSON.stringify(cred),
},
);

return {
credentialId,
};
}
2 changes: 2 additions & 0 deletions packages/auth/src/providers/cognito/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { rememberDevice } from './apis/rememberDevice';
export { forgetDevice } from './apis/forgetDevice';
export { fetchDevices } from './apis/fetchDevices';
export { autoSignIn } from './apis/autoSignIn';
export { associateWebAuthnCredential } from './apis/associateWebAuthnCredential';
export {
ConfirmResetPasswordInput,
ConfirmSignInInput,
Expand Down Expand Up @@ -62,6 +63,7 @@ export {
UpdateUserAttributeOutput,
SendUserAttributeVerificationCodeOutput,
FetchDevicesOutput,
AssociateWebAuthnCredentialOutput,
} from './types/outputs';
export {
AuthUser,
Expand Down
7 changes: 7 additions & 0 deletions packages/auth/src/providers/cognito/types/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,10 @@ export type UpdateUserAttributeOutput =
* Output type for Cognito fetchDevices API.
*/
export type FetchDevicesOutput = AWSAuthDevice[];

/**
* Output type for Cognito associateWebAuthnCredential API.
*/
export interface AssociateWebAuthnCredentialOutput {
credentialId?: string;
}
1 change: 1 addition & 0 deletions packages/auth/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
export { getAuthUserAgentDetails } from './getAuthUserAgentDetails';
export { getAuthUserAgentValue } from './getAuthUserAgentValue';
export { openAuthSession } from './openAuthSession';
export { registerPasskey } from './passkey';
29 changes: 29 additions & 0 deletions packages/auth/src/utils/passkey/base64Url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// https://datatracker.ietf.org/doc/html/rfc4648#page-7

/**
* Converts a base64url encoded string to an ArrayBuffer
* @param base64url - a base64url encoded string
* @returns ArrayBuffer
*/
export const convertBase64UrlToArrayBuffer = (
base64url: string,
): ArrayBuffer => {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');

return Uint8Array.from(atob(base64), x => x.charCodeAt(0)).buffer;
};

/**
* Converts an ArrayBuffer to a base64url encoded string
* @param buffer - the ArrayBuffer instance of a Uint8Array
* @returns string - a base64url encoded string
*/
export const convertArrayBufferToBase64Url = (buffer: ArrayBuffer): string => {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
};
37 changes: 37 additions & 0 deletions packages/auth/src/utils/passkey/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
AmplifyError,
AmplifyErrorMap,
AmplifyErrorParams,
AssertionFunction,
createAssertionFunction,
} from '@aws-amplify/core/internals/utils';

export class PasskeyError extends AmplifyError {
constructor(params: AmplifyErrorParams) {
super(params);

// Hack for making the custom error class work when transpiled to es5
// TODO: Delete the following 2 lines after we change the build target to >= es2015
this.constructor = PasskeyError;
Object.setPrototypeOf(this, PasskeyError.prototype);
}
}

export enum PasskeyErrorCode {
InvalidCredentialCreationOptions = 'InvalidCredentialCreationOptions',
PasskeyRegistrationFailed = 'PasskeyRegistrationFailed',
}

const passkeyErrorMap: AmplifyErrorMap<PasskeyErrorCode> = {
[PasskeyErrorCode.InvalidCredentialCreationOptions]: {
message: 'Invalid credential creation options',
recoverySuggestion:
'Ensure your user pool is configured to support WebAuthN passkey registration',
},
[PasskeyErrorCode.PasskeyRegistrationFailed]: {
message: 'Platform failed to create credentials',
},
};

export const assertPasskeyError: AssertionFunction<PasskeyErrorCode> =
createAssertionFunction(passkeyErrorMap, PasskeyError);
4 changes: 4 additions & 0 deletions packages/auth/src/utils/passkey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export { registerPasskey } from './registerPasskey';
8 changes: 8 additions & 0 deletions packages/auth/src/utils/passkey/registerPasskey.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils';

export const registerPasskey = async () => {
throw new PlatformNotSupportedError();
};
26 changes: 26 additions & 0 deletions packages/auth/src/utils/passkey/registerPasskey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { PasskeyCreateOptionsJson, PasskeyCreateResult } from './types';
import {
deserializeJsonToPkcCreationOptions,
serializePkcToJson,
} from './serde';
import { PasskeyErrorCode, assertPasskeyError } from './errors';

/**
* Registers a new passkey for user
* @param input - PasskeyCreateOptions
* @returns serialized PasskeyCreateResult
*/
export const registerPasskey = async (input: PasskeyCreateOptionsJson) => {
const passkeyCreationOptions = deserializeJsonToPkcCreationOptions(input);

const credential = (await navigator.credentials.create({
publicKey: passkeyCreationOptions,
})) as PasskeyCreateResult | null;

assertPasskeyError(!!credential, PasskeyErrorCode.PasskeyRegistrationFailed);

return serializePkcToJson(credential);
};
64 changes: 64 additions & 0 deletions packages/auth/src/utils/passkey/serde.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
convertArrayBufferToBase64Url,
convertBase64UrlToArrayBuffer,
} from './base64Url';
import {
PasskeyCreateOptions,
PasskeyCreateOptionsJson,
PasskeyCreateResult,
PasskeyCreateResultJson,
} from './types';

/**
* Deserializes Public Key Credential JSON
* @param input PasskeyCreateOptionsJson
* @returns PasskeyCreateOptions
*/
export const deserializeJsonToPkcCreationOptions = (
input: PasskeyCreateOptionsJson,
): PasskeyCreateOptions => {
const userIdBuffer = convertBase64UrlToArrayBuffer(input.user.id);
const challengeBuffer = convertBase64UrlToArrayBuffer(input.challenge);
const excludeCredentialsWithBuffer = (input.excludeCredentials || []).map(
excludeCred => ({
...excludeCred,
id: convertBase64UrlToArrayBuffer(excludeCred.id),
}),
);

return {
...input,
excludeCredentials: excludeCredentialsWithBuffer,
challenge: challengeBuffer,
user: {
...input.user,
id: userIdBuffer,
},
};
};

/**
* Serializes a Public Key Credential to JSON
* @param input PasskeyCreateResult
* @returns PasskeyCreateResultJson
*/
export const serializePkcToJson = (
input: PasskeyCreateResult,
): PasskeyCreateResultJson => {
return {
type: input.type,
id: input.id,
rawId: convertArrayBufferToBase64Url(input.rawId),
response: {
clientDataJSON: convertArrayBufferToBase64Url(
input.response.clientDataJSON,
),
attestationObject: convertArrayBufferToBase64Url(
input.response.attestationObject,
),
},
};
};
80 changes: 80 additions & 0 deletions packages/auth/src/utils/passkey/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

type PasskeyTransport = 'ble' | 'hybrid' | 'internal' | 'nfc' | 'usb';

export interface PasskeyCreateOptionsJson {
challenge: string;
rp: {
id: string;
name: string;
};
user: {
id: string;
name: string;
displayName: string;
};
pubKeyCredParams: {
alg: number;
type: 'public-key';
}[];
timeout: number;
excludeCredentials: {
type: 'public-key';
id: string;
transports?: PasskeyTransport[];
}[];
authenticatorSelection: {
requireResidentKey: boolean;
residentKey: 'discouraged' | 'preferred' | 'required';
userVerification: 'discouraged' | 'preferred' | 'required';
};
}

export interface PasskeyCreateOptions {
challenge: ArrayBuffer;
rp: {
id: string;
name: string;
};
user: {
id: ArrayBuffer;
name: string;
displayName: string;
};
pubKeyCredParams: {
alg: number;
type: 'public-key';
}[];
timeout: number;
excludeCredentials: {
type: 'public-key';
id: ArrayBuffer;
transports?: PasskeyTransport[];
}[];
authenticatorSelection: {
requireResidentKey: boolean;
residentKey: 'discouraged' | 'preferred' | 'required';
userVerification: 'discouraged' | 'preferred' | 'required';
};
}

export interface PasskeyCreateResult {
id: string;
rawId: ArrayBuffer;
type: 'public-key';
response: {
clientDataJSON: ArrayBuffer;
attestationObject: ArrayBuffer;
};
}

export interface PasskeyCreateResultJson {
id: string;
rawId: string;
type: 'public-key';
response: {
clientDataJSON: string;
attestationObject: string;
};
}

0 comments on commit 7b860ef

Please sign in to comment.