Last active
November 12, 2020 13:10
-
-
Save heri16/fdd7a228b43bcc886a3c11affaab9ee9 to your computer and use it in GitHub Desktop.
Lightweight Authenticator Component for Aws-Amplify
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { setContext } from "@apollo/client/link/context"; | |
import { Auth } from '@aws-amplify/auth'; | |
const getJwtToken = async () => { | |
const session = await Auth.currentSession() | |
return session.getAccessToken().getJwtToken() | |
} | |
const authLink = setContext(async (_, { headers, jwtToken }) => { | |
if (headers.authorization) return { headers } | |
const value = jwtToken || getJwtToken | |
const token = typeof value === 'function' ? await value.call(undefined) : await value; | |
if (!token) return { headers } | |
// return the headers to the context so httpLink can read them | |
return { | |
headers: { | |
...headers, | |
authorization: `Bearer ${token}`, | |
} | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import { Switch, Route } from 'react-router-dom' | |
import { Amplify } from '@aws-amplify/core' | |
import { amplifyConfig } from './config' | |
import { AllContextProvider } from './context' | |
import { | |
Authenticator, | |
FederatedSignIn, | |
SignIn, | |
CustomConfirmSignIn, | |
RequireNewPassword, | |
SignOut, | |
SignUp, | |
ConfirmSignUp, | |
VerifyContact, | |
TOTPSetup, | |
ForgotPassword, | |
Loading | |
} from './components/auth' | |
import { client } from './services/api' | |
import { ApolloProvider } from '@apollo/client' | |
const App = () => { | |
// Configure Amplify once | |
useEffect(() => { | |
if (amplifyConfig) Amplify.configure(amplifyConfig) | |
}, [amplifyConfig]) | |
return ( | |
<Switch> | |
<Route path="/drive/:uen"> | |
<AllContextProvider> | |
<div> | |
{/* SignIn with Cognito Hosted-UI */} | |
<FederatedSignIn validAuthStates={['signIn', 'signedOut', 'signedUp']} /> | |
{/* Or SignIn with Email */} | |
<SignIn validAuthStates={['signIn', 'signedOut', 'signedUp']} /> | |
</div> | |
{/* CustomConfirmSignIn handles both Captcha and OTP */} | |
<CustomConfirmSignIn validAuthStates={['customConfirmSignIn']} /> | |
<div> | |
{/* RequireNewPassword can be skipped via `props.changeState('signedIn', props.authData)` */} | |
<RequireNewPassword validAuthStates={['requireNewPassword']} /> | |
<SignOut validAuthStates={['requireNewPassword']} onSuccess={() => client.resetStore()} /> | |
</div> | |
<SignUp validAuthStates={['signUp']} signUpConfig={signUpConfig} /> | |
<ConfirmSignUp validAuthStates={['confirmSignUp']} /> | |
<div> | |
<VerifyContact validAuthStates={['verifyContact']}/> | |
<SignOut validAuthStates={['verifyContact']} onSuccess={() => client.resetStore()} /> | |
</div> | |
<TOTPSetup validAuthStates={['TOTPSetup']} /> | |
<ForgotPassword validAuthStates={['forgotPassword']} /> | |
<Loading validAuthStates={['loading']} /> | |
<ApolloProvider client={client}> | |
<Authenticator validAuthStates={['signedIn']}> | |
<DriveLayout> | |
<DriveHeader /> | |
<DriveMain /> | |
</DriveLayout> | |
</Authenticator> | |
</ApolloProvider> | |
</AllContextProvider> | |
</Route> | |
<Route path="/account"> | |
<AllContextProvider> | |
<FederatedSignIn validAuthStates={['signIn', 'signedOut', 'signedUp']} /> | |
<ApolloProvider client={client}> | |
<Authenticator validAuthStates={['signedIn']}> | |
<PortalLayout> | |
<PortalHeader /> | |
<PortalMain /> | |
</PortalLayout> | |
</Authenticator> | |
</ApolloProvider> | |
</AllContextProvider> | |
</Route> | |
<Route component={Home} /> | |
</Switch> | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: src/components/auth/index.js | |
import Authenticator from './Authenticator' | |
// Copy logic for these custom components from: | |
// https://github.com/aws-amplify/amplify-js/tree/aws-amplify%403.3.5/packages/aws-amplify-react/src/Auth | |
// Recommended to place all pure logic into `packages/lib/src/auth/SignIn/helper.js`, and | |
// then import into `src/components/auth/SignIn.jsx` to allow reuse on other sites | |
import FederatedSignIn from './FederatedSignIn' | |
import SignIn from './SignIn' | |
import ConfirmSignIn from './ConfirmSignIn' | |
import CustomConfirmSignIn from './CustomConfirmSignIn' | |
import RequireNewPassword from './RequireNewPassword' | |
import SignOut from './SignOut' | |
import SignUp from './SignUp' | |
import ConfirmSignUp from './ConfirmSignUp' | |
import VerifyContact from './VerifyContact' | |
import TOTPSetup from './TOTPSetup' | |
import ForgotPassword from './ForgotPassword' | |
import Loading from './Loading' | |
export { | |
Authenticator, | |
OAuthButton, | |
SignIn, | |
ConfirmSignIn, | |
CustomConfirmSignIn, | |
RequireNewPassword, | |
SignOut, | |
SignUp, | |
ConfirmSignUp, | |
VerifyContact, | |
TOTPSetup, | |
ForgotPassword, | |
Loading, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: src/components/auth/Authenticator.jsx | |
import React from 'react' | |
import PropTypes from 'prop-types' | |
import { | |
useAuthenticator, | |
} from '@app/lib/auth' | |
import { Auth } from '@aws-amplify/auth' | |
import { useLocalPerson, useAuthState, useAuthData, useToastData, validAuthStates } from '../../hooks' | |
import { RefetchPersonCtx } from '../../context' | |
import { SignOut } from './components/auth' | |
const PROFILE_QUERY = gql` | |
query Authenticator_Person { | |
currentPerson { | |
id | |
iss | |
sub | |
authTime | |
name | |
givenName | |
familyName | |
middleName | |
nickname | |
preferredUsername | |
profile | |
picture | |
website | |
emailVerified | |
gender | |
birthdate | |
zoneinfo | |
locale | |
phoneNumber | |
phoneNumberVerified | |
address | |
cognitoUsername | |
cognitoGroups | |
personPref { | |
defaultAccount { | |
num | |
name | |
} | |
} | |
} | |
} | |
`; | |
// App Authenticator cannot be an AuthPiece | |
const Authenticator = ({ validAuthStates, children }) => { | |
const { | |
isHidden, | |
authState, | |
authData | |
} = useAuthenticator(validAuthStates, { Auth, useAuthState, useAuthData, useToastData }) | |
//const [mutateCurrentPerson, { client, loading, data: { currentPerson } }] = useMutation( | |
const { client, loading, refetch, error, data: { currentPerson } = {} } = useQuery( | |
PROFILE_QUERY, | |
{ | |
fetchPolicy: "network-only", | |
skip: (!authData || authState !== 'signedIn'), | |
context: { | |
// See `apollo-link.js` included in this gist | |
jwtToken: () => Auth.userSession(authData).then(userSession => userSession.getIdToken().getJwtToken()) | |
}, | |
} | |
) | |
//useEffect(() => { | |
// if (!authData || authState !== 'signedIn') return | |
// mutateCurrentPerson() | |
//}, [authState, authData]) | |
const [, setLocalPerson] = useLocalPerson() | |
useEffect(() => { | |
if (currentPerson) setLocalPerson(currentPerson) | |
}, [currentPerson]) | |
if (loading) { | |
return ( | |
<> | |
<div>Loading your profile...</div> | |
<SignOut onSuccess={() => client.resetStore()} /> | |
</> | |
) | |
} | |
console.debug('currentPerson:', currentPerson) | |
// React v16+ | |
return isHidden ? null : (<RefetchPersonCtx.Provider value={refetch}>{children}</RefetchPersonCtx.Provider>) | |
// return ( | |
// <RefetchPersonCtx.Provider value={refetch}> | |
// <div className={isHidden ? 'disabled' : ''}> | |
// {children} | |
// <div> | |
// </RefetchPersonCtx.Provider> | |
// ) | |
} | |
Authenticator.propTypes = { | |
validAuthStates: PropTypes.arrayOf(PropTypes.oneOf(validAuthStates)).isRequired, | |
} | |
export Authenticator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: packages/site/src/cache.js | |
import { InMemoryCache, makeVar } from '@apollo/client' | |
export const localPerson = makeVar() | |
//export const authStateVar = makeVar('loading') | |
//export const authDataVar = makeVar() | |
//export const toastDataVar = makeVar({ error: null, show: false }) | |
export const cache = new InMemoryCache({ | |
typePolicies: { | |
Query: { | |
fields: { | |
localPerson: { | |
read() { | |
return localPersonVar() | |
}, | |
}, | |
}, | |
}, | |
}, | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: packages/site/src/config.js | |
// From: https://docs.amplify.aws/lib/auth/start/q/platform/js#re-use-existing-authentication-resource | |
export const amplifyConfig = Object.freeze({ | |
Auth: Object.freeze({ | |
// REQUIRED - Amazon Cognito Region | |
region: process.env.AWS_REGION, | |
// OPTIONAL - Amazon Cognito User Pool ID | |
userPoolId: process.env.COGNITO_USER_POOL_ID, | |
// OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string) | |
userPoolWebClientId: process.env.COGNITO_CLIENT_ID, | |
// OPTIONAL - Enforce user authentication prior to accessing AWS resources or not | |
mandatorySignIn: false, | |
// REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID | |
// REQUIRED only for access to any AWS resources requiring IAM roles / AWS_ACCESS_KEY like AWS S3 | |
identityPoolId: process.env.COGNITO_IDENTITY_POOL_ID, | |
// OPTIONAL - Amazon Cognito Federated Identity Pool Region | |
// Required only if it's different from Amazon Cognito Region | |
//identityPoolRegion: process.env.AWS_REGION, | |
// OPTIONAL - Configuration for cookie storage | |
// CookieStorage is used to make security tokens accessible across subdomains, | |
// but is less secure than customized storage object in the next section | |
// Note: if the secure flag is set to true, then the cookie transmission requires a secure protocol | |
//cookieStorage: { | |
// // REQUIRED - Cookie domain (only required if cookieStorage is provided) | |
// domain: '.yourdomain.com', | |
// // OPTIONAL - Cookie path | |
// path: '/', | |
// // OPTIONAL - Cookie expiration in days | |
// expires: 365, | |
// // OPTIONAL - See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite | |
// sameSite: "strict", | |
// // OPTIONAL - Cookie secure flag | |
// // Either true or false, indicating if the cookie transmission requires a secure protocol (https). | |
// secure: true | |
//}, | |
// OPTIONAL - customized storage object | |
// See: https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#managing-security-tokens | |
//storage: MyStorage, | |
// OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH' | |
//authenticationFlowType: 'USER_PASSWORD_AUTH', | |
// OPTIONAL - Manually set key value pairs that can be passed to Cognito Lambda Triggers | |
//clientMetadata: { myCustomKey: 'myCustomValue' }, | |
// OPTIONAL - Hosted UI configuration | |
// See: https://aws.amazon.com/premiumsupport/knowledge-center/cognito-hosted-web-ui/ | |
oauth: Object.freeze({ | |
domain: process.env.COGNITO_AUTH_DOMAIN, | |
scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'], | |
redirectSignIn: document.location.origin, | |
redirectSignOut: document.location.origin, | |
responseType: 'code' // or 'token', note that REFRESH token will only be generated when the responseType is code | |
}) | |
}), | |
//Storage: Object.freeze({ | |
// region: awsConfig.region, | |
// bucket: awsConfig.S3Bucket, | |
//}), | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: packages/site/src/context.js | |
import { useState, useCallback, useMemo , createContext, createElement } from 'react' | |
export const RefetchPersonCtx = createContext() | |
const AuthStateCtx = createContext() | |
const AuthDataCtx = createContext() | |
const ToastDataCtx = createContext() | |
export const validAuthStates = Object.freeze([ | |
'signIn', | |
'confirmSignIn', | |
'customConfirmSignIn', | |
'signedIn', | |
'signedOut', | |
'signedUp', | |
'requireNewPassword', | |
'signUp', | |
'confirmSignUp', | |
'verifyContact', | |
'forgotPassword', | |
'TOTPSetup', | |
'loading' | |
]) | |
export const useAuthState = () => useContext(AuthStateCtx); | |
export const AuthStateProvider = ({ children }) => { | |
const [authState, setAuthState] = useState('loading') | |
// Like React's setState, guarantees that setAuthState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. | |
const setValue = useCallback((val) => { | |
if (!validAuthStates.includes(val)) throw new TypeError() | |
setAuthState(val) | |
}, [validAuthStates]) | |
const value = useMemo(() => [authState, setValue], [authState, setValue]) | |
return createElement(AuthStateCtx.Provider, { value }, children) | |
}; | |
export const useAuthData = () => useContext(AuthDataCtx); | |
export const AuthDataProvider = ({ children }) => { | |
const [authData, setAuthData] = useState() | |
// Like React's setState, guarantees that setAuthData function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. | |
const value = useMemo(() => [authData, setAuthData], [authData]) | |
return createElement(AuthDataCtx.Provider, { value }, children) | |
}; | |
export const useToastData = () => useContext(ToastDataCtx); | |
export const ToastDataProvider = ({ children }) => { | |
const [toastData, setToastData] = useState({ error: null, show: false }) | |
// Like React's setState, guarantees that setToastData function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. | |
const setValue = useCallback(({ error, show }) => { | |
if (typeof show !== 'boolean') throw new TypeError() | |
setToastData({ error, show }) | |
}, []) | |
const value = useMemo(() => [toastData, setValue], [toastData, setValue]) | |
return createElement(ToastDataCtx.Provider, { value }, children) | |
}; | |
export const AllContextProvider = ({ children }) => createElement( | |
AuthStateProvider, | |
null, | |
createElement( | |
AuthDataProvider, | |
null, | |
createElement( | |
ToastDataProvider, | |
null, | |
children | |
) | |
) | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: src/components/auth/FederatedSignIn.jsx | |
import React, { useState } from 'react' | |
import PropTypes from 'prop-types' | |
import { | |
Button, | |
} from '@app/components' | |
import { | |
useAuthPiece, | |
} from '@app/lib/auth' | |
import { Auth } from '@aws-amplify/auth' | |
import { useAuthState, useAuthData, useToastData, validAuthStates } from '../../hooks' | |
const FederatedSignIn = ({ validAuthStates, federated = {}, provider }) => { | |
const { | |
isHidden, | |
authState, | |
authData, | |
changeState, | |
setError, | |
resetError, | |
usernameFromAuthData | |
} = useAuthPiece(validAuthStates, { useAuthState, useAuthData, useToastData }) | |
if (isHidden) return null | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/FederatedSignIn.tsx#L182-L202 | |
// @ts-ignore | |
const { oauth = {} } = Auth.configure(); | |
// backward compatibility | |
if (oauth['domain']) { | |
federated.oauth_config = Object.assign({}, federated.oauth_config, oauth); | |
// @ts-ignore | |
} else if (oauth.awsCognito) { | |
// @ts-ignore | |
federated.oauth_config = Object.assign( | |
{}, | |
federated.oauth_config, | |
// @ts-ignore | |
oauth.awsCognito | |
); | |
} | |
// @ts-ignore | |
if (oauth.auth0) { | |
// @ts-ignore | |
federated.auth0 = Object.assign({}, federated.auth0, oauth.auth0); | |
} | |
const signIn = useCallback(() => { | |
Auth.federatedSignIn({ provider }).catch(err => { | |
setError(err) | |
}) | |
}, [provider]) | |
return ( | |
<Button variant="contained" color="primary" onClick={signIn}> | |
SignIn with Email | |
</Button> | |
) | |
} | |
FederatedSignIn.propTypes = { | |
validAuthStates: PropTypes.arrayOf(PropTypes.oneOf(validAuthStates)).isRequired, | |
} | |
export FederatedSignIn |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: packages/site/src/hooks.js | |
import { useContext, useMemo, useRef, useEffect, useState, useCallback } from 'react' | |
import { useReactiveVar } from '@apollo/client' | |
import { | |
RefetchPersonCtx, | |
validAuthStates, | |
useAuthState, | |
useAuthData, | |
useToastData, | |
} from './context' | |
export { validAuthStates, useAuthState, useAuthData, useToastData } | |
import { | |
localPersonVar, | |
// authStateVar, | |
// authDataVar, | |
// toastDataVar | |
} from './cache' | |
export const useLocalPerson = () => { | |
const refetch = useContext(RefetchPersonCtx) | |
// useCallback(fn) is same as useMemo(() => fn), just less efficient | |
const setLocalPerson = useMemo(() => (val) => { | |
if (typeof val === 'function') { | |
// See: https://reactjs.org/docs/hooks-reference.html#functional-updates | |
// See: https://github.com/apollographql/apollo-client/blob/b874104cddf718547d9ff0b4fa51cda8267bbf2a/src/cache/inmemory/reactiveVars.ts#L36 | |
localPersonVar(val(localPersonVar())) | |
} else if (typeof val !== 'undefined') { | |
localPersonVar(val) | |
} else { | |
refetch() | |
} | |
}, [refetch]) | |
return [useReactiveVar(localPersonVar), setLocalPerson] | |
} | |
/* | |
// Just like React setState, guarantees that setXXXX function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. | |
const setAuthState = (val) => { | |
if (!validAuthStates.includes(val)) throw new TypeError() | |
authStateVar(val) | |
} | |
export const useAuthState = () => [useReactiveVar(authStateVar), setAuthState]; | |
// Just like React setState, guarantees that setXXXX function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. | |
const setAuthData = (val) => { | |
// if (typeof val === 'undefined') throw new TypeError() | |
authDataVar(val) | |
} | |
export const useAuthData = () => [useReactiveVar(authDataVar), setAuthData]; | |
// Just like React setState, guarantees that setXXXX function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. | |
const setToastData = ({ error, show }) => { | |
if (typeof show !== 'boolean') throw new TypeError() | |
toastDataVar({ error, show }) | |
} | |
export const useToastData = () => [useReactiveVar(toastDataVar), setToastData]; | |
*/ | |
// From: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state | |
// Only use this if `setState(prevValue => prevValue + 1)}` does not suffice | |
// Example Usage: | |
// const [count, setCount] = useState(0); | |
// const prevCount = usePrevious(count); | |
export function usePrevious(value) { | |
const ref = useRef(); | |
useEffect(() => { | |
ref.current = value; | |
}); | |
return ref.current; | |
} | |
// From: https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node | |
// Example usage: | |
// const [rect, ref] = useClientRect(); | |
// return (<h1 ref={ref}>Hello, world</h1>) | |
export function useClientRect() { | |
const [rect, setRect] = useState(null); | |
const ref = useCallback(node => { | |
if (node !== null) { | |
setRect(node.getBoundingClientRect()); | |
} | |
}, []); | |
return [rect, ref]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: packages/lib/src/auth/hooks.js | |
import { useMemo, useCallback, useEffect } from 'react' | |
import { Hub, isEmpty } from '@aws-amplify/core' | |
// From: https://github.com/aws-amplify/amplify-js/blob/7d17aee834ff9d2d4f280e4ede87d12a5753bef4/packages/aws-amplify-react/src/Auth/common/constants.tsx | |
export const Constants = Object.freeze({ | |
AUTH_SOURCE_KEY: 'amplify-react-auth-source', | |
AUTH0: 'auth0', | |
GOOGLE: 'google', | |
FACEBOOK: 'facebook', | |
AMAZON: 'amazon', | |
REDIRECTED_FROM_HOSTED_UI: 'amplify-redirected-from-hosted-ui', | |
}) | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L185-L211 | |
function makeHandleStateChange(setAuthState, setAuthData, setToastData, authStateLsKey) { | |
return function handleStateChange(state, data) => { | |
try { | |
localStorage.setItem(authStateLsKey, state); | |
} catch (e) { | |
console.debug('Failed to set the auth state into local storage', e); | |
} | |
setAuthState(state) | |
setAuthData(data) | |
setToastData({ error: null, show: false }) | |
} | |
} | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L213-L219 | |
function makeHandleAuthEvent(setToastData, errorMessage) { | |
return function handleAuthEvent(state, event, showToast = true) { | |
if (event.type === 'error') { | |
const message = ( | |
typeof errorMessage === 'string' ? errorMessage : | |
typeof errorMessage === 'function' ? errorMessage(event.data) : | |
event.data | |
) | |
setToastData({ error: message, show: showToast }) | |
} else if (event.type === 'reset') { | |
setToastData({ error: null, show: false }) | |
} | |
} | |
} | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/AuthPiece.tsx#L169-L174 | |
function errorMessage(err) { | |
if (typeof err === 'string') { | |
return err | |
} | |
return err.message ? err.message : JSON.stringify(err) | |
} | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/AuthPiece.tsx#L152-L167 | |
function usernameFromAuthData(authData) { | |
if (!authData) { | |
return '' | |
} | |
let username = '' | |
if (typeof authData === 'object') { | |
// user object | |
username = authData.user ? authData.user.username : authData.username | |
} else { | |
username = authData // username string | |
} | |
return username | |
} | |
export function useAuthPiece(validAuthStates, { useAuthState, useAuthData, useToastData, authStateLsKey = 'amplify-authenticator-authState' }) { | |
if (typeof useAuthState !== 'function') throw new TypeError('useAuthState not a func') | |
if (typeof useAuthData !== 'function') throw new TypeError('useAuthData not a func') | |
if (typeof useToastData !== 'function') throw new TypeError('useToastData not a func') | |
if (typeof authStateLsKey !== 'string') throw new TypeError('authStateLsKey not a string') | |
const [authState, setAuthState] = useAuthState() | |
const [authData, setAuthData] = useAuthData() | |
const [toastData, setToastData] = useToastData() | |
const onStateChange = useMemo(() => makeHandleStateChange(setAuthState, setAuthData, setToastData, authStateLsKey), [authStateLsKey]) | |
const onAuthEvent = useMemo(() => makeHandleAuthEvent(setToastData), []) | |
// const [isHidden, setHidden] = useState(true) | |
// const [inputs, setInputs ] = useState({}) | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/AuthPiece.tsx#L209-L224 | |
const isHidden = useMemo(() => Array.isArray(validAuthStates) && !validAuthStates.includes(authState), [authState, validAuthStates]) | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/AuthPiece.tsx#L176-L181 | |
const triggerAuthEvent = useCallback((event) => { | |
if (onAuthEvent) onAuthEvent(authState, event) | |
}, [authState, onAuthEvent]) | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/AuthPiece.tsx#L183-L192 | |
const changeState = useCallback((state, data) => { | |
if (onStateChange) onStateChange(state, data) | |
triggerAuthEvent({ | |
type: 'stateChange', | |
data: state, | |
}) | |
}, [onStateChange, triggerAuthEvent]) | |
// Taken from: https://github.com/aws-amplify/amplify-js/blob/53be43d7a0049e04a47e7fece5dcd726c7a414fe/packages/aws-amplify-react/src/Auth/AuthPiece.tsx#L194-L199 | |
const setError = useCallback((err) => { | |
triggerAuthEvent({ | |
type: 'error', | |
data: errorMessage(err), | |
}) | |
}, [triggerAuthEvent, errorMessage]) | |
const resetError = useCallback(() => { | |
triggerAuthEvent({ | |
type: 'reset', | |
data: null, | |
}) | |
}, [triggerAuthEvent]) | |
return { | |
isHidden, | |
authState, | |
authData, | |
// toastData, | |
changeState, | |
setError, | |
resetError, | |
usernameFromAuthData: useCallback(() => usernameFromAuthData(authData), [authData]) | |
} | |
} | |
export function useAuthListen(handler) { | |
if (typeof handler !== 'function') throw new TypeError() | |
return useEffect(() => { | |
const authListener = ({ payload }) => handler(payload); | |
Hub.listen('auth', authListener) | |
const cleanup = () => Hub.remove('auth', authListener) | |
return cleanup | |
}, [handler]) | |
} | |
export function useAuthenticator(validAuthStates, { Auth, useAuthState, useAuthData, useToastData, authStateLsKey = 'amplify-authenticator-authState' }) { | |
if (typeof useAuthState !== 'function') throw new TypeError('useAuthState not a func') | |
if (typeof useAuthData !== 'function') throw new TypeError('useAuthData not a func') | |
if (typeof useToastData !== 'function') throw new TypeError('useToastData not a func') | |
if (typeof authStateLsKey !== 'string') throw new TypeError('authStateLsKey not a string') | |
const [authState, setAuthState] = useAuthState() | |
const [authData, setAuthData] = useAuthData() | |
const [toastData, setToastData] = useToastData() | |
const handleStateChange = useMemo(() => makeHandleStateChange(setAuthState, setAuthData, setToastData, authStateLsKey), [authStateLsKey]) | |
// From: https://github.com/aws-amplify/amplify-js/blob/7d17aee834ff9d2d4f280e4ede87d12a5753bef4/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L143-L157 | |
const checkContact = useCallback((user, changeState) => { | |
if (!Auth || typeof Auth.verifiedContact !== 'function') { | |
throw new Error( | |
'No Auth module found, please ensure @aws-amplify/auth is passed to useAuthenticator' | |
); | |
} | |
Auth.verifiedContact(user).then(data => { | |
if (!isEmpty(data.verified)) { | |
changeState('signedIn', user); | |
} else { | |
user = Object.assign(user, data); | |
changeState('verifyContact', user); | |
} | |
}); | |
}, [Auth.verifiedContact]) | |
// From: https://github.com/aws-amplify/amplify-js/blob/7d17aee834ff9d2d4f280e4ede87d12a5753bef4/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L159-L183 | |
const onHubAuthEvent = useCallback(({ event, data }) => { | |
switch (event) { | |
case 'cognitoHostedUI': | |
case 'signIn': | |
checkContact(data, handleStateChange); | |
break; | |
case 'cognitoHostedUI_failure': | |
case 'parsingUrl_failure': | |
case 'signOut': | |
case 'customGreetingSignOut': | |
handleStateChange('signIn', null); | |
break; | |
default: | |
console.debug('onHubAuthEvent: ' + event); | |
} | |
}, [checkContact, handleStateChange]) | |
// From: https://github.com/aws-amplify/amplify-js/blob/7d17aee834ff9d2d4f280e4ede87d12a5753bef4/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L85 | |
useAuthListen(onHubAuthEvent) | |
// From: https://github.com/aws-amplify/amplify-js/blob/7d17aee834ff9d2d4f280e4ede87d12a5753bef4/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L110-L141 | |
const checkUser = useCallback(() => { | |
if (!Auth || typeof Auth.currentAuthenticatedUser !== 'function') { | |
throw new Error( | |
'No Auth module found, please ensure @aws-amplify/auth is passed to useAuthenticator' | |
); | |
} | |
return Auth.currentAuthenticatedUser() | |
.then(user => { | |
handleStateChange('signedIn', user); | |
}) | |
.catch(err => { | |
let cachedAuthState = null; | |
try { | |
cachedAuthState = localStorage.getItem(authStateLsKey); | |
} catch (e) { | |
console.debug('Failed to get the auth state from local storage', e); | |
} | |
const promise = | |
cachedAuthState === 'signedIn' ? Auth.signOut() : Promise.resolve(); | |
promise | |
.then(() => handleStateChange('signIn')) | |
.catch(e => { | |
console.debug('Failed to sign out', e); | |
}); | |
}); | |
}, [authStateLsKey, handleStateChange, Auth.currentAuthenticatedUser, Auth.signOut]) | |
// From: https://github.com/aws-amplify/amplify-js/blob/7d17aee834ff9d2d4f280e4ede87d12a5753bef4/packages/aws-amplify-react/src/Auth/Authenticator.tsx#L88-L109 | |
// See: https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies | |
useEffect(() => { | |
// The workaround for Cognito Hosted UI: | |
// Don't check the user immediately if redirected back from Hosted UI as | |
// it might take some time for credentials to be available, instead | |
// wait for the hub event sent from Auth module. This item in the | |
// localStorage is a mark to indicate whether the app is just redirected | |
// back from Hosted UI or not and is set in Auth:handleAuthResponse. | |
const byHostedUI = localStorage.getItem(Constants.REDIRECTED_FROM_HOSTED_UI); | |
localStorage.removeItem(Constants.REDIRECTED_FROM_HOSTED_UI); | |
if (byHostedUI !== 'true') checkUser(); | |
}, [checkUser]) | |
const isHidden = useMemo(() => Array.isArray(validAuthStates) && !validAuthStates.includes(authState), [authState, validAuthStates]) | |
return { | |
isHidden, | |
authState, | |
authData, | |
// toastData | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// File: src/components/auth/SignIn.jsx | |
import React, { useState } from 'react' | |
import PropTypes from 'prop-types' | |
import { useForm, Controller } from 'react-hook-form' | |
import { | |
useAuthPiece, | |
signInHelper, | |
} from '@app/lib/auth' | |
import { Auth } from '@aws-amplify/auth' | |
import { useAuthState, useAuthData, useToastData, validAuthStates } from '../../hooks' | |
const SignIn = ({ validAuthStates }) => { | |
const { | |
isHidden, | |
authState, | |
authData, | |
changeState, | |
setError, | |
resetError, | |
usernameFromAuthData | |
} = useAuthPiece(validAuthStates, { useAuthState, useAuthData, useToastData }) | |
const { register, handleSubmit, reset } = useForm({ | |
// See: https://react-hook-form.com/api/#useForm | |
shouldUnregister: false | |
}); | |
if (isHidden) return null | |
// TODO... | |
} | |
SignIn.propTypes = { | |
validAuthStates: PropTypes.arrayOf(PropTypes.oneOf(validAuthStates)).isRequired, | |
} | |
export SignIn |
Buttons
- Back Button inside
<CustomConfirmSignIn>
should callprops.changeState('signIn', null)
- Skip Button inside
<RequireNewPassword>
should callprops.changeState('signedIn', props.authData)
Handlers for data.challengeName / data.challengeParam
- challengeParam.captchaUrl inside
<CustomConfirmSignIn>
should callAuth.sendCustomChallengeAnswer(user, captchaResponse, { email })
. - challengeParam.email inside
<CustomConfirmSignIn>
should display login code input form. - challengeParam.username inside
<CustomConfirmSignIn>
should callAuth.signIn(challengeParam.username, temporaryPassword)
- challengeName == 'NEW_PASSWORD_REQUIRED' inside
<CustomConfirmSignIn>
should callprops.changeState('requireNewPassword', data)
.
See: https://github.com/imajin-land/lawkin/pull/75#issuecomment-712115400
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Using hooks from props is not a standard practice and likely not suported in future reactJs versions.
AuthPiece
has been changed to a Higher-Order function (i.e. function that takes a component and returns a component) calledwithAuthPiece