Skip to content

Commit

Permalink
feat(auth): passwordless (aws-amplify#14032)
Browse files Browse the repository at this point in the history
* chore: disable dependency review

* feat(auth): associateWebAuthnCredential API (#1)

* feat(auth): signIn with a webauthn credential  (#3)

wip

* feat(auth): listWebAuthnCredentials API (#6)

* feat(auth): deleteWebAuthnCredential API (aws-amplify#8)

* feat(auth): Added signInWithUserAuth for password-less Sign-In (#2)

* feat(auth): Add USER_AUTH flow in Sign Up logic (aws-amplify#11)

* feat(auth): enable autoSignIn support for passwordless (aws-amplify#7)

* tmp disable code ql

* handle SMS_OTP sign in result

* cache session from signup and confirmsignup both

* add getSignInResult test

* bundle size tests

* feat(passwordless): refactor to support new Cognito API changes (aws-amplify#14)

* refactor to support new APIs

* bundle size updates

* update exception mapping (aws-amplify#15)

* feat(auth): passwordless webauthn ceremony errors (aws-amplify#16)

* update exception mapping

* update passkey error handling

* update tests

* bundle size tests

* simplify language

* refine error messages

* fix(auth): clear auto sign in store on sign in (aws-amplify#18)

* fix(auth): clear auto sign in store on sign in

* add unit test

* feat(auth): refactor foundational APIs to not access singleton (aws-amplify#17)

* enable ssr list and delete web authn credentials

* update unit tests

* add foundation tests

* revert: expose server APIs

* feat(auth): passwordless - enable test specs / push trigger (aws-amplify#19)

* enable test specs / push trigger

* check for PublicKeyCredential

* bundle size tests

* fix recovery suggestion language

* align assertion with expected type

* fix tsdocs

* bundle size updates

* fix(auth): passwordless pr feedback (aws-amplify#22)

* callout in ts docs for password requirement

* unify callback and store reset for autosignin

* comment for clarity

* enable integ tests

* fix: set active username after auth attempt to maintain consistent user context

* temporarily run single test spec per environment

* reset push integ yml

---------

Co-authored-by: Parker Scanlon <69879391+scanlonp@users.noreply.github.com>
Co-authored-by: yuhengshs <yuhengsh@gmail.com>
  • Loading branch information
3 people authored Nov 23, 2024
1 parent ab33a03 commit 68c7f6f
Show file tree
Hide file tree
Showing 99 changed files with 5,471 additions and 283 deletions.
61 changes: 61 additions & 0 deletions .github/integ-config/integ-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -929,3 +929,64 @@ tests:
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: mfa-setup
- test_name: integ_next_passwordless_auto_sign_in
desc: 'passwordless auto sign in with session'
framework: next
category: auth
sample_name: [mfa]
spec: passwordless/auto-sign-in
# browser: *minimal_browser_list
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-autosignin
- test_name: integ_next_passwordless_first_factor_selection
desc: 'passwordless sign in with first factor selection'
framework: next
category: auth
sample_name: [mfa]
spec: passwordless/first-factor-selection
# browser: *minimal_browser_list
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-ffselect
- test_name: integ_next_passwordless_preferred_challenge
desc: 'passwordless sign in with preferred challenge'
framework: next
category: auth
sample_name: [mfa]
spec: passwordless/preferred-challenge
# browser: *minimal_browser_list
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-prefchal
- test_name: integ_next_passwordless_sign_up
desc: 'passwordless sign up'
framework: next
category: auth
sample_name: [mfa]
spec: passwordless/sign-up
# browser: *minimal_browser_list
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-signup
- test_name: integ_next_passwordless_misc
desc: 'passwordless miscellaneous flows'
framework: next
category: auth
sample_name: [mfa]
spec: passwordless/miscellaneous
# browser: *minimal_browser_list
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-misc
- test_name: integ_next_passwordless_webauthn
desc: 'passwordless webauthn sign in and lifecycle management'
framework: next
category: auth
sample_name: [mfa]
spec: passwordless/webauthn
# chrome only
# https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-webauthn
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { Amplify, fetchAuthSession } from '@aws-amplify/core';
import { decodeJWT } from '@aws-amplify/core/internals/utils';

import {
createCompleteWebAuthnRegistrationClient,
createStartWebAuthnRegistrationClient,
} from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
import {
PasskeyError,
PasskeyErrorCode,
} from '../../../src/client/utils/passkey/errors';
import { associateWebAuthnCredential } from '../../../src/client/apis/associateWebAuthnCredential';
import {
passkeyCredentialCreateOptions,
passkeyRegistrationResult,
} from '../../mockData';
import { serializePkcWithAttestationToJson } from '../../../src/client/utils/passkey/serde';
import * as utils from '../../../src/client/utils';
import { getIsPasskeySupported } from '../../../src/client/utils/passkey/getIsPasskeySupported';
import { setUpGetConfig } from '../../providers/cognito/testUtils/setUpGetConfig';
import { mockAccessToken } from '../../providers/cognito/testUtils/data';
import {
assertCredentialIsPkcWithAuthenticatorAssertionResponse,
assertCredentialIsPkcWithAuthenticatorAttestationResponse,
} from '../../../src/client/utils/passkey/types';

jest.mock('@aws-amplify/core', () => ({
...(jest.createMockFromModule('@aws-amplify/core') as object),
Amplify: { getConfig: jest.fn(() => ({})) },
}));
jest.mock('@aws-amplify/core/internals/utils', () => ({
...jest.requireActual('@aws-amplify/core/internals/utils'),
isBrowser: jest.fn(() => false),
}));
jest.mock(
'../../../src/foundation/factories/serviceClients/cognitoIdentityProvider',
);
jest.mock('../../../src/providers/cognito/factories');

jest.mock('../../../src/client/utils/passkey/getIsPasskeySupported');
jest.mock('../../../src/client/utils/passkey/types', () => ({
...jest.requireActual('../../../src/client/utils/passkey/types'),
assertCredentialIsPkcWithAuthenticatorAssertionResponse: jest.fn(),
assertCredentialIsPkcWithAuthenticatorAttestationResponse: jest.fn(),
}));

Object.assign(navigator, {
credentials: {
create: jest.fn(),
},
});

describe('associateWebAuthnCredential', () => {
const navigatorCredentialsCreateSpy = jest.spyOn(
navigator.credentials,
'create',
);
const registerPasskeySpy = jest.spyOn(utils, 'registerPasskey');

const mockFetchAuthSession = jest.mocked(fetchAuthSession);

const mockGetIsPasskeySupported = jest.mocked(getIsPasskeySupported);

const mockStartWebAuthnRegistration = jest.fn();
const mockCreateStartWebAuthnRegistrationClient = jest.mocked(
createStartWebAuthnRegistrationClient,
);

const mockCompleteWebAuthnRegistration = jest.fn();
const mockCreateCompleteWebAuthnRegistrationClient = jest.mocked(
createCompleteWebAuthnRegistrationClient,
);

const mockAssertCredentialIsPkcWithAuthenticatorAssertionResponse =
jest.mocked(assertCredentialIsPkcWithAuthenticatorAssertionResponse);
const mockAssertCredentialIsPkcWithAuthenticatorAttestationResponse =
jest.mocked(assertCredentialIsPkcWithAuthenticatorAttestationResponse);

beforeAll(() => {
setUpGetConfig(Amplify);
mockFetchAuthSession.mockResolvedValue({
tokens: { accessToken: decodeJWT(mockAccessToken) },
});
mockCreateStartWebAuthnRegistrationClient.mockReturnValue(
mockStartWebAuthnRegistration,
);
mockCreateCompleteWebAuthnRegistrationClient.mockReturnValue(
mockCompleteWebAuthnRegistration,
);
mockCompleteWebAuthnRegistration.mockImplementation(() => ({
CredentialId: '12345',
}));

navigatorCredentialsCreateSpy.mockResolvedValue(passkeyRegistrationResult);

mockGetIsPasskeySupported.mockReturnValue(true);
mockAssertCredentialIsPkcWithAuthenticatorAssertionResponse.mockImplementation(
() => undefined,
);
mockAssertCredentialIsPkcWithAuthenticatorAttestationResponse.mockImplementation(
() => undefined,
);
});

afterEach(() => {
mockFetchAuthSession.mockClear();
mockStartWebAuthnRegistration.mockClear();
navigatorCredentialsCreateSpy.mockClear();
});

it('should pass the correct service options when retrieving credential creation options', async () => {
mockStartWebAuthnRegistration.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

await associateWebAuthnCredential();

expect(mockStartWebAuthnRegistration).toHaveBeenCalledWith(
{
region: 'us-west-2',
userAgentValue: expect.any(String),
},
{
AccessToken: mockAccessToken,
},
);
});

it('should pass the correct service options when verifying a credential', async () => {
mockStartWebAuthnRegistration.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

await associateWebAuthnCredential();

expect(mockCompleteWebAuthnRegistration).toHaveBeenCalledWith(
{
region: 'us-west-2',
userAgentValue: expect.any(String),
},
{
AccessToken: mockAccessToken,
Credential: serializePkcWithAttestationToJson(
passkeyRegistrationResult,
),
},
);
});

it('should call the registerPasskey function with correct input', async () => {
mockStartWebAuthnRegistration.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

await associateWebAuthnCredential();

expect(registerPasskeySpy).toHaveBeenCalledWith(
passkeyCredentialCreateOptions,
);

expect(navigatorCredentialsCreateSpy).toHaveBeenCalled();
});

it('should throw an error when service returns empty credential creation options', async () => {
expect.assertions(2);

mockStartWebAuthnRegistration.mockImplementation(() => ({
CredentialCreationOptions: undefined,
}));

try {
await associateWebAuthnCredential();
} catch (error: any) {
expect(error).toBeInstanceOf(PasskeyError);
expect(error.name).toBe(
PasskeyErrorCode.InvalidPasskeyRegistrationOptions,
);
}
});

it('should throw an error when passkeys are not supported', async () => {
expect.assertions(2);

mockStartWebAuthnRegistration.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

mockGetIsPasskeySupported.mockReturnValue(false);

try {
await associateWebAuthnCredential();
} catch (error: any) {
expect(error).toBeInstanceOf(PasskeyError);
expect(error.name).toBe(PasskeyErrorCode.PasskeyNotSupported);
}
});
});
Loading

0 comments on commit 68c7f6f

Please sign in to comment.