Skip to content

Instantly share code, notes, and snippets.

@heri16
Last active November 12, 2020 13:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heri16/fdd7a228b43bcc886a3c11affaab9ee9 to your computer and use it in GitHub Desktop.
Save heri16/fdd7a228b43bcc886a3c11affaab9ee9 to your computer and use it in GitHub Desktop.
Lightweight Authenticator Component for Aws-Amplify
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>
)
}
// 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,
}
// 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
email
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
// 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()
},
},
},
},
},
})
// 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,
//}),
})
// 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
)
)
);
// 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
// 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];
}
// 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
}
}
// 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
@heri16
Copy link
Author

heri16 commented Oct 31, 2020

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) called withAuthPiece

@heri16
Copy link
Author

heri16 commented Oct 31, 2020

Buttons

  • Back Button inside <CustomConfirmSignIn> should call props.changeState('signIn', null)
  • Skip Button inside <RequireNewPassword> should call props.changeState('signedIn', props.authData)

Handlers for data.challengeName / data.challengeParam

  • challengeParam.captchaUrl inside <CustomConfirmSignIn> should call Auth.sendCustomChallengeAnswer(user, captchaResponse, { email }).
  • challengeParam.email inside <CustomConfirmSignIn> should display login code input form.
  • challengeParam.username inside <CustomConfirmSignIn> should call Auth.signIn(challengeParam.username, temporaryPassword)
  • challengeName == 'NEW_PASSWORD_REQUIRED' inside <CustomConfirmSignIn> should call props.changeState('requireNewPassword', data).

See: https://github.com/imajin-land/lawkin/pull/75#issuecomment-712115400

@heri16
Copy link
Author

heri16 commented Nov 9, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment