Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): passwordless #14032

Merged
merged 27 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3dbc288
chore: disable dependency review
jjarvisp Oct 7, 2024
a39c93f
feat(auth): associateWebAuthnCredential API (#1)
jjarvisp Oct 1, 2024
048eff1
feat(auth): signIn with a webauthn credential (#3)
jjarvisp Oct 28, 2024
622b641
feat(auth): listWebAuthnCredentials API (#6)
scanlonp Oct 29, 2024
f0f2c7c
feat(auth): deleteWebAuthnCredential API (#8)
scanlonp Oct 29, 2024
036a4a6
feat(auth): Added signInWithUserAuth for password-less Sign-In (#2)
yuhengshs Oct 7, 2024
9a8ab67
feat(auth): Add USER_AUTH flow in Sign Up logic (#11)
jjarvisp Oct 31, 2024
6e4f394
feat(auth): enable autoSignIn support for passwordless (#7)
jjarvisp Oct 31, 2024
b6e092c
tmp disable code ql
jjarvisp Nov 5, 2024
c838e1f
handle SMS_OTP sign in result
jjarvisp Nov 5, 2024
0589210
cache session from signup and confirmsignup both
jjarvisp Nov 5, 2024
9a3faf2
add getSignInResult test
jjarvisp Nov 5, 2024
a7cad01
bundle size tests
jjarvisp Nov 6, 2024
980d3b0
Merge pull request #13 from jjarvisp/feat/pwless-testing
jjarvisp Nov 6, 2024
cac2f28
feat(passwordless): refactor to support new Cognito API changes (#14)
jjarvisp Nov 7, 2024
c5b42e0
update exception mapping (#15)
jjarvisp Nov 12, 2024
b6b474c
feat(auth): passwordless webauthn ceremony errors (#16)
jjarvisp Nov 12, 2024
19519f2
fix(auth): clear auto sign in store on sign in (#18)
jjarvisp Nov 19, 2024
58a8650
feat(auth): refactor foundational APIs to not access singleton (#17)
jjarvisp Nov 19, 2024
34c5743
feat(auth): passwordless - enable test specs / push trigger (#19)
jjarvisp Nov 20, 2024
437fc68
Merge branch main into feat/passwordless
jjarvisp Nov 20, 2024
6fadcdf
bundle size updates
jjarvisp Nov 21, 2024
b253166
fix(auth): passwordless pr feedback (#22)
jjarvisp Nov 22, 2024
b66249d
enable integ tests
jjarvisp Nov 22, 2024
8b5320d
fix: set active username after auth attempt to maintain consistent us…
yuhengshs Nov 22, 2024
3851686
temporarily run single test spec per environment
jjarvisp Nov 22, 2024
d2d3fc5
reset push integ yml
jjarvisp Nov 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion .github/workflows/push-integ-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ concurrency:
on:
push:
branches:
- replace-with-your-branch
- feat/passwordless
jjarvisp marked this conversation as resolved.
Show resolved Hide resolved

jobs:
e2e:
Expand Down
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
Loading