Skip to content

Instantly share code, notes, and snippets.

@gabeweaver
Last active January 20, 2024 15:03
Show Gist options
  • Save gabeweaver/d1be9f0d41069437f576c375c30e134c to your computer and use it in GitHub Desktop.
Save gabeweaver/d1be9f0d41069437f576c375c30e134c to your computer and use it in GitHub Desktop.
React + Cognito User Pools + Cognito Identity JS Example
/*
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)
@spendres
Copy link

Thanks for sharing...

@kseniyalan
Copy link

Great work! Thanks!

@wgeorgecook
Copy link

Was able to take some of this to use really, really easily. Thank you for sharing!

@ThomasVuillaume
Copy link

ThomasVuillaume commented May 4, 2019

Thanks for sharing ! I was just wondering two things before using what you manged to do :

  1. Did you have any reason why not using Aws-Amplify ? (I found this one earlier, and was not really enthousiast pulling a full framework just to do login stuff...)
  2. Did you know this package was archived : https://github.com/amazon-archives/amazon-cognito-identity-js ? In step #2, which one is referenced ? The archived one or this one : https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js

@omarome
Copy link

omarome commented Apr 28, 2022

Awesome work, thanks for sharing.

@leqnam
Copy link

leqnam commented Aug 11, 2022

Thanks for sharing ! I was just wondering two things before using what you manged to do :

  1. Did you have any reason why not using Aws-Amplify ? (I found this one earlier, and was not really enthousiast pulling a full framework just to do login stuff...)
  2. Did you know this package was archived : https://github.com/amazon-archives/amazon-cognito-identity-js ? In step #2, which one is referenced ? The archived one or this one : https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js

@ThomasVuillaume : The amazon-cognito-identity-js was archived on Feb 13, 2018 and this gist was updated on Jan 2018 and your concern was 2019. 😏

@alesus97
Copy link

alesus97 commented Dec 6, 2022

Love you <3

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