I'm trying to implement an SSO react-admin login
Most of the react-admin examples a is simple username/password login and get token then store at storage. But for me, token will return from Auth Provider server and redirect to http://example.com/#/login?token=TOKEN, and make react-admin to parse the URL, and set token in localStorage. Then update React admin store to mark a user as "is logged in,"
but for me I failed to simulate logged in as a hook after validating token from app main js can you help me how to do that
A workaround like below is works for me, but this is quite dirty solution:
const url = window.location;
(() => {
if (localStorage.getItem("access_token") === "null" || localStorage.getItem("access_token") === "undefined") {
localStorage.removeItem("access_token");
}
})();
const access_token = () => {
if (localStorage.getItem("access_token")) {
return localStorage.getItem("access_token")
} else if (new URLSearchParams(url.search).get("access_token")) {
return new URLSearchParams(url.search).get("access_token")
}
}
localStorage.setItem("access_token", access_token());
export const authProvider = {
login: () => {
return Promise.resolve();
},
logout: () => {
localStorage.removeItem("access_token");
return Promise.resolve();
},
checkError: (status) => {
if (status.response.status === 401 || status.response.status === 403) {
localStorage.removeItem("access_token");
return Promise.reject();
}
return Promise.resolve();
},
checkAuth: () => {
return localStorage.getItem("access_token") ? Promise.resolve() : Promise.reject();
},
getPermissions: () => Promise.resolve(),
};
There is no login method call at all, just direct setting token to the localStorage and check with checkAuth method. Maybe, sombody will give better solution.
Related
I'm writing an authentication application in Next Js (v12.2.5). The application also uses React (v18.2.0).
The problem is with persisting the authentication state. When the browser is refreshed, the login session is killed. Why is this happening even though I am getting and setting the token in the local storage. I would like to persist the login session to survive browser refresh and only kill the session when the user logs out.
Application flow
Users authenticates through a login form which calls a Spring API to fetch credentials from a mySQL database. Everything works as expected. The user is able to login, conditional rendering and route protection all function as expected.
Persisting the session relies on the localstorage API to store the JWT token once the user logs in. The Chrome browser console shows that the token is successfully set and stored throughout the authentication process. The get method for getting the initial token also seems to work.
Background
There are several questions on SFO that cover this topic but most seem to cover the use of cookies like this example. This question covers localstorage, but simply says to wrap the token get method is useEffect which doesn't address the actual questions and problems I'm having.
This example also covers localstorage but takes a different approach, using useReducer where my approach is trying to use use Effect. I'm open to restructure my whole application to use useReducer if this is the correct way, but first I want to make sure I understand if I'm taking the right approach.
I also suspect there is a difference between persisting the user state using React and Next. From researching, the difference seems to be in the way Next also includes SSR which may explain why I'm not able to persist the state in Next?
Application code
auth-context.js
const AuthContext = React.createContext({
token: '',
admintoken: '',
isLoggedIn: false,
isAdmin: false,
login: (token) => { },
adminAccess: (admintoken) => { },
logout: () => { },
});
export const AuthContextProvider = (props) => {
useEffect(()=> {
if(typeof window !== 'undefined') {
console.log('You are on the browser');
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
} else {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
}
},[AuthContext])
const [token, setToken] = useState(initialToken);
const [admintoken, setAdminToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const userHasAdmin = !!admintoken;
const loginHandler = (token) => {
setToken(token);
localStorage.setItem('token', token);
console.log("token stored " + token);
};
const logoutHandler = () => {
setToken(null);
localStorage.removeItem('token');
};
const adminTokenHandler = (admintoken) => {
setAdminToken(admintoken);
}
const contextValue = {
token: token,
admintoken: admintoken,
isAdmin: userHasAdmin,
isLoggedIn: userIsLoggedIn,
adminAccess: adminTokenHandler,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
ProtectRoute.js
const ProtectRoute = ({ children }) => {
const authCtx = useContext(AuthContext);
const isLoggedIn = authCtx.isLoggedIn;
if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname == '/') {
return <HomePage />;
} else {
if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname !== '/auth') {
return <RestrictedSection />;
}
else {
console.log("User logged in");
// return <RestrictedSection />;
return children;
}
}
}
export default ProtectRoute;
Authform.js (login page)
const AuthForm = () => {
const emailInputRef = useRef();
const passwordInputRef = useRef();
const [isLoading, setIsLoading] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const router = useRouter();
const authCtx = useContext(AuthContext);
const submitHandler = (event) => {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("username", enteredEmail);
urlencoded.append("password", enteredPassword);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: urlencoded,
redirect: 'follow'
};
fetch(API_LOGIN_URL, requestOptions)
.then(async (res) => {
setIsLoading(false);
if (res.ok) {
return res.json();
} else {
const data = await res.json();
let errorMessage = 'Authentication failed!';
throw new Error(errorMessage);
}
})
.then((data)=> {
authCtx.login(data.access_token);
router.replace('/');
const processedData = JSON.stringify(data);
console.log("Admin status "+ processedData);
for(let i = 0; i < processedData.length; i++) {
if(processedData.includes("ROLE_SUPER_ADMIN")) {
console.log("Found Admin");
authCtx.adminAccess(true);
}
if(processedData.includes("ROLE_USER")) {
console.log("Found User");
break;
}
else {
console.log("Not Found");
}
}})
.catch((err) => {
alert(err.message);
});
};
return (
<section className={classes.auth}>
<h1>Login</h1>
<form onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor='email'>Your Email</label>
<input type='email' id='email' required ref={emailInputRef} />
</div>
<div className={classes.control}>
<label htmlFor='password'>Your Password</label>
<input type='password' id='password' required ref={passwordInputRef} />
</div>
<div className={classes.actions}>
{!isLoading && <button>Login</button>}
{isLoading && <p>Sending request</p>}
</div>
</form>
</section>
);
};
export default AuthForm;
Issue 1
From your code in auth-context.js, you are calling useEffect inside a condition.
if(typeof window !== 'undefined') {
console.log('You are on the browser');
useEffect(()=> {
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
})
} else {
useEffect(()=> {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
})
}
You SHOULD NOT call your useEffect(or any other hook) inside conditions, loops and nested functions.
Doc reference: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level.
Consider moving your conditions code inside the hook.
useEffect(()=> {
if(condition)
{run your localstorage related logic here...}
})
Issue 2
I think you should consider adding a dependency array to your useEffect hook because getting your token on every rerender seems quite expensive.
useEffect(()=> {
if(condition)
{run your localstorage related logic here...}
},[])
Still, its just a suggestion, as I don't know your code in much depth.
Issue 3
The initial token is not getting set in the use effect.
Kindly add setToken(initialToken) in the useEffect after initial token assignment.
initialToken = localStorage.getItem('token');
setToken(initialToken);
The main issue is with you trying to run serverside code on the fronted:
useEffect(()=> {
if(typeof window !== 'undefined') {
console.log('You are on the browser');
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
} else {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
}
},[AuthContext])
The above part of the code will always run on the front end(so you don't need the if part).
If you want to clear your concepts on what part of the code will work on the server and what part will run on the client, kindly refer to these documentations:
SSR: https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props
SSG: https://nextjs.org/docs/basic-features/data-fetching/get-static-props
ISR: https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
I have a Vue.js application and on the /callback route I am trying to have it do a few things. So far I am not having any luck with it because I am seeing things run async. I understand that it is normally how Vue/Javascript works however I am trying to force it to not be async.
The main issue I am having is sometimes the this.$store... are running before the items are set. This is an issue because of how things run on other Vuex actions. Mainly the getCompany one requires the loadToken one to complete as it is pulling the values from the local storage which is being set above.
I don't want to change this and how it works because of how the Vue router is set up to pull the token from local storage on each page reload. This token is used to connect to the backend so it needs to be pulled from local storage each reload as I don't want a user to log in just because they reload the page.
Code:
created() {
setTimeout(() => {
localStorage.setItem('token', this.$auth.token)
localStorage.setItem('user_data', JSON.stringify(this.$auth.user))
// Load company data
this.$store.dispatch('loadToken')
this.$store.dispatch('getCompany')
if(this.$auth == null || this.$auth.id_token['https://hello.io/account_signup_type/is_new']) {
this.$router.push('/setup')
} else {
// Load user data from Auth0
// Go to chat page
this.$router.push('/chat')
}
}, 500)
}
edit main.js code
import { Auth0Plugin } from '#/auth/auth0-plugin';
// Install the authentication plugin
Vue.use(Auth0Plugin, {
domain,
clientId,
audience,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
auth0-plugin
/**
* External Modules
*/
import Vue from 'vue';
import createAuth0Client from '#auth0/auth0-spa-js';
/**
* Vue.js Instance Definition
*/
let instance;
export const getInstance = () => instance;
/**
* Vue.js Instance Initialization
*/
export const useAuth0 = ({
onRedirectCallback = () =>
window.history.replaceState({}, document.title, window.location.pathname),
redirectUri = `${window.location.origin}/callback`,
...pluginOptions
}) => {
if (instance) return instance;
instance = new Vue({
data() {
return {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: {},
error: null,
token: null,
id_token: null
};
},
methods: {
async handleRedirectCallback() {
this.isLoading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (error) {
this.error = error;
} finally {
this.isLoading = false;
}
},
loginWithRedirect(options) {
return this.auth0Client.loginWithRedirect(options);
},
logout(options) {
return this.auth0Client.logout(options);
},
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
}
},
async created() {
this.auth0Client = await createAuth0Client({
...pluginOptions,
// responseType: 'id_token',
domain: pluginOptions.domain,
client_id: pluginOptions.clientId,
audience: pluginOptions.audience,
redirect_uri: redirectUri,
});
try {
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
const { appState } = await this.auth0Client.handleRedirectCallback();
onRedirectCallback(appState);
}
} catch (error) {
this.error = error;
} finally {
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.$auth.getTokenSilently().then(token => this.token = token)
this.$auth.getIdTokenClaims().then(id_token => this.id_token = id_token)
this.isLoading = false;
}
},
});
return instance;
};
/**
* Vue.js Plugin Definition
*/
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
edit - updated router.beforeEach
router.beforeEach(async (to, from, next) => {
const auth = getInstance()
if(to.path == '/callback' && auth != null) {
console.log('Callback')
console.log(`Token: ${auth.token}`)
console.log(`User: ${JSON.stringify(auth.user)}`)
localStorage.setItem('token', auth.token)
localStorage.setItem('user_data', JSON.stringify(auth.user))
await store.dispatch('loadToken')
await store.dispatch('getCompany')
return next()
}
if(to.path != '/login' && to.path != '/setup') {
await store.dispatch('loadToken')
await store.dispatch('getCompany')
.then(() => {
return next()
})
} else {
return next()
}
})
edit - adding guide that I followed from Auth0 to get the code I have now - mostly
https://auth0.com/blog/complete-guide-to-vue-user-authentication/
The problem is that there is race condition because dispatch calls return promises that weren't chained before accessing the result of their work.
A good practice is to chain every promise, unless proven other wise.
The code that created contains actually belongs to the router in general because authentication logic is application-wide.
It's unnecessary to access global dependencies on this component instance. This is done for historical reasons because Vue originally was used in non-modular environment. In order to use outside components, global dependencies such as store need to be explicitly imported. In case this cannot be done, this needs to be fixed.
In this case auth instance is available through getInstance. In case the authentication shouldn't be done on each navigation, this needs to be done on condition, e.g.:
import { getInstance } from '.../auth';
import store from '.../store';
...
router.beforeEach(async (to, from, next) => {
const auth = getInstance();
if (...) {
...
await store.dispatch('loadToken')
await store.dispatch('getCompany')
...
next('/setup')
...
} else {
next()
}
})
getInstance doesn't serve a good purpose because it just exposes a variable. Instead, instance could be exported and imported directly, the behaviour would be the same due to how ES modules work.
Also global store already holds application logic and commonly used to handle authentication, including local storage operations.
Is there any examples and/or way to redirect to private dashboard on successful sign in or sign up for credentials type in next-auth?
I couldn't find any clear docs around this.
I was looking at adding redirect below but wasn't sure if it was the right approach:
callbacks.signIn = async (data, account, profile) => {
if ((account.provider === 'google' && profile.verified_email === true) || (account.type === 'credentials' && data.status === 200)) {
return Promise.resolve(true)
} else {
return Promise.resolve(false)
}
}
This can actually happen when initiating the signin. From the docs., you can pass a callback URL to the sign in. you code will look like this.
signIn(provider.id, {
callbackUrl: `${window.location.origin}/protected`,
})
With the new versions of Next.js you can do the redirect on 'getStaticProps' method like the following,
Resource : https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation
export async function getStaticProps(context) {
// some imperative work..
//
if (!user) {
return {
redirect: {
destination: '/', // some destination '/dashboard' Ex,
permanent: false,
},
}
}
return {
props: {},
}
}
I'm trying to make a redirect if the user has a token saved in cookies or not, here's my code:
const Secret = () => {
const router = useRouter();
const [token] = useState({
token: cookies.get("token") || null,
});
if (token.token == null || token.token.hasOwnProperty("error")) {
router.push("/login");
} else {
router.push("/");
}
return (
<>
<h1>Secret</h1>
</>
);
};
But I get this error: No router instance found. you should only use "next/router" inside the client side of your app.
Anyone knows how to fix it? Thanks!
I think the problem coming from where you put the code in. Your condition is supposed to be happened as the component is mounted, so try this:
React.useEffect(() => {
if (token.token == null || token.token.hasOwnProperty("error")) {
router.push("/login");
} else {
router.push("/");
}
}, [])
I'm using firebase for authentication. Everytime before I do a request to the backend I request for the idToken.
service.interceptors.request.use(async request => {
const token = await firebase.auth().currentUser.getIdToken();
if (!token && request.url !== '/signIn' && request.url !== '/register') {
toast.error('Not authenticated');
return;
}
request.headers.Authorization = 'Bearer ' + token;
return request;
});
Additionally I have a request to the backend that will run in the mounted hook of my vue component.
mounted() {
plantService.getPlants().then(data => (this.suspicionList = data));
}
Usually, this works but the problem is, when I refresh the page, firebase.auth().currentUser is null and the request will fail thus not returning any data.
I already tried creating a computed property and observe that one in a watcher. But not working.
Also, I have an observer for the user in my main.js file like this:
firebase.auth().onAuthStateChanged(user => {
store.dispatch('FETCH_USER', user);
});
Anyone has an idea?
Thanks!
If I correctly understand your problem, the following should do the trick:
mounted() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
plantService.getPlants().then(data => (this.suspicionList = data));
}
});
}