Skip to content

Commit d1f6570

Browse files
authored
fix(authenticator): handle SIGN_OUT while resolving the current user (#7111)
The `idle` and `getCurrentUser` states only declare handlers for their invoke result, and `SIGN_OUT` is not handled at the machine root, so a sign out occurring while `handleGetCurrentUser` is in flight is silently dropped. If the in-flight call then resolves with the cached user, the machine settles in `authenticated` with a stale user after a successful sign out. `useAuthenticator().signOut()` is also a no-op until the machine leaves `idle`. Handle `SIGN_OUT` in both states by transitioning to `signOut`. Exiting the state cancels the in-flight invoke, so the stale result can no longer be applied. Fixes #7110
1 parent 3c6ebf6 commit d1f6570

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

.changeset/tidy-moons-refuse.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@aws-amplify/ui': patch
3+
---
4+
5+
fix(authenticator): handle SIGN_OUT while resolving the current user
6+
7+
The `idle` and `getCurrentUser` states only handled their invoke result, so a `SIGN_OUT` arriving while `handleGetCurrentUser` was in flight was silently dropped. Signing out during startup could leave the machine `authenticated` with a stale user, and `useAuthenticator().signOut()` was a no-op until the machine left `idle`. Both states now transition to `signOut`, which cancels the in-flight invoke so a stale user cannot be applied.

packages/ui/src/machines/authenticator/__tests__/index.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,4 +628,97 @@ describe('authenticator', () => {
628628
signInActor: 'runActor',
629629
});
630630
});
631+
632+
it('should handle SIGN_OUT while resolving the current user in idle', async () => {
633+
let resolveUser!: (user: unknown) => void;
634+
service = interpret(
635+
createAuthenticatorMachine()
636+
.withContext({
637+
config: {},
638+
services: {
639+
getCurrentUser: () =>
640+
new Promise((resolve) => {
641+
resolveUser = resolve;
642+
}),
643+
getAmplifyConfig: () =>
644+
Promise.resolve({}) as ReturnType<
645+
(typeof defaultServices)['getAmplifyConfig']
646+
>,
647+
},
648+
})
649+
.withConfig({
650+
actions: {
651+
clearActorDoneData: jest.fn(() => Promise.resolve),
652+
spawnSignInActor: jest.fn(() => Promise.resolve),
653+
spawnSignOutActor: jest.fn(() => Promise.resolve),
654+
},
655+
})
656+
);
657+
658+
service.start();
659+
expect(service.getSnapshot().value).toStrictEqual('idle');
660+
661+
// a sign out during `idle` is forwarded to the machine as `SIGN_OUT` by the
662+
// Hub listener while `handleGetCurrentUser` is still in flight
663+
service.send({ type: 'SIGN_OUT' });
664+
await flushPromises();
665+
expect(service.getSnapshot().value).toStrictEqual({ signOut: 'runActor' });
666+
667+
// exiting `idle` cancels the invoke, the stale user is not applied
668+
resolveUser({ username: 'stale-user' });
669+
await flushPromises();
670+
expect(service.getSnapshot().value).toStrictEqual({ signOut: 'runActor' });
671+
672+
service.send({ type: 'done.invoke.signOutActor' });
673+
await flushPromises();
674+
expect(service.getSnapshot().value).toStrictEqual({
675+
signInActor: 'runActor',
676+
});
677+
expect(service.getSnapshot().context.user).toBeUndefined();
678+
});
679+
680+
it('should handle SIGN_OUT in the getCurrentUser state', async () => {
681+
let calls = 0;
682+
service = interpret(
683+
createAuthenticatorMachine()
684+
.withContext({
685+
config: {},
686+
services: {
687+
getCurrentUser: () => {
688+
calls += 1;
689+
// reject in `idle`, then hang in `getCurrentUser`
690+
return calls === 1 ? Promise.reject() : new Promise(() => {});
691+
},
692+
getAmplifyConfig: () =>
693+
Promise.resolve({}) as ReturnType<
694+
(typeof defaultServices)['getAmplifyConfig']
695+
>,
696+
},
697+
})
698+
.withConfig({
699+
actions: {
700+
clearActorDoneData: jest.fn(() => Promise.resolve),
701+
spawnSignInActor: jest.fn(() => Promise.resolve),
702+
spawnSignOutActor: jest.fn(() => Promise.resolve),
703+
},
704+
})
705+
);
706+
707+
service.start();
708+
await flushPromises();
709+
expect(service.getSnapshot().value).toStrictEqual({ setup: 'initConfig' });
710+
711+
service.send({ type: 'SIGN_IN_WITH_REDIRECT' });
712+
expect(service.getSnapshot().value).toStrictEqual('getCurrentUser');
713+
714+
service.send({ type: 'SIGN_OUT' });
715+
await flushPromises();
716+
expect(service.getSnapshot().value).toStrictEqual({ signOut: 'runActor' });
717+
718+
service.send({ type: 'done.invoke.signOutActor' });
719+
await flushPromises();
720+
expect(service.getSnapshot().value).toStrictEqual({
721+
signInActor: 'runActor',
722+
});
723+
});
631724
});

packages/ui/src/machines/authenticator/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export function createAuthenticatorMachine(
111111
onDone: { actions: 'setUser', target: 'setup' },
112112
onError: { target: 'setup' },
113113
},
114+
// handle a sign out occurring while `handleGetCurrentUser` is in flight,
115+
// exiting cancels the invoke so a stale user cannot be applied
116+
on: { SIGN_OUT: '#authenticator.signOut' },
114117
},
115118
setup: {
116119
initial: 'initConfig',
@@ -156,6 +159,7 @@ export function createAuthenticatorMachine(
156159
},
157160
onError: { target: '#authenticator.setup' },
158161
},
162+
on: { SIGN_OUT: '#authenticator.signOut' },
159163
},
160164
signInActor: {
161165
initial: 'spawnActor',

0 commit comments

Comments
 (0)