From 8065ea4c6c61df2421c50766c04666d3c73bfa8f Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Sat, 17 May 2025 16:13:31 -0700 Subject: [PATCH 1/2] feat: modify existing implementation --- ...ePasskeyAuthenticationError.native.test.ts | 77 +++++++ .../handlePasskeyAuthenticationError.test.ts | 47 ++++ .../errors/handlePasskeyError.native.test.ts | 62 +++++ .../passkey/errors/handlePasskeyError.test.ts | 57 +++++ ...dlePasskeyRegistrationError.native.test.ts | 94 ++++++++ .../handlePasskeyRegistrationError.test.ts | 64 ++++++ .../serde.test.ts} | 6 +- packages/auth/__tests__/mockData.ts | 4 + packages/auth/src/client/utils/index.ts | 2 +- .../auth/src/client/utils/passkey/errors.ts | 214 ------------------ ...handlePasskeyAuthenticationError.native.ts | 54 +++++ .../handlePasskeyAuthenticationError.ts | 40 ++++ .../errors/handlePasskeyError.native.ts | 53 +++++ .../passkey/errors/handlePasskeyError.ts | 53 +++++ .../handlePasskeyRegistrationError.native.ts | 66 ++++++ .../errors/handlePasskeyRegistrationError.ts | 52 +++++ .../src/client/utils/passkey/errors/index.ts | 10 + .../utils/passkey/errors/passkeyError.ts | 96 ++++++++ .../passkeyErrorPlatformConstants.native.ts | 5 + .../errors/passkeyErrorPlatformConstants.ts | 5 + .../passkey/getIsPasskeySupported.native.ts | 4 +- .../client/utils/passkey/getPasskey.native.ts | 15 +- .../utils/passkey/registerPasskey.native.ts | 15 +- .../src/client/utils/passkey/types/shared.ts | 8 +- packages/auth/src/index.ts | 3 +- .../react-native/internals/utils/package.json | 8 + packages/react-native/package.json | 21 +- packages/react-native/src/index.ts | 1 + .../react-native/src/moduleLoaders/index.ts | 1 + .../moduleLoaders/loadAmplifyRtnPasskeys.ts | 28 +++ .../src/utils/getIsNativeError.ts | 18 ++ packages/react-native/src/utils/index.ts | 4 + 32 files changed, 954 insertions(+), 233 deletions(-) create mode 100644 packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts create mode 100644 packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.test.ts create mode 100644 packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts create mode 100644 packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.test.ts create mode 100644 packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts create mode 100644 packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.test.ts rename packages/auth/__tests__/client/utils/{passkey.test.ts => passkey/serde.test.ts} (89%) delete mode 100644 packages/auth/src/client/utils/passkey/errors.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/handlePasskeyError.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/index.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/passkeyError.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.native.ts create mode 100644 packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.ts create mode 100644 packages/react-native/internals/utils/package.json create mode 100644 packages/react-native/src/moduleLoaders/loadAmplifyRtnPasskeys.ts create mode 100644 packages/react-native/src/utils/getIsNativeError.ts create mode 100644 packages/react-native/src/utils/index.ts diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts new file mode 100644 index 00000000000..ab218e64442 --- /dev/null +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts @@ -0,0 +1,77 @@ +import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; + +import { + PasskeyError, + PasskeyErrorCode, +} from '../../../../../src/client/utils/passkey/errors'; +import { handlePasskeyAuthenticationError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native'; +import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError'; +import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; +import { MockNativeError } from '../../../../mockData'; + +const mockHandlePasskeyError = jest.mocked(handlePasskeyError); +jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError'); + +jest.mock('@aws-amplify/react-native/internals/utils', () => ({ + getIsNativeError: jest.fn(() => true), +})); + +const mockGetIsNativeError = jest.mocked(getIsNativeError); + +describe('handlePasskeyAuthenticationError', () => { + it('returns early if err is already instanceof PasskeyError', () => { + const err = new PasskeyError({ + name: 'PasskeyErrorName', + message: 'Error Message', + }); + + expect(handlePasskeyAuthenticationError(err)).toBe(err); + expect(mockGetIsNativeError).not.toHaveBeenCalled(); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is FAILED', () => { + const err = new MockNativeError(); + err.code = 'FAILED'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRetrievalFailed]; + + expect(handlePasskeyAuthenticationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyRetrievalFailed, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is CANCELED', () => { + const err = new MockNativeError(); + err.code = 'CANCELED'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled]; + + expect(handlePasskeyAuthenticationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyAuthenticationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); + + it('invokes handlePasskeyError when input error does not match expected cases', () => { + const err = new Error(); + err.name = 'Unknown'; + + handlePasskeyAuthenticationError(err); + + expect(mockHandlePasskeyError).toHaveBeenCalledWith(err); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); +}); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.test.ts new file mode 100644 index 00000000000..da4e7da547e --- /dev/null +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.test.ts @@ -0,0 +1,47 @@ +import { + PasskeyError, + PasskeyErrorCode, + handlePasskeyAuthenticationError, +} from '../../../../../src/client/utils/passkey/errors'; +import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError'; +import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; + +const mockHandlePasskeyError = jest.mocked(handlePasskeyError); +jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError'); + +describe('handlePasskeyAuthenticationError', () => { + it('returns early if err is already instanceof PasskeyError', () => { + const err = new PasskeyError({ + name: 'PasskeyErrorName', + message: 'Error Message', + }); + + expect(handlePasskeyAuthenticationError(err)).toBe(err); + }); + + it('returns new instance of PasskeyError with correct attributes when input error name is NotAllowedError', () => { + const err = new Error(); + err.name = 'NotAllowedError'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled]; + + expect(handlePasskeyAuthenticationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyAuthenticationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('invokes handlePasskeyError when input error does not match expected cases', () => { + const err = new Error(); + err.name = 'Unknown'; + + handlePasskeyAuthenticationError(err); + + expect(mockHandlePasskeyError).toHaveBeenCalledWith(err); + }); +}); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts new file mode 100644 index 00000000000..d5aca37ce50 --- /dev/null +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts @@ -0,0 +1,62 @@ +import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils'; + +import { + PasskeyError, + PasskeyErrorCode, +} from '../../../../../src/client/utils/passkey/errors'; +import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError.native'; +import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; +import { MockNativeError } from '../../../../mockData'; + +jest.mock('@aws-amplify/react-native/internals/utils', () => ({ + getIsNativeError: jest.fn(() => true), +})); + +describe('handlePasskeyError', () => { + it('returns new instance of PasskeyError with correct attributes when input error code is RELYING_PARTY_MISMATCH', () => { + const err = new MockNativeError(); + err.code = 'RELYING_PARTY_MISMATCH'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch]; + + expect(handlePasskeyError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.RelyingPartyMismatch, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is NOT_SUPPORTED', () => { + const err = new MockNativeError(); + err.code = 'NOT_SUPPORTED'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyNotSupported]; + + expect(handlePasskeyError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyNotSupported, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('returns unknown PasskeyError when input does not match expected cases', () => { + const err = new MockNativeError(); + err.name = 'UNKNOWN'; + + expect(handlePasskeyError(err)).toMatchObject( + new PasskeyError({ + name: AmplifyErrorCode.Unknown, + message: 'An unknown error has occurred.', + underlyingError: err, + }), + ); + }); +}); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.test.ts new file mode 100644 index 00000000000..b2566dd1339 --- /dev/null +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.test.ts @@ -0,0 +1,57 @@ +import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils'; + +import { + PasskeyError, + PasskeyErrorCode, +} from '../../../../../src/client/utils/passkey/errors'; +import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError'; +import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; + +describe('handlePasskeyError', () => { + it('returns new instance of PasskeyError with correct attributes when input error name is AbortError', () => { + const err = new Error(); + err.name = 'AbortError'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyOperationAborted]; + + expect(handlePasskeyError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyOperationAborted, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('returns new instance of PasskeyError with correct attributes when input error name is SecurityError', () => { + const err = new Error(); + err.name = 'SecurityError'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch]; + + expect(handlePasskeyError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.RelyingPartyMismatch, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('returns unknown PasskeyError when input does not match expected cases', () => { + const err = new Error(); + err.name = 'Unknown'; + + expect(handlePasskeyError(err)).toMatchObject( + new PasskeyError({ + name: AmplifyErrorCode.Unknown, + message: 'An unknown error has occurred.', + underlyingError: err, + }), + ); + }); +}); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts new file mode 100644 index 00000000000..139d627d244 --- /dev/null +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts @@ -0,0 +1,94 @@ +import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; + +import { + PasskeyError, + PasskeyErrorCode, +} from '../../../../../src/client/utils/passkey/errors'; +import { handlePasskeyRegistrationError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyRegistrationError.native'; +import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError'; +import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; +import { MockNativeError } from '../../../../mockData'; + +const mockHandlePasskeyError = jest.mocked(handlePasskeyError); +jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError'); + +jest.mock('@aws-amplify/react-native/internals/utils', () => ({ + getIsNativeError: jest.fn(() => true), +})); + +const mockGetIsNativeError = jest.mocked(getIsNativeError); + +describe('handlePasskeyRegistrationError', () => { + it('returns early if err is already instanceof PasskeyError', () => { + const err = new PasskeyError({ + name: 'PasskeyErrorName', + message: 'Error Message', + }); + + expect(handlePasskeyRegistrationError(err)).toBe(err); + expect(mockGetIsNativeError).not.toHaveBeenCalled(); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is FAILED', () => { + const err = new MockNativeError(); + err.code = 'FAILED'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationFailed]; + + expect(handlePasskeyRegistrationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyRegistrationFailed, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); + it('returns new instance of PasskeyError with correct attributes when input error code is DUPLICATE', () => { + const err = new MockNativeError(); + err.code = 'DUPLICATE'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists]; + + expect(handlePasskeyRegistrationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyAlreadyExists, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is CANCELED', () => { + const err = new MockNativeError(); + err.code = 'CANCELED'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled]; + + expect(handlePasskeyRegistrationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyRegistrationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); + + it('invokes handlePasskeyError when input error does not match expected cases', () => { + const err = new Error(); + err.name = 'Unknown'; + + handlePasskeyRegistrationError(err); + + expect(mockHandlePasskeyError).toHaveBeenCalledWith(err); + expect(mockGetIsNativeError).toHaveBeenCalledWith(err); + }); +}); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.test.ts new file mode 100644 index 00000000000..1db0f5cbeff --- /dev/null +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.test.ts @@ -0,0 +1,64 @@ +import { + PasskeyError, + PasskeyErrorCode, + handlePasskeyRegistrationError, +} from '../../../../../src/client/utils/passkey/errors'; +import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError'; +import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; + +const mockHandlePasskeyError = jest.mocked(handlePasskeyError); +jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError'); + +describe('handlePasskeyRegistrationError', () => { + it('returns early if err is already instanceof PasskeyError', () => { + const err = new PasskeyError({ + name: 'PasskeyErrorName', + message: 'Error Message', + }); + + expect(handlePasskeyRegistrationError(err)).toBe(err); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is InvalidStateError', () => { + const err = new Error(); + err.name = 'InvalidStateError'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists]; + + expect(handlePasskeyRegistrationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyAlreadyExists, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('returns new instance of PasskeyError with correct attributes when input error code is NotAllowedError', () => { + const err = new Error(); + err.name = 'NotAllowedError'; + + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled]; + + expect(handlePasskeyRegistrationError(err)).toMatchObject( + new PasskeyError({ + name: PasskeyErrorCode.PasskeyRegistrationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }), + ); + }); + + it('invokes handlePasskeyError when input error does not match expected cases', () => { + const err = new Error(); + err.name = 'Unknown'; + + handlePasskeyRegistrationError(err); + + expect(mockHandlePasskeyError).toHaveBeenCalledWith(err); + }); +}); diff --git a/packages/auth/__tests__/client/utils/passkey.test.ts b/packages/auth/__tests__/client/utils/passkey/serde.test.ts similarity index 89% rename from packages/auth/__tests__/client/utils/passkey.test.ts rename to packages/auth/__tests__/client/utils/passkey/serde.test.ts index c4fff5f891a..b227c0f0c64 100644 --- a/packages/auth/__tests__/client/utils/passkey.test.ts +++ b/packages/auth/__tests__/client/utils/passkey/serde.test.ts @@ -1,15 +1,15 @@ import { deserializeJsonToPkcCreationOptions, serializePkcWithAttestationToJson, -} from '../../../src/client/utils/passkey/serde'; +} from '../../../../src/client/utils/passkey/serde'; import { passkeyRegistrationRequest, passkeyRegistrationRequestJson, passkeyRegistrationResult, passkeyRegistrationResultJson, -} from '../../mockData'; +} from '../../../mockData'; -describe('passkey', () => { +describe('passkey serialization / deserialization', () => { it('serializes pkc into correct json format', () => { expect( JSON.stringify( diff --git a/packages/auth/__tests__/mockData.ts b/packages/auth/__tests__/mockData.ts index f1f4b09a3fe..fbcaa83047a 100644 --- a/packages/auth/__tests__/mockData.ts +++ b/packages/auth/__tests__/mockData.ts @@ -423,3 +423,7 @@ export const mockUserCredentials = [ CreatedAt: 1582939425, }, ]; + +export class MockNativeError extends Error { + code?: string; +} diff --git a/packages/auth/src/client/utils/index.ts b/packages/auth/src/client/utils/index.ts index ef0913b2b8d..deaa4a27d2e 100644 --- a/packages/auth/src/client/utils/index.ts +++ b/packages/auth/src/client/utils/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { registerPasskey } from './passkey'; +export { getPasskey, registerPasskey } from './passkey'; diff --git a/packages/auth/src/client/utils/passkey/errors.ts b/packages/auth/src/client/utils/passkey/errors.ts deleted file mode 100644 index 288cb14e810..00000000000 --- a/packages/auth/src/client/utils/passkey/errors.ts +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { - AmplifyError, - AmplifyErrorCode, - 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 { - // not supported - PasskeyNotSupported = 'PasskeyNotSupported', - // duplicate passkey - PasskeyAlreadyExists = 'PasskeyAlreadyExists', - // misconfigurations - InvalidPasskeyRegistrationOptions = 'InvalidPasskeyRegistrationOptions', - InvalidPasskeyAuthenticationOptions = 'InvalidPasskeyAuthenticationOptions', - RelyingPartyMismatch = 'RelyingPartyMismatch', - // failed credential creation / retrieval - PasskeyRegistrationFailed = 'PasskeyRegistrationFailed', - PasskeyRetrievalFailed = 'PasskeyRetrievalFailed', - // cancel / aborts - PasskeyRegistrationCanceled = 'PasskeyRegistrationCanceled', - PasskeyAuthenticationCanceled = 'PasskeyAuthenticationCanceled', - PasskeyOperationAborted = 'PasskeyOperationAborted', -} - -const notSupportedRecoverySuggestion = - 'Passkeys may not be supported on this device. Ensure your application is running in a secure context (HTTPS) and Web Authentication API is supported.'; -const abortOrCancelRecoverySuggestion = - 'User may have canceled the ceremony or another interruption has occurred. Check underlying error for details.'; -const misconfigurationRecoverySuggestion = - 'Ensure your user pool is configured to support the WEB_AUTHN as an authentication factor.'; - -const passkeyErrorMap: AmplifyErrorMap = { - [PasskeyErrorCode.PasskeyNotSupported]: { - message: 'Passkeys may not be supported on this device.', - recoverySuggestion: notSupportedRecoverySuggestion, - }, - [PasskeyErrorCode.InvalidPasskeyRegistrationOptions]: { - message: 'Invalid passkey registration options.', - recoverySuggestion: misconfigurationRecoverySuggestion, - }, - [PasskeyErrorCode.InvalidPasskeyAuthenticationOptions]: { - message: 'Invalid passkey authentication options.', - recoverySuggestion: misconfigurationRecoverySuggestion, - }, - [PasskeyErrorCode.PasskeyRegistrationFailed]: { - message: 'Device failed to create passkey.', - recoverySuggestion: notSupportedRecoverySuggestion, - }, - [PasskeyErrorCode.PasskeyRetrievalFailed]: { - message: 'Device failed to retrieve passkey.', - recoverySuggestion: - 'Passkeys may not be available on this device. Try an alternative authentication factor like PASSWORD, EMAIL_OTP, or SMS_OTP.', - }, - [PasskeyErrorCode.PasskeyAlreadyExists]: { - message: 'Passkey already exists in authenticator.', - recoverySuggestion: - 'Proceed with existing passkey or try again after deleting the credential.', - }, - [PasskeyErrorCode.PasskeyRegistrationCanceled]: { - message: 'Passkey registration ceremony has been canceled.', - recoverySuggestion: abortOrCancelRecoverySuggestion, - }, - [PasskeyErrorCode.PasskeyAuthenticationCanceled]: { - message: 'Passkey authentication ceremony has been canceled.', - recoverySuggestion: abortOrCancelRecoverySuggestion, - }, - [PasskeyErrorCode.PasskeyOperationAborted]: { - message: 'Passkey operation has been aborted.', - recoverySuggestion: abortOrCancelRecoverySuggestion, - }, - [PasskeyErrorCode.RelyingPartyMismatch]: { - message: 'Relying party does not match current domain.', - recoverySuggestion: - 'Ensure relying party identifier matches current domain.', - }, -}; - -export const assertPasskeyError: AssertionFunction = - createAssertionFunction(passkeyErrorMap, PasskeyError); - -/** - * Handle Passkey Authentication Errors - * https://w3c.github.io/webauthn/#sctn-get-request-exceptions - * - * @param err unknown - * @returns PasskeyError - */ - -export const handlePasskeyAuthenticationError = ( - err: unknown, -): PasskeyError => { - if (err instanceof PasskeyError) { - return err; - } - - if (err instanceof Error) { - if (err.name === 'NotAllowedError') { - const { message, recoverySuggestion } = - passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled]; - - return new PasskeyError({ - name: PasskeyErrorCode.PasskeyAuthenticationCanceled, - message, - recoverySuggestion, - underlyingError: err, - }); - } - } - - return handlePasskeyError(err); -}; - -/** - * Handle Passkey Registration Errors - * https://w3c.github.io/webauthn/#sctn-create-request-exceptions - * - * @param err unknown - * @returns PasskeyError - */ -export const handlePasskeyRegistrationError = (err: unknown): PasskeyError => { - if (err instanceof PasskeyError) { - return err; - } - - if (err instanceof Error) { - // Duplicate Passkey - if (err.name === 'InvalidStateError') { - const { message, recoverySuggestion } = - passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists]; - - return new PasskeyError({ - name: PasskeyErrorCode.PasskeyAlreadyExists, - message, - recoverySuggestion, - underlyingError: err, - }); - } - - // User Cancels Ceremony / Generic Catch All - if (err.name === 'NotAllowedError') { - const { message, recoverySuggestion } = - passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled]; - - return new PasskeyError({ - name: PasskeyErrorCode.PasskeyRegistrationCanceled, - message, - recoverySuggestion, - underlyingError: err, - }); - } - } - - return handlePasskeyError(err); -}; - -/** - * Handles Overlapping Passkey Errors Between Registration & Authentication - * https://w3c.github.io/webauthn/#sctn-create-request-exceptions - * https://w3c.github.io/webauthn/#sctn-get-request-exceptions - * - * @param err unknown - * @returns PasskeyError - */ -const handlePasskeyError = (err: unknown): PasskeyError => { - if (err instanceof Error) { - // Passkey Operation Aborted - if (err.name === 'AbortError') { - const { message, recoverySuggestion } = - passkeyErrorMap[PasskeyErrorCode.PasskeyOperationAborted]; - - return new PasskeyError({ - name: PasskeyErrorCode.PasskeyOperationAborted, - message, - recoverySuggestion, - underlyingError: err, - }); - } - // Relying Party / Domain Mismatch - if (err.name === 'SecurityError') { - const { message, recoverySuggestion } = - passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch]; - - return new PasskeyError({ - name: PasskeyErrorCode.RelyingPartyMismatch, - message, - recoverySuggestion, - underlyingError: err, - }); - } - } - - return new PasskeyError({ - name: AmplifyErrorCode.Unknown, - message: 'An unknown error has occurred.', - underlyingError: err, - }); -}; diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts new file mode 100644 index 00000000000..fd88b8e6cff --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; + +import { handlePasskeyError } from './handlePasskeyError'; +import { + PasskeyError, + PasskeyErrorCode, + passkeyErrorMap, +} from './passkeyError'; + +/** + * Handle Passkey Authentication Errors + * + * @param err unknown + * @returns PasskeyError + */ +export const handlePasskeyAuthenticationError = ( + err: unknown, +): PasskeyError => { + if (err instanceof PasskeyError) { + return err; + } + + if (getIsNativeError(err)) { + // Passkey Retrieval Failed + if (err.code === 'FAILED') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRetrievalFailed]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyRetrievalFailed, + message, + recoverySuggestion, + underlyingError: err, + }); + } + + if (err.code === 'CANCELED') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyAuthenticationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }); + } + } + + return handlePasskeyError(err); +}; diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.ts new file mode 100644 index 00000000000..6fd453117a8 --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { handlePasskeyError } from './handlePasskeyError'; +import { + PasskeyError, + PasskeyErrorCode, + passkeyErrorMap, +} from './passkeyError'; + +/** + * Handle Passkey Authentication Errors + * https://w3c.github.io/webauthn/#sctn-get-request-exceptions + * + * @param err unknown + * @returns PasskeyError + */ +export const handlePasskeyAuthenticationError = ( + err: unknown, +): PasskeyError => { + if (err instanceof PasskeyError) { + return err; + } + + if (err instanceof Error) { + if (err.name === 'NotAllowedError') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyAuthenticationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }); + } + } + + return handlePasskeyError(err); +}; diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts new file mode 100644 index 00000000000..354675495cc --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils'; +import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; + +import { + PasskeyError, + PasskeyErrorCode, + passkeyErrorMap, +} from './passkeyError'; + +/** + * Handles Overlapping Passkey Errors Between Registration & Authentication + * + * @param err unknown + * @returns PasskeyError + */ +export const handlePasskeyError = (err: unknown): PasskeyError => { + if (getIsNativeError(err)) { + // Relying Party / Domain Mismatch + if (err.code === 'RELYING_PARTY_MISMATCH') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch]; + + return new PasskeyError({ + name: PasskeyErrorCode.RelyingPartyMismatch, + message, + recoverySuggestion, + underlyingError: err, + }); + } + + // Not Supported + if (err.code === 'NOT_SUPPORTED') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyNotSupported]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyNotSupported, + message, + recoverySuggestion, + underlyingError: err, + }); + } + } + + return new PasskeyError({ + name: AmplifyErrorCode.Unknown, + message: 'An unknown error has occurred.', + underlyingError: err, + }); +}; diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.ts new file mode 100644 index 00000000000..ae12403044c --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils'; + +import { + PasskeyError, + PasskeyErrorCode, + passkeyErrorMap, +} from './passkeyError'; + +/** + * Handles Overlapping Passkey Errors Between Registration & Authentication + * https://w3c.github.io/webauthn/#sctn-create-request-exceptions + * https://w3c.github.io/webauthn/#sctn-get-request-exceptions + * + * @param err unknown + * @returns PasskeyError + */ +export const handlePasskeyError = (err: unknown): PasskeyError => { + if (err instanceof Error) { + // Passkey Operation Aborted + if (err.name === 'AbortError') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyOperationAborted]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyOperationAborted, + message, + recoverySuggestion, + underlyingError: err, + }); + } + // Relying Party / Domain Mismatch + if (err.name === 'SecurityError') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch]; + + return new PasskeyError({ + name: PasskeyErrorCode.RelyingPartyMismatch, + message, + recoverySuggestion, + underlyingError: err, + }); + } + } + + return new PasskeyError({ + name: AmplifyErrorCode.Unknown, + message: 'An unknown error has occurred.', + underlyingError: err, + }); +}; diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts new file mode 100644 index 00000000000..8a6e68067cb --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; + +import { handlePasskeyError } from './handlePasskeyError'; +import { + PasskeyError, + PasskeyErrorCode, + passkeyErrorMap, +} from './passkeyError'; + +/** + * Handle Passkey Registration Errors + * + * @param err unknown + * @returns PasskeyError + */ +export const handlePasskeyRegistrationError = (err: unknown): PasskeyError => { + if (err instanceof PasskeyError) { + return err; + } + + if (getIsNativeError(err)) { + // Passkey Registration Failed + if (err.code === 'FAILED') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationFailed]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyRegistrationFailed, + message, + recoverySuggestion, + underlyingError: err, + }); + } + + // Duplicate Passkey + if (err.code === 'DUPLICATE') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyAlreadyExists, + message, + recoverySuggestion, + underlyingError: err, + }); + } + + // User Cancels Ceremony + if (err.code === 'CANCELED') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyRegistrationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }); + } + } + + return handlePasskeyError(err); +}; diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.ts new file mode 100644 index 00000000000..48347e7920e --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.ts @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { handlePasskeyError } from './handlePasskeyError'; +import { + PasskeyError, + PasskeyErrorCode, + passkeyErrorMap, +} from './passkeyError'; + +/** + * Handle Passkey Registration Errors + * https://w3c.github.io/webauthn/#sctn-create-request-exceptions + * + * @param err unknown + * @returns PasskeyError + */ +export const handlePasskeyRegistrationError = (err: unknown): PasskeyError => { + if (err instanceof PasskeyError) { + return err; + } + + if (err instanceof Error) { + // Duplicate Passkey + if (err.name === 'InvalidStateError') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyAlreadyExists, + message, + recoverySuggestion, + underlyingError: err, + }); + } + + // User Cancels Ceremony / Generic Catch All + if (err.name === 'NotAllowedError') { + const { message, recoverySuggestion } = + passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled]; + + return new PasskeyError({ + name: PasskeyErrorCode.PasskeyRegistrationCanceled, + message, + recoverySuggestion, + underlyingError: err, + }); + } + } + + return handlePasskeyError(err); +}; diff --git a/packages/auth/src/client/utils/passkey/errors/index.ts b/packages/auth/src/client/utils/passkey/errors/index.ts new file mode 100644 index 00000000000..54ec7f52394 --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/index.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { handlePasskeyAuthenticationError } from './handlePasskeyAuthenticationError'; +export { handlePasskeyRegistrationError } from './handlePasskeyRegistrationError'; +export { + PasskeyError, + PasskeyErrorCode, + assertPasskeyError, +} from './passkeyError'; diff --git a/packages/auth/src/client/utils/passkey/errors/passkeyError.ts b/packages/auth/src/client/utils/passkey/errors/passkeyError.ts new file mode 100644 index 00000000000..39fb6ad0785 --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/passkeyError.ts @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AmplifyError, + AmplifyErrorMap, + AmplifyErrorParams, + AssertionFunction, + createAssertionFunction, +} from '@aws-amplify/core/internals/utils'; + +import { NOT_SUPPORTED_RECOVERY_SUGGESTION } from './passkeyErrorPlatformConstants'; + +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 { + // not supported + PasskeyNotSupported = 'PasskeyNotSupported', + // duplicate passkey + PasskeyAlreadyExists = 'PasskeyAlreadyExists', + // misconfigurations + InvalidPasskeyRegistrationOptions = 'InvalidPasskeyRegistrationOptions', + InvalidPasskeyAuthenticationOptions = 'InvalidPasskeyAuthenticationOptions', + RelyingPartyMismatch = 'RelyingPartyMismatch', + // failed credential creation / retrieval + PasskeyRegistrationFailed = 'PasskeyRegistrationFailed', + PasskeyRetrievalFailed = 'PasskeyRetrievalFailed', + // cancel / aborts + PasskeyRegistrationCanceled = 'PasskeyRegistrationCanceled', + PasskeyAuthenticationCanceled = 'PasskeyAuthenticationCanceled', + PasskeyOperationAborted = 'PasskeyOperationAborted', +} + +const ABORT_OR_CANCEL_RECOVERY_SUGGESTION = + 'User may have canceled the ceremony or another interruption has occurred. Check underlying error for details.'; + +const MISCONFIGURATION_RECOVERY_SUGGESTION = + 'Ensure your user pool is configured to support the WEB_AUTHN as an authentication factor.'; + +export const passkeyErrorMap: AmplifyErrorMap = { + [PasskeyErrorCode.PasskeyNotSupported]: { + message: 'Passkeys may not be supported on this device.', + recoverySuggestion: NOT_SUPPORTED_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.InvalidPasskeyRegistrationOptions]: { + message: 'Invalid passkey registration options.', + recoverySuggestion: MISCONFIGURATION_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.InvalidPasskeyAuthenticationOptions]: { + message: 'Invalid passkey authentication options.', + recoverySuggestion: MISCONFIGURATION_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.PasskeyRegistrationFailed]: { + message: 'Device failed to create passkey.', + recoverySuggestion: NOT_SUPPORTED_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.PasskeyRetrievalFailed]: { + message: 'Device failed to retrieve passkey.', + recoverySuggestion: + 'Passkeys may not be available on this device. Try an alternative authentication factor like PASSWORD, EMAIL_OTP, or SMS_OTP.', + }, + [PasskeyErrorCode.PasskeyAlreadyExists]: { + message: 'Passkey already exists in authenticator.', + recoverySuggestion: + 'Proceed with existing passkey or try again after deleting the credential.', + }, + [PasskeyErrorCode.PasskeyRegistrationCanceled]: { + message: 'Passkey registration ceremony has been canceled.', + recoverySuggestion: ABORT_OR_CANCEL_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.PasskeyAuthenticationCanceled]: { + message: 'Passkey authentication ceremony has been canceled.', + recoverySuggestion: ABORT_OR_CANCEL_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.PasskeyOperationAborted]: { + message: 'Passkey operation has been aborted.', + recoverySuggestion: ABORT_OR_CANCEL_RECOVERY_SUGGESTION, + }, + [PasskeyErrorCode.RelyingPartyMismatch]: { + message: 'Relying party does not match current domain.', + recoverySuggestion: + 'Ensure relying party identifier matches current domain.', + }, +}; + +export const assertPasskeyError: AssertionFunction = + createAssertionFunction(passkeyErrorMap, PasskeyError); diff --git a/packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.native.ts b/packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.native.ts new file mode 100644 index 00000000000..43cc69f1f41 --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.native.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const NOT_SUPPORTED_RECOVERY_SUGGESTION = + 'Passkeys may not be supported on this device. Ensure your application is running on a supported platform.'; diff --git a/packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.ts b/packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.ts new file mode 100644 index 00000000000..81c3ece5049 --- /dev/null +++ b/packages/auth/src/client/utils/passkey/errors/passkeyErrorPlatformConstants.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const NOT_SUPPORTED_RECOVERY_SUGGESTION = + 'Passkeys may not be supported on this device. Ensure your application is running in a secure context (HTTPS) and Web Authentication API is supported.'; diff --git a/packages/auth/src/client/utils/passkey/getIsPasskeySupported.native.ts b/packages/auth/src/client/utils/passkey/getIsPasskeySupported.native.ts index a6090da47ce..fa1f01ec73d 100644 --- a/packages/auth/src/client/utils/passkey/getIsPasskeySupported.native.ts +++ b/packages/auth/src/client/utils/passkey/getIsPasskeySupported.native.ts @@ -1,8 +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'; +import { loadAmplifyRtnPasskeys } from '@aws-amplify/react-native'; export const getIsPasskeySupported = () => { - throw new PlatformNotSupportedError(); + return loadAmplifyRtnPasskeys().getIsPasskeySupported(); }; diff --git a/packages/auth/src/client/utils/passkey/getPasskey.native.ts b/packages/auth/src/client/utils/passkey/getPasskey.native.ts index 96f6662b590..8892c250302 100644 --- a/packages/auth/src/client/utils/passkey/getPasskey.native.ts +++ b/packages/auth/src/client/utils/passkey/getPasskey.native.ts @@ -1,8 +1,17 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils'; +import { loadAmplifyRtnPasskeys } from '@aws-amplify/react-native'; -export const getPasskey = async () => { - throw new PlatformNotSupportedError(); +import { PasskeyGetOptionsJson, PasskeyGetResultJson } from './types'; +import { handlePasskeyAuthenticationError } from './errors'; + +export const getPasskey = async ( + input: PasskeyGetOptionsJson, +): Promise => { + try { + return await loadAmplifyRtnPasskeys().getPasskey(input); + } catch (err: unknown) { + throw handlePasskeyAuthenticationError(err); + } }; diff --git a/packages/auth/src/client/utils/passkey/registerPasskey.native.ts b/packages/auth/src/client/utils/passkey/registerPasskey.native.ts index 15ab00dc290..486e5791cbc 100644 --- a/packages/auth/src/client/utils/passkey/registerPasskey.native.ts +++ b/packages/auth/src/client/utils/passkey/registerPasskey.native.ts @@ -1,8 +1,17 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils'; +import { loadAmplifyRtnPasskeys } from '@aws-amplify/react-native'; -export const registerPasskey = async () => { - throw new PlatformNotSupportedError(); +import { PasskeyCreateOptionsJson, PasskeyCreateResultJson } from './types'; +import { handlePasskeyRegistrationError } from './errors'; + +export const registerPasskey = async ( + input: PasskeyCreateOptionsJson, +): Promise => { + try { + return await loadAmplifyRtnPasskeys().createPasskey(input); + } catch (err: unknown) { + throw handlePasskeyRegistrationError(err); + } }; diff --git a/packages/auth/src/client/utils/passkey/types/shared.ts b/packages/auth/src/client/utils/passkey/types/shared.ts index 847118d7e25..97da0b521a5 100644 --- a/packages/auth/src/client/utils/passkey/types/shared.ts +++ b/packages/auth/src/client/utils/passkey/types/shared.ts @@ -55,8 +55,8 @@ export interface PkcAttestationResponse { attestationObject: T; transports: string[]; publicKey?: string; - publicKeyAlgorithm: number; - authenticatorData: T; + publicKeyAlgorithm?: number; + authenticatorData?: T; } export interface PasskeyCreateResult { id: string; @@ -69,7 +69,7 @@ export interface PasskeyCreateResultJson { id: string; rawId: string; type: string; - clientExtensionResults: { + clientExtensionResults?: { appId?: boolean; credProps?: { rk?: boolean }; hmacCreateSecret?: boolean; @@ -121,7 +121,7 @@ export interface PasskeyGetResultJson { id: string; rawId: string; type: string; - clientExtensionResults: { + clientExtensionResults?: { appId?: boolean; credProps?: { rk?: boolean }; hmacCreateSecret?: boolean; diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 0ca9948aa9b..b4ba2de0c29 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -88,9 +88,8 @@ export { JWT, } from '@aws-amplify/core'; -export { associateWebAuthnCredential } from './client/apis'; - export { + associateWebAuthnCredential, listWebAuthnCredentials, deleteWebAuthnCredential, } from './client/apis'; diff --git a/packages/react-native/internals/utils/package.json b/packages/react-native/internals/utils/package.json new file mode 100644 index 00000000000..9c7acb2be22 --- /dev/null +++ b/packages/react-native/internals/utils/package.json @@ -0,0 +1,8 @@ +{ + "name": "@aws-amplify/react-native/internals/utils", + "types": "../../dist/esm/utils/index.d.ts", + "main": "../../dist/cjs/utils/index.js", + "module": "../../dist/esm/utils/index.mjs", + "react-native": "../../dist/cjs/utils/index.js", + "sideEffects": false +} diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 8158009d248..592b3e9ca5e 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -10,9 +10,27 @@ "publishConfig": { "access": "public" }, + "exports": { + ".": { + "react-native": "./dist/cjs/index.js", + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.js" + }, + "./internals/utils": { + "react-native": "./dist/cjs/utils/index.js", + "types": "./dist/esm/utils/index.d.ts", + "import": "./dist/esm/utils/index.mjs", + "require": "./dist/cjs/utils/index.js" + }, + "./package.json": "./package.json" + }, "scripts": { + "prepare:ios": "cd example && npx pod-install", + "prepare:android": "echo 'no-op'", "test": "echo 'no-op'", - "test:android": "./android/gradlew test -p ./android", + "test:ios": "echo 'no-op'", + "test:android": "cd ./example/android && ./gradlew test -i", "build-with-test": "npm run clean && npm test && tsc", "build:esm-cjs": "rollup --forceExit -c rollup.config.mjs", "build:watch": "npm run build:esm-cjs -- --watch", @@ -33,6 +51,7 @@ "react-native-get-random-values": ">=1.8.0" }, "devDependencies": { + "@aws-amplify/rtn-passkeys": "*", "@aws-amplify/rtn-push-notification": "1.2.34", "@aws-amplify/rtn-web-browser": "1.1.3", "@react-native-async-storage/async-storage": "^1.17.12", diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 357e43e0f2b..887e5f1de9b 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -8,6 +8,7 @@ export { getDeviceName, } from './apis'; export { + loadAmplifyRtnPasskeys, loadAmplifyPushNotification, loadAmplifyWebBrowser, loadAsyncStorage, diff --git a/packages/react-native/src/moduleLoaders/index.ts b/packages/react-native/src/moduleLoaders/index.ts index 9c9e24522af..7896de5c53f 100644 --- a/packages/react-native/src/moduleLoaders/index.ts +++ b/packages/react-native/src/moduleLoaders/index.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +export { loadAmplifyRtnPasskeys } from './loadAmplifyRtnPasskeys'; export { loadAmplifyPushNotification } from './loadAmplifyPushNotification'; export { loadAmplifyWebBrowser } from './loadAmplifyWebBrowser'; export { loadAsyncStorage } from './loadAsyncStorage'; diff --git a/packages/react-native/src/moduleLoaders/loadAmplifyRtnPasskeys.ts b/packages/react-native/src/moduleLoaders/loadAmplifyRtnPasskeys.ts new file mode 100644 index 00000000000..4801a369f91 --- /dev/null +++ b/packages/react-native/src/moduleLoaders/loadAmplifyRtnPasskeys.ts @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { AmplifyRtnPasskeysModule } from '@aws-amplify/rtn-passkeys'; + +export const loadAmplifyRtnPasskeys = () => { + try { + // metro bundler requires static string for loading module. + // See: https://facebook.github.io/metro/docs/configuration/#dynamicdepsinpackages + const module = require('@aws-amplify/rtn-passkeys')?.module; + if (module) { + return module as AmplifyRtnPasskeysModule; + } + + throw new Error( + 'Ensure `@aws-amplify/rtn-passkeys` is installed and linked.', + ); + } catch (e) { + // The error parsing logic cannot be extracted with metro as the `require` + // would be confused when there is a `import` in the same file importing + // another module and that causes an error + const message = (e as Error).message.replace( + /undefined/g, + '@aws-amplify/rtn-passkeys', + ); + throw new Error(message); + } +}; diff --git a/packages/react-native/src/utils/getIsNativeError.ts b/packages/react-native/src/utils/getIsNativeError.ts new file mode 100644 index 00000000000..3b99a90ed74 --- /dev/null +++ b/packages/react-native/src/utils/getIsNativeError.ts @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export interface NativeError extends Error { + code: string; + domain?: string; + userInfo?: Record; + nativeStackIOS?: never[]; + nativeStackAndroid?: Record[]; +} + +export const getIsNativeError = (err: unknown): err is NativeError => { + return ( + err instanceof Error && + 'code' in err && + ('nativeStackIOS' in err || 'nativeStackAndroid' in err) + ); +}; diff --git a/packages/react-native/src/utils/index.ts b/packages/react-native/src/utils/index.ts new file mode 100644 index 00000000000..90f57ac58eb --- /dev/null +++ b/packages/react-native/src/utils/index.ts @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { type NativeError, getIsNativeError } from './getIsNativeError'; From 069893e7aad135e90c57e0622bea26d8887a0713 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Wed, 21 May 2025 16:55:00 -0700 Subject: [PATCH 2/2] chore: refactor exports --- ...ePasskeyAuthenticationError.native.test.ts | 4 ++-- .../errors/handlePasskeyError.native.test.ts | 2 +- ...dlePasskeyRegistrationError.native.test.ts | 5 ++-- packages/auth/package.json | 8 ++++++- ...handlePasskeyAuthenticationError.native.ts | 2 +- .../errors/handlePasskeyError.native.ts | 2 +- .../handlePasskeyRegistrationError.native.ts | 2 +- packages/react-native/README.md | 3 +++ .../react-native/internals/utils/package.json | 8 ------- packages/react-native/package.json | 23 ++++++------------- .../src/{utils => apis}/getIsNativeError.ts | 2 +- packages/react-native/src/apis/index.ts | 1 + packages/react-native/src/index.ts | 1 + packages/react-native/src/utils/index.ts | 4 ---- packages/rtn-passkeys/README.md | 3 +++ packages/rtn-passkeys/package.json | 2 +- 16 files changed, 33 insertions(+), 39 deletions(-) create mode 100644 packages/react-native/README.md delete mode 100644 packages/react-native/internals/utils/package.json rename packages/react-native/src/{utils => apis}/getIsNativeError.ts (90%) delete mode 100644 packages/react-native/src/utils/index.ts create mode 100644 packages/rtn-passkeys/README.md diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts index ab218e64442..30c36d3d8a6 100644 --- a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.test.ts @@ -1,4 +1,4 @@ -import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; +import { getIsNativeError } from '@aws-amplify/react-native'; import { PasskeyError, @@ -12,7 +12,7 @@ import { MockNativeError } from '../../../../mockData'; const mockHandlePasskeyError = jest.mocked(handlePasskeyError); jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError'); -jest.mock('@aws-amplify/react-native/internals/utils', () => ({ +jest.mock('@aws-amplify/react-native', () => ({ getIsNativeError: jest.fn(() => true), })); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts index d5aca37ce50..d339b764993 100644 --- a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyError.native.test.ts @@ -8,7 +8,7 @@ import { handlePasskeyError } from '../../../../../src/client/utils/passkey/erro import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError'; import { MockNativeError } from '../../../../mockData'; -jest.mock('@aws-amplify/react-native/internals/utils', () => ({ +jest.mock('@aws-amplify/react-native', () => ({ getIsNativeError: jest.fn(() => true), })); diff --git a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts index 139d627d244..8a5e736a3f0 100644 --- a/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts +++ b/packages/auth/__tests__/client/utils/passkey/errors/handlePasskeyRegistrationError.native.test.ts @@ -1,4 +1,4 @@ -import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; +import { getIsNativeError } from '@aws-amplify/react-native'; import { PasskeyError, @@ -12,7 +12,7 @@ import { MockNativeError } from '../../../../mockData'; const mockHandlePasskeyError = jest.mocked(handlePasskeyError); jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError'); -jest.mock('@aws-amplify/react-native/internals/utils', () => ({ +jest.mock('@aws-amplify/react-native', () => ({ getIsNativeError: jest.fn(() => true), })); @@ -46,6 +46,7 @@ describe('handlePasskeyRegistrationError', () => { ); expect(mockGetIsNativeError).toHaveBeenCalledWith(err); }); + it('returns new instance of PasskeyError with correct attributes when input error code is DUPLICATE', () => { const err = new MockNativeError(); err.code = 'DUPLICATE'; diff --git a/packages/auth/package.json b/packages/auth/package.json index c215978c6f9..99e268869a2 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -96,7 +96,13 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.1.0" + "@aws-amplify/core": "^6.1.0", + "@aws-amplify/react-native": "^1.1.10" + }, + "peerDependenciesMeta": { + "@aws-amplify/react-native": { + "optional": true + } }, "devDependencies": { "@aws-amplify/core": "6.11.4", diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts index fd88b8e6cff..71b45ca1a12 100644 --- a/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; +import { getIsNativeError } from '@aws-amplify/react-native'; import { handlePasskeyError } from './handlePasskeyError'; import { diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts index 354675495cc..9c7078f824b 100644 --- a/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyError.native.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils'; -import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; +import { getIsNativeError } from '@aws-amplify/react-native'; import { PasskeyError, diff --git a/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts b/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts index 8a6e68067cb..b7cc75fc318 100644 --- a/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts +++ b/packages/auth/src/client/utils/passkey/errors/handlePasskeyRegistrationError.native.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getIsNativeError } from '@aws-amplify/react-native/internals/utils'; +import { getIsNativeError } from '@aws-amplify/react-native'; import { handlePasskeyError } from './handlePasskeyError'; import { diff --git a/packages/react-native/README.md b/packages/react-native/README.md new file mode 100644 index 00000000000..ec41888837b --- /dev/null +++ b/packages/react-native/README.md @@ -0,0 +1,3 @@ +> INTERNAL USE ONLY + +This package contains AWS Amplify React Native utilities and is intended for internal use only. Please ensure this package is installed alongside [aws-amplify](https://www.npmjs.com/package/aws-amplify). We do not advise using any exports from this package directly. diff --git a/packages/react-native/internals/utils/package.json b/packages/react-native/internals/utils/package.json deleted file mode 100644 index 9c7acb2be22..00000000000 --- a/packages/react-native/internals/utils/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@aws-amplify/react-native/internals/utils", - "types": "../../dist/esm/utils/index.d.ts", - "main": "../../dist/cjs/utils/index.js", - "module": "../../dist/esm/utils/index.mjs", - "react-native": "../../dist/cjs/utils/index.js", - "sideEffects": false -} diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 592b3e9ca5e..3e0116dbff1 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -10,21 +10,6 @@ "publishConfig": { "access": "public" }, - "exports": { - ".": { - "react-native": "./dist/cjs/index.js", - "types": "./dist/esm/index.d.ts", - "import": "./dist/esm/index.mjs", - "require": "./dist/cjs/index.js" - }, - "./internals/utils": { - "react-native": "./dist/cjs/utils/index.js", - "types": "./dist/esm/utils/index.d.ts", - "import": "./dist/esm/utils/index.mjs", - "require": "./dist/cjs/utils/index.js" - }, - "./package.json": "./package.json" - }, "scripts": { "prepare:ios": "cd example && npx pod-install", "prepare:android": "echo 'no-op'", @@ -48,7 +33,13 @@ }, "peerDependencies": { "react-native": ">=0.70", - "react-native-get-random-values": ">=1.8.0" + "react-native-get-random-values": ">=1.8.0", + "@aws-amplify/rtn-passkeys": "^1.0.0" + }, + "peerDependenciesMeta": { + "@aws-amplify/rtn-passkeys": { + "optional": true + } }, "devDependencies": { "@aws-amplify/rtn-passkeys": "*", diff --git a/packages/react-native/src/utils/getIsNativeError.ts b/packages/react-native/src/apis/getIsNativeError.ts similarity index 90% rename from packages/react-native/src/utils/getIsNativeError.ts rename to packages/react-native/src/apis/getIsNativeError.ts index 3b99a90ed74..11536c15cda 100644 --- a/packages/react-native/src/utils/getIsNativeError.ts +++ b/packages/react-native/src/apis/getIsNativeError.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export interface NativeError extends Error { +interface NativeError extends Error { code: string; domain?: string; userInfo?: Record; diff --git a/packages/react-native/src/apis/index.ts b/packages/react-native/src/apis/index.ts index ed2bc4f9678..1088a8080f3 100644 --- a/packages/react-native/src/apis/index.ts +++ b/packages/react-native/src/apis/index.ts @@ -5,3 +5,4 @@ export { computeModPow } from './computeModPow'; export { computeS } from './computeS'; export { getOperatingSystem } from './getOperatingSystem'; export { getDeviceName } from './getDeviceName'; +export { getIsNativeError } from './getIsNativeError'; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 887e5f1de9b..8e6efdcde04 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -6,6 +6,7 @@ export { computeS, getOperatingSystem, getDeviceName, + getIsNativeError, } from './apis'; export { loadAmplifyRtnPasskeys, diff --git a/packages/react-native/src/utils/index.ts b/packages/react-native/src/utils/index.ts deleted file mode 100644 index 90f57ac58eb..00000000000 --- a/packages/react-native/src/utils/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -export { type NativeError, getIsNativeError } from './getIsNativeError'; diff --git a/packages/rtn-passkeys/README.md b/packages/rtn-passkeys/README.md new file mode 100644 index 00000000000..7c08cd5ae2c --- /dev/null +++ b/packages/rtn-passkeys/README.md @@ -0,0 +1,3 @@ +> INTERNAL USE ONLY + +This package contains AWS Amplify React Native utilities and is intended for internal use only. Please ensure this package is installed alongside [aws-amplify](https://www.npmjs.com/package/aws-amplify) and [@aws-amplify/react-native](https://www.npmjs.com/package/@aws-amplify/react-native). We do not advise using any exports from this package directly. diff --git a/packages/rtn-passkeys/package.json b/packages/rtn-passkeys/package.json index 08e5a8b7cc5..42ef9d93de9 100644 --- a/packages/rtn-passkeys/package.json +++ b/packages/rtn-passkeys/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/rtn-passkeys", - "version": "0.1.0", + "version": "1.0.0", "description": "React Native module for aws-amplify webauthn support", "source": "./src/index.ts", "main": "./dist/cjs/index.js",