Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit abd18da

Browse files
committedMay 17, 2025·
feat: modify existing implementation
1 parent c3efbfc commit abd18da

32 files changed

+954
-233
lines changed
 
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { getIsNativeError } from '@aws-amplify/react-native/internals/utils';
2+
3+
import {
4+
PasskeyError,
5+
PasskeyErrorCode,
6+
} from '../../../../../src/client/utils/passkey/errors';
7+
import { handlePasskeyAuthenticationError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyAuthenticationError.native';
8+
import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError';
9+
import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError';
10+
import { MockNativeError } from '../../../../mockData';
11+
12+
const mockHandlePasskeyError = jest.mocked(handlePasskeyError);
13+
jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError');
14+
15+
jest.mock('@aws-amplify/react-native/internals/utils', () => ({
16+
getIsNativeError: jest.fn(() => true),
17+
}));
18+
19+
const mockGetIsNativeError = jest.mocked(getIsNativeError);
20+
21+
describe('handlePasskeyAuthenticationError', () => {
22+
it('returns early if err is already instanceof PasskeyError', () => {
23+
const err = new PasskeyError({
24+
name: 'PasskeyErrorName',
25+
message: 'Error Message',
26+
});
27+
28+
expect(handlePasskeyAuthenticationError(err)).toBe(err);
29+
expect(mockGetIsNativeError).not.toHaveBeenCalled();
30+
});
31+
32+
it('returns new instance of PasskeyError with correct attributes when input error code is FAILED', () => {
33+
const err = new MockNativeError();
34+
err.code = 'FAILED';
35+
36+
const { message, recoverySuggestion } =
37+
passkeyErrorMap[PasskeyErrorCode.PasskeyRetrievalFailed];
38+
39+
expect(handlePasskeyAuthenticationError(err)).toMatchObject(
40+
new PasskeyError({
41+
name: PasskeyErrorCode.PasskeyRetrievalFailed,
42+
message,
43+
recoverySuggestion,
44+
underlyingError: err,
45+
}),
46+
);
47+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
48+
});
49+
50+
it('returns new instance of PasskeyError with correct attributes when input error code is CANCELED', () => {
51+
const err = new MockNativeError();
52+
err.code = 'CANCELED';
53+
54+
const { message, recoverySuggestion } =
55+
passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled];
56+
57+
expect(handlePasskeyAuthenticationError(err)).toMatchObject(
58+
new PasskeyError({
59+
name: PasskeyErrorCode.PasskeyAuthenticationCanceled,
60+
message,
61+
recoverySuggestion,
62+
underlyingError: err,
63+
}),
64+
);
65+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
66+
});
67+
68+
it('invokes handlePasskeyError when input error does not match expected cases', () => {
69+
const err = new Error();
70+
err.name = 'Unknown';
71+
72+
handlePasskeyAuthenticationError(err);
73+
74+
expect(mockHandlePasskeyError).toHaveBeenCalledWith(err);
75+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
76+
});
77+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
PasskeyError,
3+
PasskeyErrorCode,
4+
handlePasskeyAuthenticationError,
5+
} from '../../../../../src/client/utils/passkey/errors';
6+
import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError';
7+
import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError';
8+
9+
const mockHandlePasskeyError = jest.mocked(handlePasskeyError);
10+
jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError');
11+
12+
describe('handlePasskeyAuthenticationError', () => {
13+
it('returns early if err is already instanceof PasskeyError', () => {
14+
const err = new PasskeyError({
15+
name: 'PasskeyErrorName',
16+
message: 'Error Message',
17+
});
18+
19+
expect(handlePasskeyAuthenticationError(err)).toBe(err);
20+
});
21+
22+
it('returns new instance of PasskeyError with correct attributes when input error name is NotAllowedError', () => {
23+
const err = new Error();
24+
err.name = 'NotAllowedError';
25+
26+
const { message, recoverySuggestion } =
27+
passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled];
28+
29+
expect(handlePasskeyAuthenticationError(err)).toMatchObject(
30+
new PasskeyError({
31+
name: PasskeyErrorCode.PasskeyAuthenticationCanceled,
32+
message,
33+
recoverySuggestion,
34+
underlyingError: err,
35+
}),
36+
);
37+
});
38+
39+
it('invokes handlePasskeyError when input error does not match expected cases', () => {
40+
const err = new Error();
41+
err.name = 'Unknown';
42+
43+
handlePasskeyAuthenticationError(err);
44+
45+
expect(mockHandlePasskeyError).toHaveBeenCalledWith(err);
46+
});
47+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils';
2+
3+
import {
4+
PasskeyError,
5+
PasskeyErrorCode,
6+
} from '../../../../../src/client/utils/passkey/errors';
7+
import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError.native';
8+
import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError';
9+
import { MockNativeError } from '../../../../mockData';
10+
11+
jest.mock('@aws-amplify/react-native/internals/utils', () => ({
12+
getIsNativeError: jest.fn(() => true),
13+
}));
14+
15+
describe('handlePasskeyError', () => {
16+
it('returns new instance of PasskeyError with correct attributes when input error code is RELYING_PARTY_MISMATCH', () => {
17+
const err = new MockNativeError();
18+
err.code = 'RELYING_PARTY_MISMATCH';
19+
20+
const { message, recoverySuggestion } =
21+
passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch];
22+
23+
expect(handlePasskeyError(err)).toMatchObject(
24+
new PasskeyError({
25+
name: PasskeyErrorCode.RelyingPartyMismatch,
26+
message,
27+
recoverySuggestion,
28+
underlyingError: err,
29+
}),
30+
);
31+
});
32+
33+
it('returns new instance of PasskeyError with correct attributes when input error code is NOT_SUPPORTED', () => {
34+
const err = new MockNativeError();
35+
err.code = 'NOT_SUPPORTED';
36+
37+
const { message, recoverySuggestion } =
38+
passkeyErrorMap[PasskeyErrorCode.PasskeyNotSupported];
39+
40+
expect(handlePasskeyError(err)).toMatchObject(
41+
new PasskeyError({
42+
name: PasskeyErrorCode.PasskeyNotSupported,
43+
message,
44+
recoverySuggestion,
45+
underlyingError: err,
46+
}),
47+
);
48+
});
49+
50+
it('returns unknown PasskeyError when input does not match expected cases', () => {
51+
const err = new MockNativeError();
52+
err.name = 'UNKNOWN';
53+
54+
expect(handlePasskeyError(err)).toMatchObject(
55+
new PasskeyError({
56+
name: AmplifyErrorCode.Unknown,
57+
message: 'An unknown error has occurred.',
58+
underlyingError: err,
59+
}),
60+
);
61+
});
62+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils';
2+
3+
import {
4+
PasskeyError,
5+
PasskeyErrorCode,
6+
} from '../../../../../src/client/utils/passkey/errors';
7+
import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError';
8+
import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError';
9+
10+
describe('handlePasskeyError', () => {
11+
it('returns new instance of PasskeyError with correct attributes when input error name is AbortError', () => {
12+
const err = new Error();
13+
err.name = 'AbortError';
14+
15+
const { message, recoverySuggestion } =
16+
passkeyErrorMap[PasskeyErrorCode.PasskeyOperationAborted];
17+
18+
expect(handlePasskeyError(err)).toMatchObject(
19+
new PasskeyError({
20+
name: PasskeyErrorCode.PasskeyOperationAborted,
21+
message,
22+
recoverySuggestion,
23+
underlyingError: err,
24+
}),
25+
);
26+
});
27+
28+
it('returns new instance of PasskeyError with correct attributes when input error name is SecurityError', () => {
29+
const err = new Error();
30+
err.name = 'SecurityError';
31+
32+
const { message, recoverySuggestion } =
33+
passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch];
34+
35+
expect(handlePasskeyError(err)).toMatchObject(
36+
new PasskeyError({
37+
name: PasskeyErrorCode.RelyingPartyMismatch,
38+
message,
39+
recoverySuggestion,
40+
underlyingError: err,
41+
}),
42+
);
43+
});
44+
45+
it('returns unknown PasskeyError when input does not match expected cases', () => {
46+
const err = new Error();
47+
err.name = 'Unknown';
48+
49+
expect(handlePasskeyError(err)).toMatchObject(
50+
new PasskeyError({
51+
name: AmplifyErrorCode.Unknown,
52+
message: 'An unknown error has occurred.',
53+
underlyingError: err,
54+
}),
55+
);
56+
});
57+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { getIsNativeError } from '@aws-amplify/react-native/internals/utils';
2+
3+
import {
4+
PasskeyError,
5+
PasskeyErrorCode,
6+
} from '../../../../../src/client/utils/passkey/errors';
7+
import { handlePasskeyRegistrationError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyRegistrationError.native';
8+
import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError';
9+
import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError';
10+
import { MockNativeError } from '../../../../mockData';
11+
12+
const mockHandlePasskeyError = jest.mocked(handlePasskeyError);
13+
jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError');
14+
15+
jest.mock('@aws-amplify/react-native/internals/utils', () => ({
16+
getIsNativeError: jest.fn(() => true),
17+
}));
18+
19+
const mockGetIsNativeError = jest.mocked(getIsNativeError);
20+
21+
describe('handlePasskeyRegistrationError', () => {
22+
it('returns early if err is already instanceof PasskeyError', () => {
23+
const err = new PasskeyError({
24+
name: 'PasskeyErrorName',
25+
message: 'Error Message',
26+
});
27+
28+
expect(handlePasskeyRegistrationError(err)).toBe(err);
29+
expect(mockGetIsNativeError).not.toHaveBeenCalled();
30+
});
31+
32+
it('returns new instance of PasskeyError with correct attributes when input error code is FAILED', () => {
33+
const err = new MockNativeError();
34+
err.code = 'FAILED';
35+
36+
const { message, recoverySuggestion } =
37+
passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationFailed];
38+
39+
expect(handlePasskeyRegistrationError(err)).toMatchObject(
40+
new PasskeyError({
41+
name: PasskeyErrorCode.PasskeyRegistrationFailed,
42+
message,
43+
recoverySuggestion,
44+
underlyingError: err,
45+
}),
46+
);
47+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
48+
});
49+
it('returns new instance of PasskeyError with correct attributes when input error code is DUPLICATE', () => {
50+
const err = new MockNativeError();
51+
err.code = 'DUPLICATE';
52+
53+
const { message, recoverySuggestion } =
54+
passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists];
55+
56+
expect(handlePasskeyRegistrationError(err)).toMatchObject(
57+
new PasskeyError({
58+
name: PasskeyErrorCode.PasskeyAlreadyExists,
59+
message,
60+
recoverySuggestion,
61+
underlyingError: err,
62+
}),
63+
);
64+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
65+
});
66+
67+
it('returns new instance of PasskeyError with correct attributes when input error code is CANCELED', () => {
68+
const err = new MockNativeError();
69+
err.code = 'CANCELED';
70+
71+
const { message, recoverySuggestion } =
72+
passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled];
73+
74+
expect(handlePasskeyRegistrationError(err)).toMatchObject(
75+
new PasskeyError({
76+
name: PasskeyErrorCode.PasskeyRegistrationCanceled,
77+
message,
78+
recoverySuggestion,
79+
underlyingError: err,
80+
}),
81+
);
82+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
83+
});
84+
85+
it('invokes handlePasskeyError when input error does not match expected cases', () => {
86+
const err = new Error();
87+
err.name = 'Unknown';
88+
89+
handlePasskeyRegistrationError(err);
90+
91+
expect(mockHandlePasskeyError).toHaveBeenCalledWith(err);
92+
expect(mockGetIsNativeError).toHaveBeenCalledWith(err);
93+
});
94+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
PasskeyError,
3+
PasskeyErrorCode,
4+
handlePasskeyRegistrationError,
5+
} from '../../../../../src/client/utils/passkey/errors';
6+
import { handlePasskeyError } from '../../../../../src/client/utils/passkey/errors/handlePasskeyError';
7+
import { passkeyErrorMap } from '../../../../../src/client/utils/passkey/errors/passkeyError';
8+
9+
const mockHandlePasskeyError = jest.mocked(handlePasskeyError);
10+
jest.mock('../../../../../src/client/utils/passkey/errors/handlePasskeyError');
11+
12+
describe('handlePasskeyRegistrationError', () => {
13+
it('returns early if err is already instanceof PasskeyError', () => {
14+
const err = new PasskeyError({
15+
name: 'PasskeyErrorName',
16+
message: 'Error Message',
17+
});
18+
19+
expect(handlePasskeyRegistrationError(err)).toBe(err);
20+
});
21+
22+
it('returns new instance of PasskeyError with correct attributes when input error code is InvalidStateError', () => {
23+
const err = new Error();
24+
err.name = 'InvalidStateError';
25+
26+
const { message, recoverySuggestion } =
27+
passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists];
28+
29+
expect(handlePasskeyRegistrationError(err)).toMatchObject(
30+
new PasskeyError({
31+
name: PasskeyErrorCode.PasskeyAlreadyExists,
32+
message,
33+
recoverySuggestion,
34+
underlyingError: err,
35+
}),
36+
);
37+
});
38+
39+
it('returns new instance of PasskeyError with correct attributes when input error code is NotAllowedError', () => {
40+
const err = new Error();
41+
err.name = 'NotAllowedError';
42+
43+
const { message, recoverySuggestion } =
44+
passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled];
45+
46+
expect(handlePasskeyRegistrationError(err)).toMatchObject(
47+
new PasskeyError({
48+
name: PasskeyErrorCode.PasskeyRegistrationCanceled,
49+
message,
50+
recoverySuggestion,
51+
underlyingError: err,
52+
}),
53+
);
54+
});
55+
56+
it('invokes handlePasskeyError when input error does not match expected cases', () => {
57+
const err = new Error();
58+
err.name = 'Unknown';
59+
60+
handlePasskeyRegistrationError(err);
61+
62+
expect(mockHandlePasskeyError).toHaveBeenCalledWith(err);
63+
});
64+
});

‎packages/auth/__tests__/client/utils/passkey.test.ts renamed to ‎packages/auth/__tests__/client/utils/passkey/serde.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {
22
deserializeJsonToPkcCreationOptions,
33
serializePkcWithAttestationToJson,
4-
} from '../../../src/client/utils/passkey/serde';
4+
} from '../../../../src/client/utils/passkey/serde';
55
import {
66
passkeyRegistrationRequest,
77
passkeyRegistrationRequestJson,
88
passkeyRegistrationResult,
99
passkeyRegistrationResultJson,
10-
} from '../../mockData';
10+
} from '../../../mockData';
1111

12-
describe('passkey', () => {
12+
describe('passkey serialization / deserialization', () => {
1313
it('serializes pkc into correct json format', () => {
1414
expect(
1515
JSON.stringify(

‎packages/auth/__tests__/mockData.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,7 @@ export const mockUserCredentials = [
423423
CreatedAt: 1582939425,
424424
},
425425
];
426+
427+
export class MockNativeError extends Error {
428+
code?: string;
429+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
export { registerPasskey } from './passkey';
4+
export { getPasskey, registerPasskey } from './passkey';

‎packages/auth/src/client/utils/passkey/errors.ts

Lines changed: 0 additions & 214 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { getIsNativeError } from '@aws-amplify/react-native/internals/utils';
5+
6+
import { handlePasskeyError } from './handlePasskeyError';
7+
import {
8+
PasskeyError,
9+
PasskeyErrorCode,
10+
passkeyErrorMap,
11+
} from './passkeyError';
12+
13+
/**
14+
* Handle Passkey Authentication Errors
15+
*
16+
* @param err unknown
17+
* @returns PasskeyError
18+
*/
19+
export const handlePasskeyAuthenticationError = (
20+
err: unknown,
21+
): PasskeyError => {
22+
if (err instanceof PasskeyError) {
23+
return err;
24+
}
25+
26+
if (getIsNativeError(err)) {
27+
// Passkey Retrieval Failed
28+
if (err.code === 'FAILED') {
29+
const { message, recoverySuggestion } =
30+
passkeyErrorMap[PasskeyErrorCode.PasskeyRetrievalFailed];
31+
32+
return new PasskeyError({
33+
name: PasskeyErrorCode.PasskeyRetrievalFailed,
34+
message,
35+
recoverySuggestion,
36+
underlyingError: err,
37+
});
38+
}
39+
40+
if (err.code === 'CANCELED') {
41+
const { message, recoverySuggestion } =
42+
passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled];
43+
44+
return new PasskeyError({
45+
name: PasskeyErrorCode.PasskeyAuthenticationCanceled,
46+
message,
47+
recoverySuggestion,
48+
underlyingError: err,
49+
});
50+
}
51+
}
52+
53+
return handlePasskeyError(err);
54+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { handlePasskeyError } from './handlePasskeyError';
5+
import {
6+
PasskeyError,
7+
PasskeyErrorCode,
8+
passkeyErrorMap,
9+
} from './passkeyError';
10+
11+
/**
12+
* Handle Passkey Authentication Errors
13+
* https://w3c.github.io/webauthn/#sctn-get-request-exceptions
14+
*
15+
* @param err unknown
16+
* @returns PasskeyError
17+
*/
18+
export const handlePasskeyAuthenticationError = (
19+
err: unknown,
20+
): PasskeyError => {
21+
if (err instanceof PasskeyError) {
22+
return err;
23+
}
24+
25+
if (err instanceof Error) {
26+
if (err.name === 'NotAllowedError') {
27+
const { message, recoverySuggestion } =
28+
passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled];
29+
30+
return new PasskeyError({
31+
name: PasskeyErrorCode.PasskeyAuthenticationCanceled,
32+
message,
33+
recoverySuggestion,
34+
underlyingError: err,
35+
});
36+
}
37+
}
38+
39+
return handlePasskeyError(err);
40+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils';
5+
import { getIsNativeError } from '@aws-amplify/react-native/internals/utils';
6+
7+
import {
8+
PasskeyError,
9+
PasskeyErrorCode,
10+
passkeyErrorMap,
11+
} from './passkeyError';
12+
13+
/**
14+
* Handles Overlapping Passkey Errors Between Registration & Authentication
15+
*
16+
* @param err unknown
17+
* @returns PasskeyError
18+
*/
19+
export const handlePasskeyError = (err: unknown): PasskeyError => {
20+
if (getIsNativeError(err)) {
21+
// Relying Party / Domain Mismatch
22+
if (err.code === 'RELYING_PARTY_MISMATCH') {
23+
const { message, recoverySuggestion } =
24+
passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch];
25+
26+
return new PasskeyError({
27+
name: PasskeyErrorCode.RelyingPartyMismatch,
28+
message,
29+
recoverySuggestion,
30+
underlyingError: err,
31+
});
32+
}
33+
34+
// Not Supported
35+
if (err.code === 'NOT_SUPPORTED') {
36+
const { message, recoverySuggestion } =
37+
passkeyErrorMap[PasskeyErrorCode.PasskeyNotSupported];
38+
39+
return new PasskeyError({
40+
name: PasskeyErrorCode.PasskeyNotSupported,
41+
message,
42+
recoverySuggestion,
43+
underlyingError: err,
44+
});
45+
}
46+
}
47+
48+
return new PasskeyError({
49+
name: AmplifyErrorCode.Unknown,
50+
message: 'An unknown error has occurred.',
51+
underlyingError: err,
52+
});
53+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils';
5+
6+
import {
7+
PasskeyError,
8+
PasskeyErrorCode,
9+
passkeyErrorMap,
10+
} from './passkeyError';
11+
12+
/**
13+
* Handles Overlapping Passkey Errors Between Registration & Authentication
14+
* https://w3c.github.io/webauthn/#sctn-create-request-exceptions
15+
* https://w3c.github.io/webauthn/#sctn-get-request-exceptions
16+
*
17+
* @param err unknown
18+
* @returns PasskeyError
19+
*/
20+
export const handlePasskeyError = (err: unknown): PasskeyError => {
21+
if (err instanceof Error) {
22+
// Passkey Operation Aborted
23+
if (err.name === 'AbortError') {
24+
const { message, recoverySuggestion } =
25+
passkeyErrorMap[PasskeyErrorCode.PasskeyOperationAborted];
26+
27+
return new PasskeyError({
28+
name: PasskeyErrorCode.PasskeyOperationAborted,
29+
message,
30+
recoverySuggestion,
31+
underlyingError: err,
32+
});
33+
}
34+
// Relying Party / Domain Mismatch
35+
if (err.name === 'SecurityError') {
36+
const { message, recoverySuggestion } =
37+
passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch];
38+
39+
return new PasskeyError({
40+
name: PasskeyErrorCode.RelyingPartyMismatch,
41+
message,
42+
recoverySuggestion,
43+
underlyingError: err,
44+
});
45+
}
46+
}
47+
48+
return new PasskeyError({
49+
name: AmplifyErrorCode.Unknown,
50+
message: 'An unknown error has occurred.',
51+
underlyingError: err,
52+
});
53+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { getIsNativeError } from '@aws-amplify/react-native/internals/utils';
5+
6+
import { handlePasskeyError } from './handlePasskeyError';
7+
import {
8+
PasskeyError,
9+
PasskeyErrorCode,
10+
passkeyErrorMap,
11+
} from './passkeyError';
12+
13+
/**
14+
* Handle Passkey Registration Errors
15+
*
16+
* @param err unknown
17+
* @returns PasskeyError
18+
*/
19+
export const handlePasskeyRegistrationError = (err: unknown): PasskeyError => {
20+
if (err instanceof PasskeyError) {
21+
return err;
22+
}
23+
24+
if (getIsNativeError(err)) {
25+
// Passkey Registration Failed
26+
if (err.code === 'FAILED') {
27+
const { message, recoverySuggestion } =
28+
passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationFailed];
29+
30+
return new PasskeyError({
31+
name: PasskeyErrorCode.PasskeyRegistrationFailed,
32+
message,
33+
recoverySuggestion,
34+
underlyingError: err,
35+
});
36+
}
37+
38+
// Duplicate Passkey
39+
if (err.code === 'DUPLICATE') {
40+
const { message, recoverySuggestion } =
41+
passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists];
42+
43+
return new PasskeyError({
44+
name: PasskeyErrorCode.PasskeyAlreadyExists,
45+
message,
46+
recoverySuggestion,
47+
underlyingError: err,
48+
});
49+
}
50+
51+
// User Cancels Ceremony
52+
if (err.code === 'CANCELED') {
53+
const { message, recoverySuggestion } =
54+
passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled];
55+
56+
return new PasskeyError({
57+
name: PasskeyErrorCode.PasskeyRegistrationCanceled,
58+
message,
59+
recoverySuggestion,
60+
underlyingError: err,
61+
});
62+
}
63+
}
64+
65+
return handlePasskeyError(err);
66+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { handlePasskeyError } from './handlePasskeyError';
5+
import {
6+
PasskeyError,
7+
PasskeyErrorCode,
8+
passkeyErrorMap,
9+
} from './passkeyError';
10+
11+
/**
12+
* Handle Passkey Registration Errors
13+
* https://w3c.github.io/webauthn/#sctn-create-request-exceptions
14+
*
15+
* @param err unknown
16+
* @returns PasskeyError
17+
*/
18+
export const handlePasskeyRegistrationError = (err: unknown): PasskeyError => {
19+
if (err instanceof PasskeyError) {
20+
return err;
21+
}
22+
23+
if (err instanceof Error) {
24+
// Duplicate Passkey
25+
if (err.name === 'InvalidStateError') {
26+
const { message, recoverySuggestion } =
27+
passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists];
28+
29+
return new PasskeyError({
30+
name: PasskeyErrorCode.PasskeyAlreadyExists,
31+
message,
32+
recoverySuggestion,
33+
underlyingError: err,
34+
});
35+
}
36+
37+
// User Cancels Ceremony / Generic Catch All
38+
if (err.name === 'NotAllowedError') {
39+
const { message, recoverySuggestion } =
40+
passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled];
41+
42+
return new PasskeyError({
43+
name: PasskeyErrorCode.PasskeyRegistrationCanceled,
44+
message,
45+
recoverySuggestion,
46+
underlyingError: err,
47+
});
48+
}
49+
}
50+
51+
return handlePasskeyError(err);
52+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export { handlePasskeyAuthenticationError } from './handlePasskeyAuthenticationError';
5+
export { handlePasskeyRegistrationError } from './handlePasskeyRegistrationError';
6+
export {
7+
PasskeyError,
8+
PasskeyErrorCode,
9+
assertPasskeyError,
10+
} from './passkeyError';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import {
5+
AmplifyError,
6+
AmplifyErrorMap,
7+
AmplifyErrorParams,
8+
AssertionFunction,
9+
createAssertionFunction,
10+
} from '@aws-amplify/core/internals/utils';
11+
12+
import { NOT_SUPPORTED_RECOVERY_SUGGESTION } from './passkeyErrorPlatformConstants';
13+
14+
export class PasskeyError extends AmplifyError {
15+
constructor(params: AmplifyErrorParams) {
16+
super(params);
17+
18+
// Hack for making the custom error class work when transpiled to es5
19+
// TODO: Delete the following 2 lines after we change the build target to >= es2015
20+
this.constructor = PasskeyError;
21+
Object.setPrototypeOf(this, PasskeyError.prototype);
22+
}
23+
}
24+
25+
export enum PasskeyErrorCode {
26+
// not supported
27+
PasskeyNotSupported = 'PasskeyNotSupported',
28+
// duplicate passkey
29+
PasskeyAlreadyExists = 'PasskeyAlreadyExists',
30+
// misconfigurations
31+
InvalidPasskeyRegistrationOptions = 'InvalidPasskeyRegistrationOptions',
32+
InvalidPasskeyAuthenticationOptions = 'InvalidPasskeyAuthenticationOptions',
33+
RelyingPartyMismatch = 'RelyingPartyMismatch',
34+
// failed credential creation / retrieval
35+
PasskeyRegistrationFailed = 'PasskeyRegistrationFailed',
36+
PasskeyRetrievalFailed = 'PasskeyRetrievalFailed',
37+
// cancel / aborts
38+
PasskeyRegistrationCanceled = 'PasskeyRegistrationCanceled',
39+
PasskeyAuthenticationCanceled = 'PasskeyAuthenticationCanceled',
40+
PasskeyOperationAborted = 'PasskeyOperationAborted',
41+
}
42+
43+
const ABORT_OR_CANCEL_RECOVERY_SUGGESTION =
44+
'User may have canceled the ceremony or another interruption has occurred. Check underlying error for details.';
45+
46+
const MISCONFIGURATION_RECOVERY_SUGGESTION =
47+
'Ensure your user pool is configured to support the WEB_AUTHN as an authentication factor.';
48+
49+
export const passkeyErrorMap: AmplifyErrorMap<PasskeyErrorCode> = {
50+
[PasskeyErrorCode.PasskeyNotSupported]: {
51+
message: 'Passkeys may not be supported on this device.',
52+
recoverySuggestion: NOT_SUPPORTED_RECOVERY_SUGGESTION,
53+
},
54+
[PasskeyErrorCode.InvalidPasskeyRegistrationOptions]: {
55+
message: 'Invalid passkey registration options.',
56+
recoverySuggestion: MISCONFIGURATION_RECOVERY_SUGGESTION,
57+
},
58+
[PasskeyErrorCode.InvalidPasskeyAuthenticationOptions]: {
59+
message: 'Invalid passkey authentication options.',
60+
recoverySuggestion: MISCONFIGURATION_RECOVERY_SUGGESTION,
61+
},
62+
[PasskeyErrorCode.PasskeyRegistrationFailed]: {
63+
message: 'Device failed to create passkey.',
64+
recoverySuggestion: NOT_SUPPORTED_RECOVERY_SUGGESTION,
65+
},
66+
[PasskeyErrorCode.PasskeyRetrievalFailed]: {
67+
message: 'Device failed to retrieve passkey.',
68+
recoverySuggestion:
69+
'Passkeys may not be available on this device. Try an alternative authentication factor like PASSWORD, EMAIL_OTP, or SMS_OTP.',
70+
},
71+
[PasskeyErrorCode.PasskeyAlreadyExists]: {
72+
message: 'Passkey already exists in authenticator.',
73+
recoverySuggestion:
74+
'Proceed with existing passkey or try again after deleting the credential.',
75+
},
76+
[PasskeyErrorCode.PasskeyRegistrationCanceled]: {
77+
message: 'Passkey registration ceremony has been canceled.',
78+
recoverySuggestion: ABORT_OR_CANCEL_RECOVERY_SUGGESTION,
79+
},
80+
[PasskeyErrorCode.PasskeyAuthenticationCanceled]: {
81+
message: 'Passkey authentication ceremony has been canceled.',
82+
recoverySuggestion: ABORT_OR_CANCEL_RECOVERY_SUGGESTION,
83+
},
84+
[PasskeyErrorCode.PasskeyOperationAborted]: {
85+
message: 'Passkey operation has been aborted.',
86+
recoverySuggestion: ABORT_OR_CANCEL_RECOVERY_SUGGESTION,
87+
},
88+
[PasskeyErrorCode.RelyingPartyMismatch]: {
89+
message: 'Relying party does not match current domain.',
90+
recoverySuggestion:
91+
'Ensure relying party identifier matches current domain.',
92+
},
93+
};
94+
95+
export const assertPasskeyError: AssertionFunction<PasskeyErrorCode> =
96+
createAssertionFunction(passkeyErrorMap, PasskeyError);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export const NOT_SUPPORTED_RECOVERY_SUGGESTION =
5+
'Passkeys may not be supported on this device. Ensure your application is running on a supported platform.';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export const NOT_SUPPORTED_RECOVERY_SUGGESTION =
5+
'Passkeys may not be supported on this device. Ensure your application is running in a secure context (HTTPS) and Web Authentication API is supported.';
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils';
4+
import { loadAmplifyRtnPasskeys } from '@aws-amplify/react-native';
55

66
export const getIsPasskeySupported = () => {
7-
throw new PlatformNotSupportedError();
7+
return loadAmplifyRtnPasskeys().getIsPasskeySupported();
88
};
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils';
4+
import { loadAmplifyRtnPasskeys } from '@aws-amplify/react-native';
55

6-
export const getPasskey = async () => {
7-
throw new PlatformNotSupportedError();
6+
import { PasskeyGetOptionsJson, PasskeyGetResultJson } from './types';
7+
import { handlePasskeyAuthenticationError } from './errors';
8+
9+
export const getPasskey = async (
10+
input: PasskeyGetOptionsJson,
11+
): Promise<PasskeyGetResultJson> => {
12+
try {
13+
return await loadAmplifyRtnPasskeys().getPasskey(input);
14+
} catch (err: unknown) {
15+
throw handlePasskeyAuthenticationError(err);
16+
}
817
};
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils';
4+
import { loadAmplifyRtnPasskeys } from '@aws-amplify/react-native';
55

6-
export const registerPasskey = async () => {
7-
throw new PlatformNotSupportedError();
6+
import { PasskeyCreateOptionsJson, PasskeyCreateResultJson } from './types';
7+
import { handlePasskeyRegistrationError } from './errors';
8+
9+
export const registerPasskey = async (
10+
input: PasskeyCreateOptionsJson,
11+
): Promise<PasskeyCreateResultJson> => {
12+
try {
13+
return await loadAmplifyRtnPasskeys().createPasskey(input);
14+
} catch (err: unknown) {
15+
throw handlePasskeyRegistrationError(err);
16+
}
817
};

‎packages/auth/src/client/utils/passkey/types/shared.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ export interface PkcAttestationResponse<T> {
5555
attestationObject: T;
5656
transports: string[];
5757
publicKey?: string;
58-
publicKeyAlgorithm: number;
59-
authenticatorData: T;
58+
publicKeyAlgorithm?: number;
59+
authenticatorData?: T;
6060
}
6161
export interface PasskeyCreateResult {
6262
id: string;
@@ -69,7 +69,7 @@ export interface PasskeyCreateResultJson {
6969
id: string;
7070
rawId: string;
7171
type: string;
72-
clientExtensionResults: {
72+
clientExtensionResults?: {
7373
appId?: boolean;
7474
credProps?: { rk?: boolean };
7575
hmacCreateSecret?: boolean;
@@ -121,7 +121,7 @@ export interface PasskeyGetResultJson {
121121
id: string;
122122
rawId: string;
123123
type: string;
124-
clientExtensionResults: {
124+
clientExtensionResults?: {
125125
appId?: boolean;
126126
credProps?: { rk?: boolean };
127127
hmacCreateSecret?: boolean;

‎packages/auth/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ export {
8888
JWT,
8989
} from '@aws-amplify/core';
9090

91-
export { associateWebAuthnCredential } from './client/apis';
92-
9391
export {
92+
associateWebAuthnCredential,
9493
listWebAuthnCredentials,
9594
deleteWebAuthnCredential,
9695
} from './client/apis';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@aws-amplify/react-native/internals/utils",
3+
"types": "../../dist/esm/utils/index.d.ts",
4+
"main": "../../dist/cjs/utils/index.js",
5+
"module": "../../dist/esm/utils/index.mjs",
6+
"react-native": "../../dist/cjs/utils/index.js",
7+
"sideEffects": false
8+
}

‎packages/react-native/package.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,27 @@
1010
"publishConfig": {
1111
"access": "public"
1212
},
13+
"exports": {
14+
".": {
15+
"react-native": "./dist/cjs/index.js",
16+
"types": "./dist/esm/index.d.ts",
17+
"import": "./dist/esm/index.mjs",
18+
"require": "./dist/cjs/index.js"
19+
},
20+
"./internals/utils": {
21+
"react-native": "./dist/cjs/utils/index.js",
22+
"types": "./dist/esm/utils/index.d.ts",
23+
"import": "./dist/esm/utils/index.mjs",
24+
"require": "./dist/cjs/utils/index.js"
25+
},
26+
"./package.json": "./package.json"
27+
},
1328
"scripts": {
29+
"prepare:ios": "cd example && npx pod-install",
30+
"prepare:android": "echo 'no-op'",
1431
"test": "echo 'no-op'",
15-
"test:android": "./android/gradlew test -p ./android",
32+
"test:ios": "echo 'no-op'",
33+
"test:android": "cd ./example/android && ./gradlew test -i",
1634
"build-with-test": "npm run clean && npm test && tsc",
1735
"build:esm-cjs": "rollup --forceExit -c rollup.config.mjs",
1836
"build:watch": "npm run build:esm-cjs -- --watch",
@@ -33,6 +51,7 @@
3351
"react-native-get-random-values": ">=1.8.0"
3452
},
3553
"devDependencies": {
54+
"@aws-amplify/rtn-passkeys": "*",
3655
"@aws-amplify/rtn-push-notification": "1.2.34",
3756
"@aws-amplify/rtn-web-browser": "1.1.3",
3857
"@react-native-async-storage/async-storage": "^1.17.12",

‎packages/react-native/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export {
88
getDeviceName,
99
} from './apis';
1010
export {
11+
loadAmplifyRtnPasskeys,
1112
loadAmplifyPushNotification,
1213
loadAmplifyWebBrowser,
1314
loadAsyncStorage,

‎packages/react-native/src/moduleLoaders/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
export { loadAmplifyRtnPasskeys } from './loadAmplifyRtnPasskeys';
45
export { loadAmplifyPushNotification } from './loadAmplifyPushNotification';
56
export { loadAmplifyWebBrowser } from './loadAmplifyWebBrowser';
67
export { loadAsyncStorage } from './loadAsyncStorage';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { AmplifyRtnPasskeysModule } from '@aws-amplify/rtn-passkeys';
5+
6+
export const loadAmplifyRtnPasskeys = () => {
7+
try {
8+
// metro bundler requires static string for loading module.
9+
// See: https://facebook.github.io/metro/docs/configuration/#dynamicdepsinpackages
10+
const module = require('@aws-amplify/rtn-passkeys')?.module;
11+
if (module) {
12+
return module as AmplifyRtnPasskeysModule;
13+
}
14+
15+
throw new Error(
16+
'Ensure `@aws-amplify/rtn-passkeys` is installed and linked.',
17+
);
18+
} catch (e) {
19+
// The error parsing logic cannot be extracted with metro as the `require`
20+
// would be confused when there is a `import` in the same file importing
21+
// another module and that causes an error
22+
const message = (e as Error).message.replace(
23+
/undefined/g,
24+
'@aws-amplify/rtn-passkeys',
25+
);
26+
throw new Error(message);
27+
}
28+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export interface NativeError extends Error {
5+
code: string;
6+
domain?: string;
7+
userInfo?: Record<string, unknown>;
8+
nativeStackIOS?: never[];
9+
nativeStackAndroid?: Record<string, unknown>[];
10+
}
11+
12+
export const getIsNativeError = (err: unknown): err is NativeError => {
13+
return (
14+
err instanceof Error &&
15+
'code' in err &&
16+
('nativeStackIOS' in err || 'nativeStackAndroid' in err)
17+
);
18+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export { type NativeError, getIsNativeError } from './getIsNativeError';

0 commit comments

Comments
 (0)
Please sign in to comment.