I am working on the authentication of my app and I have managed to add login to my page. Users are able to login and their session is stored however as soon as I refresh the page their session is gone.
ReactJs + NextJS
I know there is getTokenSilently but it will return this when I call it!
error: "login_required"
error_description: "Login required"
state: "N3B+aWt4T1dBeGlibWsua2ZkdX5LTzR6T19ndTdXfkJ2Tm5kUzJIY3lXTQ=="
What am I doing wrong here?
My profile page!
useEffect(() => {
if (typeof window !== `undefined`) {
if (!loading && !isAuthenticated) {
loginWithRedirect({})
}
}
});
Home page which shows an icon if user is logged in!
<Button
className="account-button"
variant="textButton"
icon={<i className="flaticon-user" />}
aria-label="login"
title={loading ? 'loading' : isAuthenticated ? 'Hi' : 'login'}
/>
Auth service
// src/react-auth0-spa.js
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "#auth0/auth0-spa-js";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
if (window.location.search.includes("code=") &&
window.location.search.includes("state=")) {
const { appState } = await auth0FromHook.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await auth0FromHook.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await auth0FromHook.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await auth0Client.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
The problem was using Brave Browser!!!!!! Detailed description here:
Right. So the silent authentication issue, the “login required” error, is what you get when your browser does not, or cannot, send the “auth0” cookie. This is the cookie that Auth0 leaves on the browser client once the user has a session with Auth0, i.e. the user has logged in through an interactive flow. You should be able to confirm this by looking at the network logs or analyzing the HAR output. The scenarios that work will have the cookie attached, whereas the ones that fail will not. If that’s the case, this is neither a sample nor SDK issue as they are not involved in the setting of that cookie; it is issued by the authorization server.
If the browser cannot sent this cookie, it’s most likely because of some software or browser extension or something which is blocking third-party tracking cookies. Safari does this by default thanks to its Intelligent Tracking Prevention (ITP2) 1 software that is built-in. This can explain why silent auth works in Chrome’s incognito mode but not in normal mode. If you’re running some extensions, you might want to disable some of them to narrow down which one is preventing the cookie from being sent.
What I can’t readily explain is how it works in Safari’s Private mode, as I thought ITP2 would block such cookies regardless. Let me get some clarity on that.
https://community.auth0.com/t/failed-silent-auth-login-required/33165/24
Related
My web app uses Firebase Auth to handle user authentication along with a backend API, these are provided to the React app as a provider. The idea is that the backend API will verify the user's token when they sign in and deal with any custom claims / data that needs to be sent to the client.
The problem I'm having is that the provider is rerendering multiple times during the login flow, and each rerender is making an API call. I've managed to get the amount of rerenders down to two, but if I add other 'features' to the provider (e.g update the user's state if their access should change) then this adds to the amount of rerenders, sometimes exponentially, which leads me to suspect that the provider is rerendering as a result of setUserState being called, perhaps unnecessarily. Either way, it is clearly indicative of a problem somewhere in my code, which I've included below:
import {useState, useContext, createContext, useEffect} from 'react'
import {auth, provider} from './firebase'
import {getAuth, onAuthStateChanged, signInWithPopup, signOut} from 'firebase/auth'
import {api} from './axios'
export const UserContext = createContext(null)
export const useAuth = () => useContext(UserContext)
const verifyToken = token => {
return api({
method: 'post',
url: '/verifyToken',
headers: {token}
})
}
const UserProvider = props => {
const [userState, setUserState] = useState(null)
const [loading, setLoading] = useState(true)
const userSignedOut = async () => {
setLoading(true)
return await signOut(auth).then(() => {
setUserState(null)
}).catch(e => {
console.error(e)
}).finally(() => {
setLoading(false)
})
}
const userSignIn = async () => {
console.log('userSignIn')
setLoading(true)
try {
return await signInWithPopup(auth, provider)
} catch (e) {
console.error(e)
} finally {
if (!userState) {
setLoading(false)
}
}
}
const handleUserSignIn = async user => {
console.log('handleUserSignIn', user)
if (user && getAuth().currentUser) {
setLoading(true)
const idToken = await getAuth().currentUser.getIdToken(true)
const firebaseJWT = await getAuth().currentUser.getIdTokenResult()
if (!firebaseJWT) {throw(new Error('no jwt'))}
verifyToken(idToken).then(res => {
if (res.data.role !== firebaseJWT.claims.role) {
throw(new Error('role level claims mismatch'))
} else {
user.verifiedToken = res.data
console.log(`user ${user.uid} valid and token verified`, user)
setUserState(user)
setLoading(false)
}
}).catch(e => {
userSignedOut()
console.error('handleUserSignIn', e)
}).finally(() => {
setLoading(false)
})
} else {
console.log('no user')
userSignedOut()
}
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async user => {
console.log('onAuthStateChanged')
if (user?.uid && user.accessToken) {
await handleUserSignIn(user)
} else {
setUserState(null)
setLoading(false)
}
})
return () => unsubscribe()
}, [])
const value = {
signOut: userSignedOut, // for sign out button
signIn: userSignIn, // for sign in button
user: userState
}
return (
<UserContext.Provider value={value}>
{props.children}
</UserContext.Provider>
)
}
export default UserProvider
I tried to create a codesandbox for this, but unfortunately I was unable to simulate the Firebase auth functions.
The login flow is supposed to look like this:
The user signs in using their Google account.
The app is now loading, and the user cannot interact with it yet (they just get a spinning wheel).
The user's data and accessToken are sent to the backend API server. (function verifyToken)
The API server sets any custom claims and returns the verified token in its response, as well as the access that the user is supposed to have.
If the user's role / custom claims do not match what the API says they should be, the user is signed out.
The user's data is set using setUserState()
The app has finished loading, and the user is signed in.
I would like to avoid unnecessary rerenders and API calls and I suspect that some refactoring may be in order, but I'm not really sure what is best to do here.
I think I should ask this question, cause I face some very weird problem.
I have a protected route, /app, if user is not login reached this page, redirect to /auth/login, so I have this, common Context hooks:
const { user, isLoading } = useAuth()
useEffect(() => {
if (!(user || isLoading)) {
// HERE REDIRECT TO /auth/login
}
}, [user, isLoading])
//here make sense
Now in my useAuth(), I have this:
const useAuth = () => {
const [user, setUser] = useState(null)
useEffect(() => {
firebase.auth().onAuthStateChanged((currentUser) => {
setIsLoading(false)
if (currentUser) {
console.log(currentUser)
//here
getUserFromMyServer(currentUser.uid).then(res => {
let userDetails = res.detail
setUser(userDetails) // here will get User data from server
}).catch(error => setUser(null))
} else {
setUser(null)
}
})
}, [])
return {
user,
isLoading,
loginWithTwitter, // a function will mention later
getTwitterRedirectResult, //a function will mention later
logout // a function
}
}
This is what I call to server
// here is the getUserFromMyServer function in another files, it look like this:
export const getUserFromMyServer = async () => {
const path = "/user"
const firebaseToken = await firebase.auth().currentUser.getIdToken()
console.log(firebaseToken)
const myInit = {
headers: {
Authorization: `Bearer ${firebaseToken}`,
}
}
return API.get(apiName, path, myInit) // Here return a promise
}
OK, so far is working well. When done login, it will setUser(), then will redirect from /auth/login to /app according to the user, this is what I want.
Now when I implement Sign in with Twitter, it comes in a problem.
The behavior: When done Sign in with Twitter, it redirect to /app. Then when /app is loaded for 20s like that then it go back to /auth/login again. This is keep repeating, non-stop until the browser hang.
Here is my code for Sign in with Twitter.
useAuth.js (same file with code above with loginWithTwitter)
const [operation, setOperation] = useState('login')
const loginWithTwitter = () => {
setOperation(type)
let provider = getTwitterProvider()
return firebase.auth().signInWithRedirect(provider)
}
const getTwitterRedirectResult = () => {
return firebase
.auth()
.getRedirectResult()
.then((result) => {
console.log(result)
var credential = result.credential;
var token = credential.accessToken;
var secret = credential.secret;
var twitterHandle = result.additionalUserInfo.username
if (operation === 'sign-up') {
// if sign up, here save to my db
createUserInDb(twitterHandle, token, secret).then(res=> {
setUser(res.data)
}).catch(error => setUser(null))
}
if (operation === 'login') {
// here get back the user details same as above
getUser()
}
}).catch((error) => {
console.log(error.message)
setErrorMessage(error.message)
setUser(null)
});
}
Then in my /auth/login and /auth/sign-up I have this code:
const Login = () => {
const { user, getTwitterRedirectResult } = useAuth() //
useEffect(() => {
getTwitterRedirectResult() // here call to twitter redirect result
}, [])
useEffect(() => {
if (user) {
// code redirect to app
router.push('/app')
}
})
}
So now, when login/sign up with password, is no problem. Once login, go to /app. then stay there.
But when sign in with Twitter, it go to /app. Stay there for 20s, then it redirect back to /auth/login. Basically is infinite loop.
So now my question:
What is the correct way to call getTwitterRedirect in react? Is it call at useEffect() when the page load?
How to solve the infinite loop? Done login go to /app, then it go back to /auth/login again.
How to save user data to my own db when getting the result back from getTwitterRedirect. So when Sign up I need to create the record, but when login with Twitter, I just need to get back the same record. But right now, it only have 1 function getTwitterRedirect, login and sign up also use this. So how can I solve this?
Please someone tell me what I doing wrong, I have totally no clue.
Thanks in advance.
I am trying to conditionally render a React Native screen dependent on whether 1) the screen is loading whilst the app checks for authentication status 2) the user is already authenticated thereby displaying the home screen 3) the user is not authenticated thereby displaying the login/signup screen.
I've been largely success implementing this, however, in the case of (2), the Signup screen is very briefly displayed until the correct Homepage screen is shown.. which is annoying. Here is my code:
const App = () => {
const [isReady, setIsReady] = useState(false)
const [user, setUser] = useState({})
useEffect(() => {
const checkAuth = async () => {
try {
await Auth.getCurrentAuthenticatedUser()
.then(user => setUser(user))
} catch (e) {
console.log('An error occurred.. ', e)
}
}
checkAuth()
setIsReady(true)
}, [])
const showAppLoading = (!isReady && !user)
if (showAppLoading) {
// App is loading
view = <LoadingComponent />
} else if (!user) {
// User not authenticated - go to Authenticator
view = <AuthNavigator />
} else {
// User is authenticated - go to App
view = <App signOut={signOut} />
}
return (
<View>{view}</View>
)
}
It appears that <AuthNavigator /> loads briefly because user state hasn't loaded in time, however, I don't know why this is because setUser() has been called straight after I get the user credentials.
Hope you can help
Try this way.
useEffect(() => {
try {
Auth.getCurrentAuthenticatedUser()
.then(user => {
setUser(user);
setIsReady(true);
});
} catch (e) {
console.log('An error occurred.. ', e)
}
}, [])
I have a very famous problem which I think everybody has at least once tackled. I want to persist the user logged-in in my react app even if the page is refreshed. I have read all the related questions and articles about how this can be done but unfortunately I got nowhere.
In my ProtectedComponent I have the following code:
const ProtectedRoute = ({ notLoggedInPath }) => {
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
return (
<Fragment>
{isLoggedIn && <RecorderPage />}
{!isLoggedIn && <Redirect to={notLoggedInPath} />}
</Fragment>
);
};
As you can see I have implemented a variable so-called isLoggedIn in my initialState of auth reducer and if this variable is true the protected route will be accessible otherwise not.
In my Sign In component I store the received token from the api to the localStorage. This is completely done. But my main question is that when the user signs in and then navigates to a protected route, by refreshing the page my initialState(isLoggedIn) goes away and changes to false, making the user logged out. This is completely natural in the culture of ReactJS. But how can I implement a way in which when my app is being launched, it looks for authenticating the previously received token and if it has not expired yet it navigates the user to the page on which the app is refreshed. This is done by a gigantic number of websites so I know it can be done. But I don't know how?
My sign in component:
const SignInForm = () => {
const dispatch = useDispatch();
const history = useHistory();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = () => {
axios({
method: 'post',
url: 'someurl/api/token/',
data: {
username: username,
password: password,
},
})
.then((res) => {
const token = res.data.access;
localStorage.setItem('token', token);
dispatch(updateUserInStore(token, username));
dispatch(makeUserLoggedIn());
history.push('/recorder');
})
.catch((err) => {
console.log(err);
console.log(err.response);
});
};
return (
<some jsx/>
)
It is worth mentioning that I have also used the hook useEffect in my mother-level component App. I mean when my app launches the callback in useEffect checks if the localStorage token can be authorised or not but because of async nature of js and also axios request this is not a solution since the initialState is set before the response of this axios request is received.
My App component:
const App = () => {
const dispatch = useDispatch();
const history = useHistory();
const tokenLocalStored = localStorage.getItem('token');
const checkIfUserCanBeLoggedIn = () => {
const token = localStorage.getItem('token');
axios({
method: 'get',
url: 'some-url/api/sentence',
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((res) => {
dispatch(makeUserLoggedIn());
})
.catch((err) => {
console.log(err);
console.log(err.response);
return false;
});
};
useEffect(() => {
checkIfUserCanBeLoggedIn();
});
return (
<Some JSX>
)
When the page reloads execute the async logic in useEffect hook on App.js. Use a state like authChecking to show a loader while the auth state is being checked.
const [authChecking, updateAuthChecking] = useState(true)
useEffect(() => {
asyncFunc
.then(updateUserObjToStore)
.finally(() => updateAuthChecking(false))
}, [])
I have also written a article on medium about this feel free to check it out if you have any doubts.
https://medium.com/#jmathew1706/configuring-protected-routes-in-react-using-react-router-af3958bfeffd
Bonus tip: Try to keep this logic in a custom hook will ensure proper separation of concerns.
I am trying to log in a user with Google in my React/Firebase app. I've followed a tutorial on youtube (https://www.youtube.com/watch?v=umr9eNbx3ag) but the results are different. When I click the Log In button, I get redirected to Google, choose an account and then get redirected to my site.
It seems as my 'if' statement never runs, auth.currentUser never evaluates to true.
This is my Firebase file
firebase.initializeApp(firebaseConfig)
export const firestore = firebase.firestore()
export const auth = firebase.auth()
export const provider = new firebase.auth.GoogleAuthProvider()
export const signInWithGoogle = () => auth.signInWithRedirect(provider)
export const signOut = () => auth.signOut()
export default firebase
This is my log in component
import { auth, signInWithGoogle, signOut } from '../../Firebase/Firebase'
const LoginOrRegister = () => {
const { username, setUsername, idToken, setIdToken } = useContext(Context)
useEffect(() => {
auth.onAuthStateChanged(async nextUser => {
if (auth.currentUser) {
setIdToken(await auth.currentUser.getIdToken())
setUsername(auth.currentUser.displayName)
} else {
setIdToken(null)
}
})
}, [])
return (
<div>
<LogInForm>
<button onClick={signInWithGoogle}> Log in with Google </button>
</div>
)
Since you are using signInWithRedirect you need to make use of auth.getRedirectResult() instead of auth.onAuthStateChanged as you are actually navigating away from the app and coming back in
Below code will work or you.
useEffect(() => {
auth
.getRedirectResult()
.then(function(result) {
console.log(result);
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
setToken(token);
// ...
}
// The signed-in user info.
var user = result.user;
console.log(user);
setData(user);
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
console.log(errorCode, errorMessage);
// ...
});
}, []);
You can find the reference documentation here