I cant figure out why but when I use cognito with my own custom user context everything works just fine but as soon as I use withAuthenticator higher order component it breaks my user context and I cant for the life of me figure out why, or even how to fix it. Ill post my user context file below for reference and tell you where it breaks.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
//TODO must redo cognito from scratch and will probably be able to keep this user context untouched
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser().then(x=>setUser(x)).catch((err)=>setUser(null))
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false)
Auth.signIn(username, password)
.then( x => {
setUser(x)
console.log('Welcome: ' + x.challengeParam.userAttributes.email)
setUserEmail(x.challengeParam.userAttributes.email)
setSignInError(false)
})
.catch((err)=>{
console.log(err.code)
if(err.code === 'UserNotFoundException' || 'NotAuthorizedException'){
err.message = 'Invalid username or password'
setSignInError(true)
console.log(err.message)
}
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
return x
})
}
const signup = (username, email, password) => {
Auth.signUp({ username, password, attributes: { email } })
.then( x => {
setUser(x)
return x
})
.catch((err)=>{
if(err.code){
err.message = 'Your Username or Password was incorrect'
}
throw err
})
}
const vals = useMemo( () => ({user, login, logout, signup, handleSignInError, userEmail, signInError}), [user, userEmail, signInError])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}
Under the login function it now returns user not found after I wrap a component and npm i aws-amplify-react. The funny thing is when I uninstall it I still get the same error and cant go back without fully removing amplify and going through a complete amplify init again. Even more confusing, My app is hosted on vercel and that breaks after I attempt to do this on my local machine. If im not missing something there and my app does break in the cloud even though I dont push my modified code then im guessing cognito is getting something in the cloud when I attempt this on my local machine and then screwing up my untouched copy on vercel????? Since then Ive also tried using next-auth which makes me think I should just stick to front end work or find a better solution? any help would be appreciated. Ill revert to my old setup and rebuild my cognito and amplify from scratch just to get it going again.
You need to call Cognito configure prior to calling your auth provider. Place it before you define your auth provider or context.
Auth.configure({...your_config})
const UserContext = () => {};
I also use a auth hook with my context that removes the need for a HOC.
import { useContext } from 'react';
export const useAuth = () => useContext(UserContext);
// use it in components and pages
const user = useAuth();
Ensure that your configuration is using all of the proper types. If you don't, it sometimes fails silently. For example ENV files are always passed as strings so some options must be cast to the proper type like cookie expires
{
authenticationFlowType: 'USER_SRP_AUTH',
cookieStorage: {
...other settings
expires: Number(process.env.NEXT_PUBLIC_COGNITO_COOKIE_EXPIRES),
}
};
You will also need to call Auth.configure on every page that you need access to Congito auth inside of getStaticPaths, getStaticProps, and getServerSideProps. This is because they are independently called from your app during build or on a server.
Auth.configure({...your_config})
const getStaticProps = () => {};
const getStaticPaths = () => {};
const getServerSideProps = () => {};
If you can use it, their hosted UI is pretty good.
Lastly, AWS has a few libraries for Amplify and I use #aws-amplify/auth - I don't know if this makes a difference.
I added the config file to my _app.js and set ssr: true for ssr authentication
import Amplify from 'aws-amplify'
import config from '../src/aws-exports'
Amplify.configure({...config, ssr: true})
Here is my working user context. I removed the signup function and will add it later once i work on it and test it.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
const [sub, setSub] = useState(null)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser()
.then(x=>{
setUser(x.username)
setUserEmail(x.attributes.email)
setSub(x.attributes.sub)
})
.catch((err)=>{
console.log(err)
setUser(null)
})
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false);
Auth.signIn(username, password)
.then((x) => {
setUser(x.username)
setSignInError(false)
console.log(x)
})
.catch((err)=>{
console.log(err)
setSignInError(true)
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
setSub(null)
})
}
}
const vals = useMemo( () => ({user, sub, login, logout, handleSignInError, userEmail, signInError}), [user, userEmail, signInError, sub])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}
Related
I'm working on a web application with react and Next.js and I also have a Node.js API separated as a back-end.
I have a login form where I send the data to the API to recover JWT, when I do that, everything works fine, but after redirecting the user to the protected route "dashboard", or after a refresh the user context gets lost.
Here is the protected route :
import React, {useContext, useEffect} from 'react'
import { useRouter } from "next/router";
import { Context } from '../../context/context';
export default function DashboardIndexView() {
const router = useRouter();
const {isUserAuthenticated, setUserToken, userToken} = useContext(Context);
useEffect(() => {
isUserAuthenticated()
? router.push("/dashboard")
: router.push("/authentication/admin");
}, []);
return (
<>
<h1>Dashboard index view</h1>
</>
)
}
and here is the context file :
import React, {createContext, useEffect, useState} from 'react'
import { useRouter } from "next/router";
import axios from 'axios'
export const Context = createContext(null)
const devURL = "http://localhost:4444/api/v1/"
export const ContextProvider = ({children}) => {
const router = useRouter()
const [user, setUser] = useState()
const [userToken, setUserToken] = useState()
const [loading, setLoading] = useState(false)
const [successMessage, setSuccessMessage] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const Login = (em,pass) => {
setLoading(true)
axios.post(devURL+"authentication/login", {
email : em,
password : pass
})
.then((res)=>{
setSuccessMessage(res.data.message)
setErrorMessage(null)
setUser(res.data.user)
setUserToken(res.data.token)
localStorage.setItem('userToken', res.data.token)
localStorage.setItem('user', res.data.user)
setLoading(false)
})
.catch((err)=>{
setErrorMessage(err.response.data.message)
setSuccessMessage(null)
setLoading(false)
})
}
const Logout = () => {
setUserToken()
setUser()
localStorage.setItem('userToken', null)
localStorage.setItem('user', null)
router.push('/authentication/admin')
}
const isUserAuthenticated = () => !!userToken
return (
<Context.Provider value={{
Login,
user,
loading,
userToken,
setUserToken,
Logout,
successMessage,
setSuccessMessage,
setErrorMessage,
isUserAuthenticated,
errorMessage}}>
{children}
</Context.Provider>
)
}
How can I keep the user on the dashboard page even when a refresh happens ?
It's normal for useContext() to lose its value on a page refresh. Contexts don't persist any data, they simply share the data between components. In, Next.js, it can work between pages because Next.js handles navigation on the client side. But as you've noticed, as soon as you refresh, the app is mounted from scratch and this time the context never gets the value of the JWT because the JWT was never sent on this new instance of your app.
The solution, at a high-level, is to store the JWT somewhere (localStorage or cookie) and inject the value in your Context.Provider. You're already setting the values in localStorage now you just need a useEffect that will read them and add them to the context:
useEffect(() => {
setUser(localStorage.get('user'));
setUserToken(localStorage.get('userToken'));
}, []);
But the real solution, in my opinion, is to use https://next-auth.js.org/ instead. It handles security concerns and is a well-known library for Next.js
I'm using firebase in my React app.
I've got a problem when my app is deployed to the web. (via github-pages).
The strange part here is that it's telling me that I don't have permissions while my firebase rules are set to allow all incoming requests (see image).
Does anyone know how to solve this issue / know why this is happening?
When I press my "Get Tokens" button I request data from the usersData collection with the following query:
const {user, logout, deleteSignedUser} = UserAuth();
async function getDataFromUser() {
// we get the documentId of the user with:
// docId = user.uid; The uid of the user is the same as the uid of the document in the users' collection.
const docRef = doc(firestoreDB, 'usersData', user.uid).withConverter(tokensConverter);
// we get the document
const docSnap = await getDoc(docRef);
// print the document to the console
console.log(docSnap.data());
// we get the data from the document and set it to the states
setTokens(docSnap.data().tokens); // we set the tokens to the state
setUsername(docSnap.data().username); // we set the username to the state
}
const [tokens, setTokens] = useState(undefined);
const [username, setUsername] = useState(undefined);
return(
<>
{tokens && <h4>Tokens available: {tokens}</h4>}
{username && <h4>Username: {username}</h4>}
<Button onClick={getDataFromUser}
variant='primary'
className="col-6">
Get Tokens
</Button>
</>
);
My AuthContext:
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
sendPasswordResetEmail,
reauthenticateWithCredential,
deleteUser,
EmailAuthProvider,
} from 'firebase/auth';
import { auth } from '../services/firebase';
const UserContext = createContext(undefined);
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({});
// sign up a new user with email and password
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
};
// login an existing with email and password
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
}
// reset password
const resetPassword = (email) => {
return sendPasswordResetEmail(auth, email);
}
// logout the user
const logout = () => {
return signOut(auth)
}
// delete the user
const deleteSignedUser = async (password) => {
const credential = EmailAuthProvider.credential(auth.currentUser.email, password)
const result = await reauthenticateWithCredential(auth.currentUser, credential)
await deleteUser(result.user)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
console.log(currentUser);
setUser(currentUser);
});
return () => {
unsubscribe();
};
}, []);
return (
<UserContext.Provider value={{ createUser, user, logout, signIn, resetPassword, deleteSignedUser}}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
My firebase rules
What it should do (this is on localhost)
Error when deployed on the web (github-pages)
I had started this project as a copy from my first project made with react and firebase.
I had edited my project id in the files you can see listed below, but forgot to change this id in my .env.production.local file.
So while I was testing with my database on localhost I was using the correct id from the .env.development.local file, but the moment I published my project with github-pages it tried using the other file with the wrong id which of course doesn't work.
Short version: Make sure your .env files have the right data / are filled in for both local and public deploy.
.env files
A field for my project id
Example Firebase config where I was filling in my project id from the .env file
I have a nextjs app. I want to authenticate users with firebase. But I want some pages to client-side render and some pages to server-side render. But When I am using this Hook in _app.tsx all pages are rendered on the client side.
How can I use this hook on a specific page so that only that page renders on the client side?
_app.tsx
function MyApp({ Component, pageProps }: AppProps) {
return (
<UserAuthContentProvider>
<Layout>
<Component {...pageProps} />
</Layout></UserAuthContentProvider>
);
}
AuthContext Hook
export const auth = getAuth(app);
const AuthContext = createContext<any>({});
export const useAuthContextProvider = () => useContext(AuthContext);
export const UserAuthContentProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const router = useRouter();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
setIsUserAuthenticated(true);
} else {
setUser(null);
setIsUserAuthenticated(false);
}
setLoading(false);
});
return () => unsubscribe();
});
};
const signUp = async (email: string, password: string) => {
await createUserWithEmailAndPassword(auth, email, password).then((result) => {
if (!result.user.emailVerified) {
router.push("/verification");
} else {
router.push("/dashboard");
}
});
};
const logIn = async (email: string, password: string) => {
await signInWithEmailAndPassword(auth, email, password).then((result) => {
if (!result.user.emailVerified) {
router.push("/verification");
} else {
router.push("/dashboard");
}
});
};
const logOut = async () => {
setUser(null);
await auth.signOut().finally(() => {
router.push("/");
});
};
return (
<AuthContext.Provider
value={{
user,
logIn,
signUp,
logOut,
isUserAuthenticated,
}}
>
{loading ? null : children}
</AuthContext.Provider>
);
If you look in the NextJS Data Fetching docs here they go over which hook to use to trigger which pages you want to render and where.
If you want certain pages to server render you use getServerSideProps if you want to do a regular React runtime client render you can use getInitialProps and if you want to Static Site Generate at server build time you use getStaticProps. Depending on which hook you use on which Page is what determines the NextJS rendering strategy.
In development mode NextJS always uses SSR for developer experience so if you want to test you will need to run npm run build && npm run start.
If you only want a certain page to do one of the rending strategies maybe you can put the rending hook with the strategy you want as a noop on that page.
Since that hook is in the _app it will always run during all strategies so the pages always have that data hydrated. Depending on the strategy will depend on how often that data updates or when its referenced during the build cycle.
I've got an auth context which handles login / logout, saves a token and user details in state as well as localStorage.
An axios instance (authAxios) used in the app intercepts all requests from within this AuthContext.
There's another context provider nested inside AuthContext which makes fetch requests upon login, and uses the authAxios instance. It requires the token to be available with authAxios based on the role level of user (admin, staff, etc). The inner context provider makes its requests from a useEffect with dependencies [ token / roleLevel ] that it's consuming from the AuthContext provider.
The problem arises when the app sees a user log out and a different user (of a different roleLevel) log in. The inner context provider's useEffect fires its request with authAxios, which strangely doesn't have the updated token. Logging the token from AuthContext spits out the new token, but authAxios is still with the old token for that first request even for the new user.
Code below:
/* authAxios */
import axios from "axios";
import {apiEndpoint} from "./constURLs";
const authAxios = axios.create({
baseURL: apiEndpoint,
});
export default authAxios;
/*************/
/* AuthContext.tsx */
import authAxios from "./authAxios";
export const AuthContext = React.createContext<AuthContextState>(defaultState);
export const AuthContextProvider = (children: React.ReactNode) => {
const [state, setState] = React.useState<AuthContextState>(retrieveFromStorage());
/* auth functionality */
React.useMemo(() => {
console.log("token", ctxState.token); // new token available here for the first request
authAxios.interceptors.request.use(req =>
if (ctxState.token) {
req.headers.Authorization = `Token ${ctxState.token}`;
}
return req;
}, err => {
if (DEBUG) {
console.error("authAxios error", err);
}
return Promise.reject(err);
})
}, [ctxState.token])
return (
<AuthContext.Provider value={state}>{children}</AuthContext.Provider>
)
/*************/
/* InnerContext.tsx */
export const InnerContext = React.createContext<InnerCtxState>(defaultState);
export const InnerContextProvider = (children: React.ReactNode) => {
const [state, setState] = React.useState<InnerCtxState>(retrieveFromStorage());
const {token} = React.useContext(AuthContext);
const updateState = React.useCallback((key: string) => {
authAxios.get() /* fetch request here */
}, [token]);
React.useEffect(() => {
if (token) {
updateState("foo");
}
}, [token, updateState])
return (
<InnerContext.Provider value={state}>{children}</InnerContext.Provider>
)
}
/*************/
/* App.tsx */
const App = () => {
return (
<AuthContextProvider>
<InnerContextProvider>
<Router/>
</InnerContextProvider>
</AuthContextProvider>
)
}
I've tried to attach the token to authAxios from localStorage directly, but even that doesn't seem to do the trick.
The interim workaround I'm using is to force reload through window.location.reload() in logoutHandler() in AuthContext
Thanks in advance.
I'm trying to find a way to access the creationTime and lastSignInTime described in this documentation.
Are there any examples of using it within react hooks?
I can't make sense of the firebase documentation generally - it's just words on a page. I think it is designed for people who intuitively know how to fill in the blanks. I remain mystified as to how to do that in general.
I can access auth.user.email using a react hook as follows:
import React, { useState, useEffect, useContext, createContext } from "react";
import firebase from "../firebase";
import {auth} from "../firebase";
const authContext = createContext();
// Provider wraps app and makes auth object available by useAuth().
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Hook to get the auth
export const useAuth = () => {
return useContext(authContext);
};
// Provider hook that creates auth state
function useProvideAuth() {
const [user, setUser] = useState(null);
const signin = (email, password) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(response => {
setUser(response.user);
return response.user;
});
};
const signup = (email, password) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(response => {
setUser(response.user);
return response.user;
});
};
const signout = () => {
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
});
};
const sendPasswordResetEmail = email => {
return firebase
.auth()
.sendPasswordResetEmail(email)
.then(() => {
return true;
});
};
const confirmPasswordReset = (code, password) => {
return firebase
.auth()
.confirmPasswordReset(code, password)
.then(() => {
return true;
});
};
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
return () => unsubscribe();
}, []);
return {
user,
signin,
signup,
signout,
sendPasswordResetEmail,
confirmPasswordReset
};
}
Now, I'm trying to figure out what I need to do to either access the string values described here or the timestamps described here.
I tried each of (all guesses):
{auth.user.UserMetadata().creationTime}
{auth.user.creationTime}
{auth.user.UserMetadata.creationTime}
This works.
{auth.user.metadata.creationTime}
I don't understand why. The references in the firebase documentation refer to metadata as UserMetadata. I don't know how to find the piece of information that tells people to make the leap between UserMetadata and metadata.
If anyone knows what the key to this is, I'd be forever grateful for the insight.