Solving AWS Amplify NEW_PASSWORD_REQUIRED with React App Context
The Problem
I recently ran into a really silly situation trying to use a React UI to login to an Amazon Web Services (AWS) Cognito User Pool. For my use-case, users are created with a temporary password, and placed into a “Force change password” aka NEW_PASSWORD_REQUIRED
state in my AWS Cognito pool.
The trouble was, once I’d login with the temporary password, all of the Auth.currentAuthenticatedUser()
or Auth.userSession()
, et. al. functions would error when trying to fetch from AWS. \n
I stumbled upon this github issue which directed me to ensure I was handling the user.challengeName
variable by redirecting to a /update-password
route in my app. This was a pain but made sense.
Once this was implemented, I worked on passing the CognitoUser
through to the /update-password
route. Cursor suggested I do it I pass it through URL state, which seemed logical enough for me.
After implementing that however, passing the CognitoUser
to AWS with the Auth.completeNewPassword(cognitoUser, newPassword)
function kept failing. “Must be something with passing the object itself through, no problem, I’ll just JSON.stringify
it on the way in, and JSON.parse
it on the way out. But lo and behold when trying to reconstruct the CognitoUser
and pass it in to completeNewPassword
and we’d get user.completeNewPasswordChallenge is not a function
user.completeNewPasswordChallenge is not a function
The github comments seemed to mention that storing the object via Redux or Amplify Cache was the right move.
Adding a whole new library just to reset a password seemed ridiculous. Turns out React App Context is just fine!
The below code examples will show you how I did it. It probably could be improved but on the off-hand someone runs into the same issue, maybe this will save them some time.
The Code
- App Context (
contextLib.ts
):
/* eslint-disable @typescript-eslint/no-empty-function */
import { CognitoUser } from 'amazon-cognito-identity-js'
import { createContext, useContext } from 'react'
isAuthenticated: boolean
export interface AppContextType {
isAuthenticated: boolean
userHasAuthenticated: React.Dispatch<React.SetStateAction<boolean>>
cognitoUser: CognitoUser | null
setCognitoUser: React.Dispatch<React.SetStateAction<CognitoUser | null>>
}
export const AppContext = createContext<AppContextType>({
isAuthenticated: false,
userHasAuthenticated: () => {},
cognitoUser: null,
setCognitoUser: () => {},
})
export function useAppContext() {
return useContext(AppContext)
}
function App() {
const [isAuthenticated, userHasAuthenticated] = useState(false)
const [cognitoUser, setCognitoUser] = useState<CognitoUser | null>(null)
<AppContext.Provider value={{ isAuthenticated, userHasAuthenticated: setIsAuthenticated }}>
return (
<AppContext.Provider value={{ isAuthenticated, userHasAuthenticated, cognitoUser, setCognitoUser }}>
<QueryClientProvider client={queryClient}>
<AppContent />
</QueryClientProvider>
</AppContext.Provider>
)
}
Login Component (Login.tsx
):
export default function Login() {
const nav = useNavigate()
const { userHasAuthenticated, setCognitoUser } = useAppContext()
const [isLoading, setIsLoading] = useState(false)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
function validateForm() {
function validateForm() {
return email.length > 0 && password.length > 0
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsLoading(true)
setError('')
try {
console.log('Attempting to sign in...')
const user = await Auth.signIn(email, password)
console.log('Sign in result:', user)
const userName = user.username
if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
console.log('New password required')
// Pass userName and cognitoUser to the reset-password route
setCognitoUser(user)
nav('/reset-password')
} else {
// Normal sign-in flow
await Auth.currentSession()
const currentAuthenticatedUser = await Auth.currentAuthenticatedUser({ bypassCache: true })
console.log('Current authenticated user:', currentAuthenticatedUser)
nav('/')
userHasAuthenticated(true)
nav('/')
}
} catch (error) {
Reset Password Component (ResetPassword.tsx
):
export default function ResetPassword() {
const { cognitoUser, userHasAuthenticated } = useAppContext()
const nav = useNavigate()
const [isLoading, setIsLoading] = useState(false)
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [error, setError] = useState('')
const [error, setError] = useState('')
function validateForm() {
return newPassword.length > 0 && confirmPassword.length > 0 && newPassword === confirmPassword
}
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsLoading(true)
setError('')
setError('')
if (newPassword !== confirmPassword) {
setError('Passwords do not match')
setIsLoading(false)
return
}
}
try {
if (!cognitoUser) {
setError('User information is missing. Please try logging in again.')
return
}
if (!cognitoUser) {
console.log('Attempting to complete new password challenge...')
await Auth.completeNewPassword(cognitoUser, newPassword)
console.log('Password reset successful')
userHasAuthenticated(true)
nav('/')
} catch (error) {
console.error('Error resetting password:', error)
onError(error)
setError('Failed to reset password. Please try again.')
} finally {
setIsLoading(false)
}
}