I am using #azure/msal-react and #azure/msal-browser for authentication in our application. So far i have intialized a msal instance and used it to acquire a token and fetch the alias. Everything works until i do a page refresh i want to understand if this is a cache issue i see data is present in local storage regarding msal but still getting null in active account.
here you can see the msal instance with configuration (authProvider)
AuthProvider.js
import * as APIConstants from "./Common/APIConstants";
import {
BrowserCacheLocation,
PublicClientApplication,
LogLevel
} from '#azure/msal-browser';
const loggerCallback = (logLevel, message) => {
console.log(message);
};
const configuration = {
auth: {
authority: APIConstants.TENANT,
clientId: APIConstants.CLIENT_ID,
postLogoutRedirectUri: window.location.origin,
redirectUri: window.location.origin,
validateAuthority: true,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: BrowserCacheLocation.localStorage,
storeAuthStateInCookie: true,
},
system: {
loggerOptions: {
loggerCallback,
logLevel: LogLevel.Info,
piiLoggingEnabled: false
}
}
};
export const authProvider = new PublicClientApplication(configuration);
Login.js
import React, { Component } from "react";
import { authProvider } from "../../authProvider";
import * as APIConstants from "../../Common/APIConstants";
import {
EventType,
} from '#azure/msal-browser';
class Login extends Component {
constructor() {
super()
this.successEventId = null;
}
loginSuccessCallback = (event) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload;
const account = payload.account;
authProvider.setActiveAccount(account);
this.props.history.push("/dashboard");
}
}
componentDidMount = () => {
this.successEventId = authProvider.addEventCallback(this.loginSuccessCallback);
}
componentWillUnmount = () => {
if (this.successEventId)
authProvider.removeEventCallback(this.successEventId);
}
onLDAPLogin = () => {
authProvider.loginRedirect({
scopes: [APIConstants.RESOURCE_URI],
})
}
render() {
return (
< div >
<button onClick={() => this.onLDAPLogin()}>LDAP Login</button>
</div>
);
}
}
export default Login;
dashboard.js
purpose dashboard.js is to collect data through api call where you use alias and token of that user to make the call
const getAllData = async () => {
let user = await Services.getLogedInUser();
if (user) {
let username = user.username;
let alias = username.split("#")[0];
let token = await Services.getAccessToken();
if (token) {
await initiateApiCall(alias,token);
} else {
props.history.push("/");
}
}
else {
props.history.push("/");
}
}
Services.js
export const getAccessToken = async () => {
const activeAccount = authProvider.getActiveAccount();
const accounts = authProvider.getAllAccounts();
if (!activeAccount && accounts.length === 0) {
/*
* User is not signed in. Throw error or wait for user to login.
* Do not attempt to log a user in outside of the context of MsalProvider
*/
}
const request = {
scopes: [APIConstants.RESOURCE_URI],
account: activeAccount || accounts[0]
};
const authResult = await authProvider.acquireTokenSilent(request);
console.log('auth result acquire token ', authResult);
return authResult.accessToken
};
export const getLogedInUser = async () => {
console.log('authProvider', authProvider);
console.log('all accounts ', authProvider.getAllAccounts());
let account = authProvider.getActiveAccount();
console.log('account ', account);
return account;
};
As it is shown in the console i get both active account and all accounts value but in case i refresh the page active account is null and all accounts is an empty array
Related
I am moving my package from react-adal to #azure/msal-react. In react-adal I can authorise and able to go my app. I am using same client_id and Tenant_id but seems like getAllAccounts() returns me empty array, it means no user found as a result I am not getting any token. I used exactly same what the doc says. I am not sure what I am making mistake.
Here is my setup
import { Configuration, PopupRequest, PublicClientApplication } from '#azure/msal-browser'
export const msalConfig: Configuration = {
auth: {
clientId: process.env.NEXT_PUBLIC_MSAL_CLIENT_ID || '',
redirectUri: process.env.NEXT_PUBLIC_MSAL_REDIRECT_URL,
authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_MSAL_TENANT}`,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: 'localStorage', // This configures where your cache will be stored
storeAuthStateInCookie: false,
},
}
export const loginRequest: PopupRequest = {
scopes: ['User.Read'],
}
export const msalInstance = new PublicClientApplication(msalConfig)
const currentAccounts = msalInstance.getAllAccounts()
console.log({ currentAccounts }) // returns empty array
This is how I warp my app with MsalProvider
import { ApolloProvider } from '#apollo/client'
import { MsalProvider } from '#azure/msal-react'
import { defaultClient } from 'apollo'
import { msalInstance } from 'msal-auth-config' // import msalInstance from config
import type { AppProps } from 'next/app'
import React from 'react'
const App = ({ Component, pageProps }: AppProps): JSX.Element => {
return (
<MsalProvider instance={msalInstance}>
<ApolloProvider client={defaultClient}>
<App />
</ApolloProvider>
</MsalProvider>
)
}
export default App
Here I want to return token
const authLink = setContext((_operation, { headers }) => {
const accounts = msalInstance.getAllAccounts()
//console.log({ accounts, headers })
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0])
}
return msalInstance
.acquireTokenSilent(loginRequest)
.then((response) => {
console.log(response) // return undefined
return { headers: { ...headers, Authorization: `Bearer ${response.idToken}` } }
})
.catch((error) => {
if (error instanceof InteractionRequiredAuthError) {
return msalInstance.acquireTokenRedirect(loginRequest)
}
return
})
})
Have you try to make it like this
import { useState, useEffect } from "react";
import { useMsal } from "#azure/msal-react";
import { InteractionStatus } from "#azure/msal-browser";
const { instance, accounts, inProgress } = useMsal();
const [loading, setLoading] = useState(false);
const [apiData, setApiData] = useState(null);
useEffect(() => {
if (!loading && inProgress === InteractionStatus.None && accounts.length > 0) {
if (apiData) {
// Skip data refresh if already set - adjust logic for your specific use case
return;
}
const tokenRequest = {
account: accounts[0], // This is an example - Select account based on your app's requirements
scopes: ["User.Read"]
}
// Acquire an access token
instance.acquireTokenSilent(tokenRequest).then((response) => {
// Call your API with the access token and return the data you need to save in state
callApi(response.accessToken).then((data) => {
setApiData(data);
setLoading(false);
});
}).catch(async (e) => {
// Catch interaction_required errors and call interactive method to resolve
if (e instanceof InteractionRequiredAuthError) {
await instance.acquireTokenRedirect(tokenRequest);
}
throw e;
});
}
}, [inProgress, accounts, instance, loading, apiData]);
if (loading || inProgress === InteractionStatus.Login) {
// Render loading component
} else if (apiData) {
// Render content that depends on data from your API
}
Read more here
you are probably missing the handleRedirectPromise...
once the redirect is done the promise should catch the account... if not try another aquireSilentToken to catch it in the promise below.
instance.handleRedirectPromise().then(resp => {
if (resp && resp.account) instance.setActiveAccount(resp.account);
});
The user is set and persists on all the pages but when the browser window is refreshed the user is logged out. Not Sure if the problem is how I'm setting the cookies, any advice or an alternative is very welcomed.
Here the UserContext file:
import { destroyCookie } from "nookies";
import { setCookie } from "nookies";
import React from "react";
// import { useCookies } from "react-cookie"
import cookie from "cookie";
import { Person, tokenAuth } from "../auth";
import { LoginFormValues } from "../components/Auth/LoginForm";
import { noop } from "../helpers";
import { COOKIE_AUTH_NAME, MONTH } from "../helpers/user";
interface UserContextProps {
user?: User;
logout: () => void;
login: (values: LoginFormValues) => Promise<void>;
}
const UserContext = React.createContext<UserContextProps>({
user: undefined,
logout: noop,
login: async () => noop(),
});
export const UseUserContext = () => React.useContext(UserContext);
interface UserContextProviderProps {
children: React.ReactNode;
user?: User;
}
export const UserContextProvider = ({
children,
user: initialUser,
}: UserContextProviderProps) => {
const [user, setUser] = React.useState<User | undefined>(initialUser);
const login = async (values: LoginFormValues) => {
try {
const tokenResponse = await tokenAuth(values);
if (tokenResponse.status === 200) {
const { token } = tokenResponse.data;
setCookie(null, COOKIE_AUTH_NAME, token, {
maxAge: 3600,
path: "/",
sameSite: "strict",
});
const userResponse = await Person();
if (userResponse.status === 200) {
setUser(userResponse.data);
}
}
} catch (error) {
let errorMessage = "failed";
if (error instanceof Error) {
errorMessage = error.message;
}
console.error(errorMessage);
}
};
const logout = () => {
setUser(undefined);
destroyCookie(null, COOKIE_AUTH_NAME);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
Im wondering if my problem is in my _app.tsx Im also using a Django rest api to retrieve the data this how I'm calling the api. This is what the file looks like:
import axios, { AxiosResponse } from "axios";
import { parseCookies } from "nookies";
export function getJwtToken() {
return sessionStorage.getItem("jwt");
}
async function request<T>(
method: "GET" | "POST" | "PATCH" | "DELETE" | "PUT" | "OPTIONS",
url: string,
data?: Record<string, unknown>
): Promise<AxiosResponse<T>> {
const cookies = parseCookies();
const { authToken } = cookies;
const baseUrl = process.env.NEXT_PUBLIC_API_URL;
const response: AxiosResponse = await axios({
method,
url: `${baseUrl}${url}`,
data,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
axios.defaults.headers.common["Authorization"] = authToken;
return response;
}
interface TokenData {
token: string;
}
export function tokenAuth({ email = "", password = "" }) {
return request<TokenData>("POST", "/api/client/login", {
email,
password,
});
}
export function Person() {
return request<User>("GET", `/api/client/user`);
}
This file is pretty basic and I don't think its the route of the problem.
I created a nextjs web application with Auth0.
A user can login and logout. I can reach also user information. But I cannot see user roles.
I am getting the profile with Auth0's getSession method.
I want to create 2 different pages in NextJS which are for user and admin roles.
So it will be basic API authorization that I want. If a user is in user role then the user can access only the user page. If a user has admin role then the user can access all pages.
To I achive this I need to get user roles.The getSession method does not return the user roles.
How can solve the problem?
Thanks
I'm sure you've figured it out by now, but I had the same question.
You have to explicitly add the roles to your tokens (Auth and id) if you want them attached to the user object.
You can do this multiple ways but I did this by adding a rule in Auth0:
function setRolesToUser(user, context, callback) {
const namespace = 'http://my-website-name.com';
const assignedRoles = (context.authorization || {}).roles;
let idTokenClaims = context.idToken || {};
let accessTokenClaims = context.accessToken || {};
idTokenClaims[`${namespace}/roles`] = assignedRoles;
accessTokenClaims[`${namespace}/roles`] = assignedRoles;
context.idToken = idTokenClaims;
context.accessToken = accessTokenClaims;
callback(null, user, context);
}
Now your token(s) will have the rule attached as a namespaced array w/ the assigned roles in said array.
Now that your tokens have the roles attached you can use it to gate content on Nextjs like this:
import { getSession } from '#auth0/nextjs-auth0';
export async function getServerSideProps({ req, res }) {
const session = getSession(req, res);
if (!session?.user['http://my-website-name/roles'].includes('admin')) {
return { props: { error: 'Forbidden' } };
} else {
return {
props: {},
};
}
}
Then in your component, you can return early like so:
if (error) return <div>{error}</div>;
This explains server side but you can do similar client-side w/ useUser:
import { useUser} from '#auth0/nextjs-auth0';
For this you have to add rule in auth0 dashboard: https://auth0.com/docs/customize/rules. I wrote a custom rule function in this post
Then write a HOC:
function withAuth<T>(Component: React.ComponentType<T>) {
return (role: string) => {
return (props: T) => {
// CUSTOM HOOK TO GET THE USER
const { data, loading } = useGetUser();
if (loading) {
return <p>Loading...</p>;
}
if (!data) {
return <Redirect to="/api/v1/login" />;
} else {
if (role && !isAuthorized(data, role)) {
return <Redirect to="/api/v1/login" />;
}
return <Component user={data} loading={loading} {...props} />;
}
};
};
}
export default withAuth;
isAuthorized:
import { Claims } from "#auth0/nextjs-auth0/dist/session/session";
export const isAuthorized = (user: Claims, role: string) => {
return (
user &&
// user[process.env.AUTH0_NAMESPACE + "/roles"] &&
user["https://yourdomain.auth.com" + "/roles"].includes(role)
);
};
useGetUser Hook
export const useGetUser = () => {
// "/api/v1/me" is passed to fetcher as url
const { data, error, ...rest } = useSWR("/api/v1/me", fetcher);
return { data, error, loading: !data && !error, ...rest };
};
fetcher for useSWR
export const fetcher = (url: string) =>
fetch(url).then(
async (res: Response): Promise<any> => {
const result = await res.json();
if (res.status !== 200) {
return Promise.reject(result);
} else {
return result;
}
}
);
/api/v1/me api function in pages.
// you should already have set auth) instance
import auth0 from "#/utils/auth0";
import { NextApiRequest, NextApiResponse } from "next";
export default async function me(req: NextApiRequest, res: NextApiResponse) {
try {
await auth0.handleProfile(req, res);
} catch (error) {
console.log("error in v1/me ", error);
res.status(error.status || 400).end(error.message);
}
}
After this set up you can wrap your components with withAuth
const Dashboard: React.FC<DashboardProps> = ({ user, loading }) => {
return (
<BaseLayout
>
<BasePage >
{/* YOUR COMPONENT LOGIC HERE */}
</BasePage>
</BaseLayout>
);
};
export default withAuth(Dashboard)("admin");
So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.
The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.
This is my HOC code:
(cookie are stored under userToken name)
import React from 'react';
const jwt = require('jsonwebtoken');
const RequireAuthentication = (WrappedComponent) => {
return WrappedComponent;
};
export async function getServerSideProps({req,res}) {
const token = req.cookies.userToken || null;
// no token so i take user to login page
if (!token) {
res.statusCode = 302;
res.setHeader('Location', '/admin/login')
return {props: {}}
} else {
// we have token so i return nothing without changing location
return;
}
}
export default RequireAuthentication;
If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.
You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.
For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.
export function requireAuthentication(gssp) {
return async (context) => {
const { req, res } = context;
const token = req.cookies.userToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/admin/login',
statusCode: 302
}
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
}
}
You would then use it in your page by wrapping the getServerSideProps function.
// pages/index.js (or some other page)
export const getServerSideProps = requireAuthentication(context => {
// Your normal `getServerSideProps` code here
})
Based on Julio's answer, I made it work for iron-session:
import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '#/utils/index'
export const withAuth = (gssp: any) => {
return async (context: GetServerSidePropsContext) => {
const { req } = context
const user = req.session.user
if (!user) {
return {
redirect: {
destination: '/',
statusCode: 302,
},
}
}
return await gssp(context)
}
}
export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))
And then I use it like:
export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
return {
props: {},
}
})
My withSessionSsr function looks like:
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'
const IRON_OPTIONS: IronSessionOptions = {
cookieName: process.env.IRON_COOKIE_NAME,
password: process.env.IRON_PASSWORD,
ttl: 60 * 2,
}
function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, IRON_OPTIONS)
}
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
return withIronSessionSsr(handler, IRON_OPTIONS)
}
export { withSessionRoute, withSessionSsr }
I am still navigating React hooks and contexts and have been attempting to use both to store user information for further usage in other portions of my app after successful authentication from an axios request. With my code that follows, I successfully set the state used in the context, but when the state is accessed following a redirect that occurs directly after setting the value, it comes back as undefined and I'm not sure what is preventing the value from being stored.
Provided is my context and hook (AppSession.js):
import React, { createContext, useContext, useState } from 'react'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const [user, setUser] = useState()
if (user){
console.log("useState: Authenticated")
console.log(user)
} else {
console.log("useState: Not authenticated")
console.log(user)
}
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
**Provided is the axios request and console logs upon successful response (login.js):**
axios.post(
'/api/auth/signin/',
{ email, password },
{
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
}).then((res) => {
console.log(res.data) // {authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com"}
const data = res.data; //
console.log("updateUserState")
setUser(data)
}).then(()=> {
return window.location = '/app/profile/'
}).catch((err) => {
console.log(err)
})
// Console.logs
{ authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com" } // login.js
updateUserState // login.js
useState: Authenticated // AppSession.js
{ authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com" } // AppSession.js
Then the code for profile.js which is the result of redirect to /app/profile with console logs:
import React from 'react'
import { getUserState } from '../../contexts/AppSession'
import Layout from '../../components/Universal/Layout'
export default function Profile(props) {
const checkUser = getUserState()
console.log(checkUser)
console.log(props)
return (
<Layout
title="Signin"
description="TEST"
>
<h1>Protected Page</h1>
<p>You can view this page because you are signed in.</p>
<br />
<b>Check User: {checkUser}</b>
</Layout>
)
}
// Console.logs
useState: Not authenticated // AppSession.js
undefined // AppSession.js (console.log(user))
undefied // profile.js (console.log(checkUser))
As you can see the storage is short-lives as the subsuquent page that loads upon redirect access the user state and it is undefined. Any idea why this might be?