Skip to content

Commit 4ba0fb5

Browse files
authored
fix(authenticator): migrate totpSecretCode generation to state machine (#3333)
1 parent b855475 commit 4ba0fb5

File tree

30 files changed

+405
-329
lines changed

30 files changed

+405
-329
lines changed

.changeset/bright-worms-explain.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@aws-amplify/ui-react-core": patch
3+
"@aws-amplify/ui-react-native": patch
4+
"@aws-amplify/ui-react": patch
5+
"@aws-amplify/ui": patch
6+
"@aws-amplify/ui-vue": patch
7+
"@aws-amplify/ui-angular": patch
8+
---
9+
10+
fix(authenticator): migrate totpSecretCode generation to state machine

packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ <h3 class="amplify-heading amplify-heading--3">{{ this.headerText }}</h3>
1717
height="228"
1818
/>
1919
<div class="amplify-flex" data-amplify-copy>
20-
<div>{{ secretKey }}</div>
20+
<div>{{ totpSecretCode }}</div>
2121
<div data-amplify-copy-svg (click)="copyText()">
2222
<div data-amplify-copy-tooltip>{{ copyTextLabel }}</div>
2323
<svg

packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.spec.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import { BaseFormFieldsComponent } from '../base-form-fields/base-form-fields.co
66
import { FormFieldComponent } from '../form-field/form-field.component';
77
import { ButtonComponent } from '../../../../primitives/button/button.component';
88
import { TestBed, ComponentFixture } from '@angular/core/testing';
9-
import QRCode from 'qrcode';
10-
import { Auth } from 'aws-amplify';
9+
1110
import { MockComponent } from 'ng-mocks';
12-
import { getTotpCodeURL } from '@aws-amplify/ui';
1311

1412
const mockUser = { username: 'username' };
1513
const mockContext = {
@@ -18,16 +16,8 @@ const mockContext = {
1816
user: mockUser,
1917
};
2018

21-
const DEFAULT_TOTP_ISSUER = 'AWSCognito';
22-
const SECRET_KEY = 'secretKey';
23-
24-
const setupTOTPSpy = jest.spyOn(Auth, 'setupTOTP');
25-
26-
const toDataURLSpy = jest.spyOn(QRCode, 'toDataURL');
27-
2819
describe('SetupTotpComponent', () => {
2920
let fixture: ComponentFixture<SetupTotpComponent>;
30-
let component: SetupTotpComponent;
3121

3222
beforeEach(async () => {
3323
jest.resetAllMocks();
@@ -44,6 +34,7 @@ describe('SetupTotpComponent', () => {
4434
submitForm: jest.fn(),
4535
context: jest.fn().mockReturnValue({}),
4636
slotContext: jest.fn().mockReturnValue({}),
37+
totpSecretCode: 'Keep it quiet!',
4738
};
4839

4940
await TestBed.configureTestingModule({
@@ -61,27 +52,9 @@ describe('SetupTotpComponent', () => {
6152
}).compileComponents();
6253

6354
fixture = TestBed.createComponent(SetupTotpComponent);
64-
component = fixture.componentInstance;
6555
});
56+
6657
it('successfully mounts', () => {
6758
expect(fixture).toBeTruthy();
6859
});
69-
70-
it('validate generateQR Code generates correct code', async () => {
71-
setupTOTPSpy.mockResolvedValue(SECRET_KEY);
72-
const defaultTotpCode = getTotpCodeURL(
73-
DEFAULT_TOTP_ISSUER,
74-
mockUser.username,
75-
SECRET_KEY
76-
);
77-
await fixture.detectChanges();
78-
79-
expect(setupTOTPSpy).toHaveBeenCalledTimes(1);
80-
expect(setupTOTPSpy).toHaveBeenCalledWith(mockUser);
81-
82-
await fixture.detectChanges();
83-
84-
expect(toDataURLSpy).toHaveBeenCalledTimes(1);
85-
expect(toDataURLSpy).toHaveBeenCalledWith(defaultTotpCode);
86-
});
8760
});

packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, HostBinding, OnInit } from '@angular/core';
22
import QRCode from 'qrcode';
3-
import { Auth, Logger } from 'aws-amplify';
3+
import { Logger } from 'aws-amplify';
44
import {
55
FormFieldsArray,
66
getActorContext,
@@ -29,7 +29,7 @@ export class SetupTotpComponent implements OnInit {
2929
@HostBinding('attr.data-amplify-authenticator-setup-totp') dataAttr = '';
3030
public headerText = getSetupTOTPText();
3131
public qrCodeSource = '';
32-
public secretKey = '';
32+
public totpSecretCode = '';
3333
public copyTextLabel = getCopyText();
3434

3535
// translated texts
@@ -48,15 +48,19 @@ export class SetupTotpComponent implements OnInit {
4848
}
4949

5050
async generateQRCode() {
51-
// TODO: This should be handled in core.
52-
const state = this.authenticator.authState;
53-
const actorContext = getActorContext(state) as SignInContext;
54-
const { user, formFields } = actorContext;
51+
const { authState: state, totpSecretCode, user } = this.authenticator;
52+
const { formFields } = getActorContext(state) as SignInContext;
5553
const { totpIssuer = 'AWSCognito', totpUsername = user?.username } =
5654
formFields?.setupTOTP?.QR ?? {};
55+
56+
this.totpSecretCode = totpSecretCode;
57+
5758
try {
58-
this.secretKey = await Auth.setupTOTP(user);
59-
const totpCode = getTotpCodeURL(totpIssuer, totpUsername, this.secretKey);
59+
const totpCode = getTotpCodeURL(
60+
totpIssuer,
61+
totpUsername,
62+
this.totpSecretCode
63+
);
6064

6165
logger.info('totp code was generated:', totpCode);
6266
this.qrCodeSource = await QRCode.toDataURL(totpCode);
@@ -77,7 +81,7 @@ export class SetupTotpComponent implements OnInit {
7781
}
7882

7983
copyText(): void {
80-
navigator.clipboard.writeText(this.secretKey);
84+
navigator.clipboard.writeText(this.totpSecretCode);
8185
this.copyTextLabel = getCopiedText();
8286
}
8387
}

packages/angular/projects/ui-angular/src/lib/services/authenticator.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ export class AuthenticatorService implements OnDestroy {
8383
return this._facade?.codeDeliveryDetails;
8484
}
8585

86+
public get totpSecretCode() {
87+
return this._facade?.totpSecretCode;
88+
}
89+
8690
/**
8791
* Service facades
8892
*/

packages/react-core/src/Authenticator/hooks/types.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
LegacyFormFieldOptions,
77
} from '@aws-amplify/ui';
88

9+
import { UseAuthenticator } from './useAuthenticator';
10+
911
export type AuthenticatorRouteComponentKey =
1012
| 'confirmResetPassword'
1113
| 'confirmSignIn'
@@ -31,8 +33,6 @@ export type AuthenticatorMachineContextKey = keyof AuthenticatorMachineContext;
3133
export type AuthenticatorRouteComponentName =
3234
Capitalize<AuthenticatorRouteComponentKey>;
3335

34-
export type GetTotpSecretCode = () => Promise<string>;
35-
3636
interface HeaderProps {
3737
children?: React.ReactNode;
3838
}
@@ -42,8 +42,8 @@ interface FooterProps {
4242
}
4343

4444
type FormFieldsProps = {
45-
isPending: AuthenticatorMachineContext['isPending'];
46-
validationErrors?: AuthenticatorMachineContext['validationErrors'];
45+
isPending: UseAuthenticator['isPending'];
46+
validationErrors?: UseAuthenticator['validationErrors'];
4747
};
4848

4949
export type FooterComponent<Props = {}> = React.ComponentType<
@@ -70,74 +70,74 @@ export interface ComponentSlots<FieldType = {}> {
7070
* Common component prop types used for both RWA and RNA implementations
7171
*/
7272
export type CommonRouteProps = {
73-
error?: AuthenticatorMachineContext['error'];
74-
isPending: AuthenticatorMachineContext['isPending'];
75-
handleBlur: AuthenticatorMachineContext['updateBlur'];
76-
handleChange: AuthenticatorMachineContext['updateForm'];
77-
handleSubmit: AuthenticatorMachineContext['submitForm'];
73+
error?: UseAuthenticator['error'];
74+
isPending: UseAuthenticator['isPending'];
75+
handleBlur: UseAuthenticator['updateBlur'];
76+
handleChange: UseAuthenticator['updateForm'];
77+
handleSubmit: UseAuthenticator['submitForm'];
7878
};
7979

8080
/**
8181
* Base Route component props
8282
*/
8383
export type ConfirmResetPasswordBaseProps<FieldType = {}> = {
84-
resendCode: AuthenticatorMachineContext['resendCode'];
85-
validationErrors?: AuthenticatorMachineContext['validationErrors'];
84+
resendCode: UseAuthenticator['resendCode'];
85+
validationErrors?: UseAuthenticator['validationErrors'];
8686
} & CommonRouteProps &
8787
ComponentSlots<FieldType>;
8888

8989
export type ConfirmSignInBaseProps<FieldType = {}> = {
9090
challengeName: AuthChallengeName;
91-
toSignIn: AuthenticatorMachineContext['toSignIn'];
91+
toSignIn: UseAuthenticator['toSignIn'];
9292
} & CommonRouteProps &
9393
ComponentSlots<FieldType>;
9494

9595
export type ConfirmSignUpBaseProps<FieldType = {}> = {
96-
codeDeliveryDetails: AuthenticatorMachineContext['codeDeliveryDetails'];
97-
resendCode: AuthenticatorMachineContext['resendCode'];
96+
codeDeliveryDetails: UseAuthenticator['codeDeliveryDetails'];
97+
resendCode: UseAuthenticator['resendCode'];
9898
} & CommonRouteProps &
9999
ComponentSlots<FieldType>;
100100

101101
export type ConfirmVerifyUserProps<FieldType = {}> = {
102-
skipVerification: AuthenticatorMachineContext['skipVerification'];
102+
skipVerification: UseAuthenticator['skipVerification'];
103103
} & CommonRouteProps &
104104
ComponentSlots<FieldType>;
105105

106106
export type ForceResetPasswordBaseProps<FieldType = {}> = {
107-
toSignIn: AuthenticatorMachineContext['toSignIn'];
108-
validationErrors?: AuthenticatorMachineContext['validationErrors'];
107+
toSignIn: UseAuthenticator['toSignIn'];
108+
validationErrors?: UseAuthenticator['validationErrors'];
109109
} & CommonRouteProps &
110110
ComponentSlots<FieldType>;
111111

112112
export type ResetPasswordBaseProps<FieldType = {}> = {
113-
toSignIn: AuthenticatorMachineContext['toSignIn'];
113+
toSignIn: UseAuthenticator['toSignIn'];
114114
} & CommonRouteProps &
115115
ComponentSlots<FieldType>;
116116

117117
export type SetupTOTPBaseProps<FieldType = {}> = {
118-
getTotpSecretCode: GetTotpSecretCode;
119-
toSignIn: AuthenticatorMachineContext['toSignIn'];
118+
toSignIn: UseAuthenticator['toSignIn'];
119+
totpSecretCode: UseAuthenticator['totpSecretCode'];
120120
} & CommonRouteProps &
121121
ComponentSlots<FieldType>;
122122

123123
export type SignInBaseProps<FieldType = {}> = {
124124
hideSignUp?: boolean;
125-
toFederatedSignIn: AuthenticatorMachineContext['toFederatedSignIn'];
126-
toResetPassword: AuthenticatorMachineContext['toResetPassword'];
127-
toSignUp: AuthenticatorMachineContext['toSignUp'];
125+
toFederatedSignIn: UseAuthenticator['toFederatedSignIn'];
126+
toResetPassword: UseAuthenticator['toResetPassword'];
127+
toSignUp: UseAuthenticator['toSignUp'];
128128
} & CommonRouteProps &
129129
ComponentSlots<FieldType>;
130130

131131
export type SignUpBaseProps<FieldType = {}> = {
132132
hideSignIn?: boolean;
133-
toFederatedSignIn: AuthenticatorMachineContext['toFederatedSignIn'];
134-
toSignIn: AuthenticatorMachineContext['toSignIn'];
135-
validationErrors?: AuthenticatorMachineContext['validationErrors'];
133+
toFederatedSignIn: UseAuthenticator['toFederatedSignIn'];
134+
toSignIn: UseAuthenticator['toSignIn'];
135+
validationErrors?: UseAuthenticator['validationErrors'];
136136
} & CommonRouteProps &
137137
ComponentSlots<FieldType>;
138138

139139
export type VerifyUserProps<FieldType = {}> = {
140-
skipVerification: AuthenticatorMachineContext['skipVerification'];
140+
skipVerification: UseAuthenticator['skipVerification'];
141141
} & CommonRouteProps &
142142
ComponentSlots<FieldType>;
143143

packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const getTotpSecretCode = jest.fn();
1414
const hasValidationErrors = false;
1515
const initializeMachine = jest.fn();
1616
const isPending = false;
17+
const QRFields = null;
1718
const resendCode = jest.fn();
1819
const route = 'idle';
1920
const skipVerification = jest.fn();
@@ -24,6 +25,7 @@ const toFederatedSignIn = jest.fn();
2425
const toResetPassword = jest.fn();
2526
const toSignIn = jest.fn();
2627
const toSignUp = jest.fn();
28+
const totpSecretCode = null;
2729
const unverifiedContactMethods = {};
2830
const updateBlur = jest.fn();
2931
const updateForm = jest.fn();
@@ -53,6 +55,7 @@ export const mockMachineContext: AuthenticatorMachineContext = {
5355
socialProviders,
5456
toFederatedSignIn,
5557
toResetPassword,
58+
totpSecretCode,
5659
unverifiedContactMethods,
5760
validationErrors,
5861
};
@@ -61,4 +64,5 @@ export const mockUseAuthenticatorOutput: UseAuthenticator = {
6164
...mockMachineContext,
6265
fields,
6366
getTotpSecretCode,
64-
} as unknown as UseAuthenticator;
67+
QRFields,
68+
};

packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,31 @@
22

33
exports[`useAuthenticator returns the expected values 1`] = `
44
Object {
5-
"QRFields": undefined,
5+
"QRFields": null,
66
"authStatus": "authenticated",
77
"codeDeliveryDetails": Object {},
88
"error": undefined,
99
"fields": undefined,
1010
"getTotpSecretCode": undefined,
1111
"hasValidationErrors": false,
12-
"initializeMachine": [Function],
12+
"initializeMachine": [MockFunction],
1313
"isPending": false,
14-
"resendCode": [Function],
14+
"resendCode": [MockFunction],
1515
"route": "idle",
16-
"signOut": [Function],
17-
"skipVerification": [Function],
16+
"signOut": [MockFunction],
17+
"skipVerification": [MockFunction],
1818
"socialProviders": Array [],
19-
"submitForm": [Function],
20-
"toFederatedSignIn": [Function],
21-
"toResetPassword": [Function],
22-
"toSignIn": [Function],
23-
"toSignUp": [Function],
19+
"submitForm": [MockFunction],
20+
"toFederatedSignIn": [MockFunction],
21+
"toResetPassword": [MockFunction],
22+
"toSignIn": [MockFunction],
23+
"toSignUp": [MockFunction],
24+
"totpSecretCode": null,
2425
"unverifiedContactMethods": Object {
2526
"email": "test#example.com",
2627
},
27-
"updateBlur": [Function],
28-
"updateForm": [Function],
28+
"updateBlur": [MockFunction],
29+
"updateForm": [MockFunction],
2930
"user": Object {},
3031
"validationErrors": undefined,
3132
}

0 commit comments

Comments
 (0)