Last active
January 20, 2024 15:03
-
-
Save gabeweaver/d1be9f0d41069437f576c375c30e134c to your computer and use it in GitHub Desktop.
React + Cognito User Pools + Cognito Identity JS Example
This file contains 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
/* | |
This example was built using standard create-react-app out of the box with no modifications or ejections | |
to the underlying scripts. | |
In this example, i'm using Google as a social provider configured within the Cognito User Pool. | |
Each step also represents a file, so you can see how I've chosen to organize stuff...you can do it however | |
you'd like so long as you follow the basic flow (which may or may not be the official way....but its what I found that works. | |
The docs are pretty horrible) | |
The basic flow is: | |
onClick -> | |
Login -> | |
Select Google from the Cognito Hosted UI -> | |
Cognito auths with Google and returns the token in the url at the configured callback URL -> | |
CognitoAuthSDK parses the url and stores the idToken and accessToken in local storage -> | |
On the auth success handler, a new session with CognitoID is initiated -> | |
CognitoId creates the user in the Identity Pool by pulling data from local storage that the Cognito Auth JS SDK stored -> | |
After CognitoID success is started and the credential provider is set in the core AWS SDK, AWS SDK facilitates exhanging the | |
termporary tokens by way of refresh | |
My original assumption was that the Cognito Auth JS SDK would handle the authentication for both the User Pool and the | |
Federated Identity pool...but after authenticating with Cognito Auth JS, no Cognito ID user was ever created...not sure | |
if that is intended design, but I had to user both Coginto Auth JS methods and Cognito ID methods to get things working | |
and a successful session with both initiated. | |
*/ | |
/* | |
############################ | |
Step #1: lib/awsSDK.js - Import named methods from the AWS SDK and do some "global" config like setting the Region | |
############################ | |
*/ | |
import { | |
config as AWSConfig, | |
CognitoIdentityCredentials, | |
Lambda | |
} from 'aws-sdk' | |
const AWSRegion = process.env.REACT_APP_AWS_REGION | |
AWSConfig.region = AWSRegion | |
export { AWSRegion, AWSConfig, CognitoIdentityCredentials, Lambda } | |
/* | |
############################ | |
Step #2: lib/cognitoId.js - Implement the Cognito Id methods to start a Cognito Id session | |
############################ | |
*/ | |
import { AWSRegion, AWSConfig, CognitoIdentityCredentials } from './awsSDK' | |
import { CognitoUserPool } from 'amazon-cognito-identity-js' | |
/* Config for CognitoID */ | |
const config = { | |
identityPool: process.env.REACT_APP_COGNITO_IDENTITY_POOL, | |
userPool: { | |
UserPoolId: process.env.REACT_APP_COGNITO_USER_POOL_ID, | |
ClientId: process.env.REACT_APP_COGNITO_CLIENT_ID | |
} | |
} | |
// Gets a User Pool instance | |
const getUserPool = () => new CognitoUserPool(config.userPool) | |
// Gets user attributes based on the passed cognitoUser | |
const getUserAttributes = user => { | |
return user.getUserAttributes((err, result) => { | |
if (err) { | |
alert(err) | |
return | |
} | |
return result | |
}) | |
} | |
// Gets a cognito user | |
const getCognitoUser = user => { | |
const pool = getUserPool() | |
return pool.getCurrentUser() | |
} | |
// The primary method for verifying/starting a CoginotID session | |
const verifySession = ({ props, username }) => { | |
const poolUrl = `cognito-idp.${AWSRegion}.amazonaws.com/${ | |
config.userPool.UserPoolId | |
}` | |
/* Note - I'm skipping the basic auth step since I already have the accessToken and jwtToken stored locally | |
thanks to Cognito Auth */ | |
/* You don't have to do this, but I am so I can get the user's name from the parsed JWT token so I don't have | |
to call getUserAttributes after the session as been started. */ | |
const cognitoUser = getCognitoUser() | |
let name | |
/** Get a new session and set it in the AWS config */ | |
cognitoUser.getSession((err, result) => { | |
console.log(err, result) | |
if (result) { | |
name = result.idToken.payload.given_name | |
AWSConfig.credentials = new CognitoIdentityCredentials({ | |
IdentityPoolId: config.identityPool, | |
Logins: { | |
[poolUrl]: result.idToken.jwtToken | |
} | |
}) | |
} | |
}) | |
/* Refresh the temporary token */ | |
AWSConfig.credentials.refresh(err => { | |
if (err) { | |
console.error('Failed To Login To CognitoID:', err) | |
props.history.push('/', { | |
error: 'Failed to refresh your session. Please login again.' | |
}) | |
} else { | |
props.storeSession({ | |
token, | |
name | |
}) | |
} | |
}) | |
} | |
const cognitoId = { | |
getUserPool, | |
getCognitoUser, | |
getUserAttributes, | |
verifySession | |
} | |
export default cognitoId | |
/* | |
############################ | |
Step #3: lib/cognitoAuth.js - Create a basic Cognito Auth JS Lib -> The User Pool Stuff | |
############################ | |
*/ | |
// Note the import path because of a defect in the package | |
import { CognitoAuth } from 'amazon-cognito-auth-js/dist/amazon-cognito-auth' | |
// This is the CognitoIdSDK lib - see Step #2 | |
import cognitoId from './cognitoId' | |
// Convert my string in the env var to a comma separated array | |
const Arr = string => string.split(/:\s|,\s/) | |
// define the config for the Auth JS SDK | |
const config = { | |
UserPoolId: process.env.REACT_APP_COGNITO_USER_POOL_ID, | |
ClientId: process.env.REACT_APP_COGNITO_CLIENT_ID, | |
AppWebDomain: process.env.REACT_APP_COGNITO_APP_DOMAIN, | |
TokenScopesArray: Arr(process.env.REACT_APP_COGNITO_GOOGLE_SCOPES), | |
RedirectUriSignIn: process.env.REACT_APP_COGNITO_SIGN_IN_REDIRECT_URI, | |
RedirectUriSignOut: process.env.REACT_APP_COGNITO_SIGN_OUT_REDIRECT_URI | |
} | |
// Generic error handler for "public" methods exported by this lib. | |
const handleError = (authFunction, push) => { | |
try { | |
return authFunction | |
} catch (error) { | |
return push('/', { authenticated: false, error }) | |
} | |
} | |
/* | |
Callback handlers... | |
- auth is the CognitoAuthSDK instance | |
- props contains history from react-router-dom and a dispatch action for updating the redux state after successfully | |
starting a session with CognitoId | |
*/ | |
const handleSuccess = ({ auth, ...props }) => { | |
auth.userhandler = { | |
onSuccess: result => { | |
return cognitoId.verifySession({ props, auth }) | |
}, | |
onFailure: result => { | |
props.history.push('/', { error: 'Unable to Auth' }) | |
} | |
} | |
} | |
/* Basically exposes methods within the SDK in a common lib for the rest of the app to consume. | |
Each SDK function is wrapped in the handleError method above. */ | |
const cognitoAuthSDK = ({ onError, data, props }) => { | |
const auth = new CognitoAuth(data) | |
const ex = f => onError(f, props.history.push) | |
handleSuccess({ auth, ...props }) | |
return { | |
cached: () => ex(auth.getCachedSession()), | |
lastUser: () => ex(auth.getLastUser()), | |
login: () => ex(auth.getSession()), | |
logout: () => ex(auth.signOut()), | |
parsedUrl: href => ex(auth.parseCognitoWebResponse(href)), | |
user: () => ex(auth.getCurrentUser()), | |
verifySession: () => cognitoId.verifySession({ props, auth }) | |
} | |
} | |
/* Initializes the SDK with the config, error handler, and props from the component its being used in */ | |
export default props => | |
cognitoAuthSDK({ | |
props, | |
data: config, | |
onError: handleError | |
}) | |
/* | |
############################ | |
Step #4: enhancers/withAuth.js - Creates a higher order component to inject the auth libraries into the wrapped component | |
############################ | |
*/ | |
import React, { Component } from 'react' | |
import { aws, compose, getDisplayName } from '../lib' | |
/* an HOC that connects the session from the redux store */ | |
import { connectSession } from './recipes' | |
/* The HOC */ | |
const withAuth = WrappedComponent => { | |
class Auth extends Component { | |
render() { | |
if (!this.props.history || !this.props.location) { | |
return new Error('withAuth is missing required route props') | |
} | |
const authProps = aws.cognitoAuth(this.props) | |
return <WrappedComponent {...authProps} {...this.props} /> | |
} | |
} | |
Auth.displayName = getDisplayName(WrappedComponent, 'withAuth') | |
return Auth | |
} | |
const enhanceWithAuth = compose(connectSession, withAuth) | |
export default enhanceWithAuth | |
/* | |
############################ | |
Step #5: components/Login.js - Starts the auth process | |
############################ | |
*/ | |
import React from 'react' | |
import GoogleButton from 'react-google-button' | |
import { withRouter } from 'react-router-dom' | |
import { withAuth } from '../../enhancers' | |
const Login = ({ login, session }) => | |
!session ? <GoogleButton onClick={login} type="light" /> : null | |
export default withAuth(Login) | |
/* | |
############################ | |
Step #6: routes/callback.js - The configured callback route | |
############################ | |
*/ | |
import React, { Component } from 'react' | |
import { Redirect } from 'react-router-dom' | |
import { withAuth } from '../../enhancers' | |
/** | |
The callback route is used to handle callbacks from external services such as authentication providers. | |
Providers currently configured to use /callback: | |
* AWS Cognito User Pool | |
How it works: | |
When successfully authenticating with a social provider, the Cognito User Pool redirects the user to this | |
route with temporary tokens in the url hash. Since it is a fresh page reload upon redirect, once the component | |
mounts, we call `parsedUrl` from the auth API to parse the hash and initiate starting a session with CognitoId. | |
After successfully initiating a session with CognitoId, `session` is set in the redux store, which triggers | |
the route components to receive updated props, which triggers a redirect to the routes index. | |
When a user initiates logging out, the Cognito User Pool redirects the user to this route upon successfully | |
closing the current session and removing the tokens from storage. When this callback happens, there is no hash | |
in the url, which triggers a redirect to the route index. If a user navigates to /callback manually, they will | |
also be redirected to the route index. | |
The `withAuth` enhancer provides `parsedUrl` and `session` to this route. | |
*/ | |
class CallbackRoute extends Component { | |
state = {} | |
/** If a hash is in the URL, start the CognitoId auth process */ | |
componentDidMount() { | |
if (this.props.location.hash) { | |
this.props.parsedUrl(window.location.href) | |
} | |
} | |
/** Redirect the user to the route index after starting a session with CognitoId */ | |
componentWillReceiveProps(next) { | |
if (!this.props.session && next.session) { | |
this.setState({ redirect: true }) | |
} | |
} | |
render() { | |
if (!this.props.location.hash || this.state.redirect) { | |
return <Redirect to="/" /> | |
} | |
return <div /> | |
} | |
} | |
export default withAuth(CallbackRoute) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Love you <3