I am trying to secure a Vue.js SPA using msal-browser redirect flow and vue router navigation guards. My solution is highly inspired by this sample from Microsoft's Github.
What I am trying to achieve:
I have a protected resource, ie https://localhost/items, that can only be accessed if the user is authenticated. If I enter that URL in the address bar, my app must make sure the user is authenticated before displaying it. If not authenticated, then the page is redirected to Microsoft's authentication page. If the auth is successful, the user should be redirected back to the page they were trying to access.
The issue(s) that I am facing
I am currently facing two (maybe distinct) issues:
- My code works almost fine. When the user is not authenticated, the page gets redirected to the auth page, and after authentication, the page goes back to the initial page (in this case,
/items). However, when I enter thehttps://localhost/itemsin the address bar, the page/itemsis displayed for a brief amount of time (a glance) before getting redirected to the authentication page. This page should never be displayed unless the user has the rights to view it. The following gif illustrates the behavior (notice the page showing briefly before redirecting at the very beginning):
- The code I have in the navigation guard is very cumbersome (nested
promises/then/catchstatements). I tried re-writing it usingasync/await, since I am using TypeScript, but for some reason, when I use theasync/awaitpattern, the page never gets redirected back to/items. It gets redirected to/redirect, which is a blank page used as theredirectUri.
Environment
- Vue.js: 3.3.4
- Vue router: 4.1.6
- msal-browser: 3.1.0
- typescript
My code
Auth configuration:
import { LogLevel, PublicClientApplication } from "@azure/msal-browser";
const isIE =
window.navigator.userAgent.indexOf("MSIE ") > -1 ||
window.navigator.userAgent.indexOf("Trident/") > -1;
// Config object to be passed to Msal on creation
export const msalConfig = {
auth: {
clientId: "myClientId",
authority: "myAuthority",
redirectUri: "/redirect",
postLogoutRedirectUri: "https://account.microsoft.com/account",
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: isIE,
}
};
export const msalInstance = new PublicClientApplication(msalConfig);
Vue router configuration:
const routes: Array<RouteRecordRaw> = [
{
path: "/items",
name: "items",
component: TheItems,
meta: {
requiresRoles: routeRoleAccess.items,
},
},
{
path: "/redirect",
name: "redirect",
component: TheAuthRedirect,
},
{
path: "/:pathMatch(.*)*",
name: "notFound",
component: TheNotFoundPage,
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes,
});
//!\\ This is the code I was trying to translate to async/await
router.beforeResolve(
async (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
const unauthorizedRoute = { name: "notFound" };
if (!to.meta?.requiresRoles) {
return true;
}
return msalInstance
.acquireTokenSilent(loginRequest) // try to get the token silently in case the user is authenticated
.then(async (authResult) => {
saveAuthData(authResult); // If success save some data (user name, roles) to the store
return hasAccessTo(to.name as string) || unauthorizedRoute; // check if user has enough privileges to access page
})
.catch(() => {
msalInstance
.handleRedirectPromise()
.then(() => {
return msalInstance
.loginRedirect() // If user is not authenticated, redirect
.then(async () => {
const authResult = await msalInstance.acquireTokenSilent( // if success, get the token to save data to store
loginRequest
);
saveAuthData(authResult);
return hasAccessTo(to.name as string) || unauthorizedRoute;
})
.catch(() => {
return unauthorizedRoute;
});
})
.catch(() => {
return unauthorizedRoute;
});
});
}
);
export default router;
What I tried
- Using beforeEach(), beforeResolve(), and beforeEnter() guards
- Refactoring the code in the navigation guard to
try {
const authResult = await msalInstance.acquireTokenSilent(loginRequest);
saveAuthData(authResult);
return hasAccessTo(to.name as string) || unauthorizedRoute;
} catch {
try {
await msalInstance.handleRedirectPromise();
await msalInstance.loginRedirect();
const authResult = await msalInstance.acquireTokenSilent(loginRequest);
saveAuthData(authResult);
return hasAccessTo(to.name as string) || unauthorizedRoute;
} catch {
return unauthorizedRoute;
}
}
However, this code redirects to /redirect (which is a blank page) instead of /items
My way of implementing auth with msal might be incorrect. If so, I will definitely appreciate some help on how to do it properly.
Edit
After @Estus Flask pointed out that I'm not returning the promise in the catch block in the navigation guard, I am now getting the same behavior with the async/await: the page is getting redirected to the /redirect page, which is a blank page. This is an unexpected behavior; the page should be redirected to /items instead.