Skip to content

Instantly share code, notes, and snippets.

@hrafnkellpalsson
Last active April 6, 2021 03:58
Show Gist options
  • Select an option

  • Save hrafnkellpalsson/92657f1253432595d7800dd58bc8b507 to your computer and use it in GitHub Desktop.

Select an option

Save hrafnkellpalsson/92657f1253432595d7800dd58bc8b507 to your computer and use it in GitHub Desktop.
Auth state machine for AWS Amplify Authenticator component
import { Machine } from "xstate"
import Auth from "@aws-amplify/auth"
import { Hub } from "@aws-amplify/core"
import { sentry } from "../sentry"
import { queryCache } from "react-query"
// Some of the events from the Hub auth channel are in the documentation here
// https://docs.amplify.aws/lib/auth/auth-events/q/platform/js
// Even more events in the source code
// https://github.com/aws-amplify/amplify-js/blob/92d8d800256119d1ba84bb90097f91a983b1e5c0/packages/auth/src/Auth.ts
export const AuthEvents = {
configured: "configured",
tokenRefresh: "tokenRefresh",
tokenRefresh_failure: "tokenRefresh_failure",
signIn: "signIn",
signIn_failure: "signIn_failure",
signUp: "signUp",
signUp_failure: "signUp_failure",
signOut: "signOut",
forgotPassword: "forgotPassword",
forgotPassword_failure: "forgotPassword_failure",
forgotPasswordSubmit: "forgotPasswordSubmit",
forgotPasswordSubmit_failure: "forgotPasswordSubmit_failure",
completeNewPassword_failure: "completeNewPassword_failure",
}
export const authMachine = Machine(
{
id: "auth",
strict: false, // The Hub sends a number of auth events we ignore
initial: "checkingIfLoggedIn",
invoke: {
src: "listenToAuthEvents",
},
states: {
checkingIfLoggedIn: {
invoke: {
src: "checkIfLoggedIn",
},
on: {
[AuthEvents.signIn]: "loggedIn",
[AuthEvents.signOut]: "loggedOut",
},
},
loggedIn: {
on: {
[AuthEvents.signOut]: "loggedOut",
},
},
loggedOut: {
entry: ["clearRQCache", "clearSentryScope"],
invoke: {
src: "logOutCleanup",
// We do an internal self transition so the service won't be invoked again. Otherwise we'd get into an endless loop.
onError: { target: "loggedOut", internal: true },
},
on: {
[AuthEvents.signIn]: "loggedIn",
[AuthEvents.signUp_failure]: "signUpFailed",
},
},
signUpFailed: {
on: {
[AuthEvents.signIn]: "loggedIn",
},
},
},
},
{
services: {
checkIfLoggedIn: (_ctx, _e) => async (sendParent) => {
try {
// This method can be used to check if a user is logged in when the page is loaded.
// It will throw an error if there is no user logged in.
// https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#retrieve-current-authenticated-user
const _user = await Auth.currentAuthenticatedUser()
sendParent(AuthEvents.signIn)
} catch (e) {
// The Amplify Auth package throws a string rather than an error
// For extra safety we could log to a service if we get a different error
const expectedError = "The user is not authenticated"
sendParent(AuthEvents.signOut)
}
},
listenToAuthEvents: (_ctx, _e) => (sendParent) => {
// Note that after user confirms their sign up with the code they received in their email,
// the Hub simply sends a 'signIn' event, there is no separate 'confirmSignUp' event.
const hubListener = (data) => {
const event = data.payload.event
sendParent(event)
}
Hub.listen("auth", hubListener)
// If any of the Cognito tokens, or any Cognito data, in local storage are deleted log we want to log the user out.
// Note that storage events don't always fire on the page that modified storage, see
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#responding_to_storage_changes_with_the_storageevent
// Not sure if that applies only to programmatic changes to storage or manual changes also.
// However, manual deletions of Cognito tokens from storage, do cause a storage event to fire in Chrome and Safari, and so the user
// is logged out. This doesn't work in Firefox however, but whatever, if it works in other browsers that's good enough for me.
const storageListener = (e) => {
const { key, newValue } = e
const isCognitoKey = key.includes("CognitoIdentityServiceProvider")
const wasDeleted = newValue === null
if (isCognitoKey && wasDeleted) {
sendParent(AuthEvents.signOut)
}
}
window.addEventListener("storage", storageListener)
return () => {
Hub.remove("auth", hubListener)
window.removeEventListener("storage", storageListener)
}
},
logOutCleanup: (_ctx, _e) => async () => {
// Clears tokens from local storage and sends sign out event to Hub auth channel
await Auth.signOut()
},
},
actions: {
// We don't create a redirect action because the signOut event is only ever sent when clicking a link that already redirects to "/"
// and we've set up our routing so whenever you navigate to any route apart from "/auth" when logged out you'll be redirected to "/"
clearRQCache: (_ctx, _e) => queryCache.clear(),
clearSentryScope: (_ctx, _e) =>
sentry.configureScope((scope) => scope.setUser(null)),
},
}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment