@wra-gov/vue-msal
v0.1.3
Published
[](https://www.npmjs.com/package/@wra-gov/vue-msal)
Readme
Vue Msal
Vue wrapper around Microsoft's MSAL.js for browser library
Installation
You can install the library,
npm install @wra-gov/vue-msal --saveUsage
Msal Instance
To use msal, a msal instance must be created. Msal instances have need an auth configuration. The user must provide this auth configuration.
The auth and cache objects are important. Objects in system help with debugging and are not required.
import { LogLevel, PublicClientApplication } from "@azure/msal-browser";
// Config object to be passed to Msal on creation
export const msalConfig = {
auth: {
clientId: "", // Fill in
authority: "", // Fill in
redirectUri: "/", // Must be registered as a SPA redirectURI on your app registration
postLogoutRedirectUri: "/" // Must be registered as a SPA redirectURI on your app registration
},
cache: {
cacheLocation: "localStorage"
},
system: {
loggerOptions: {
loggerCallback: (
level: LogLevel,
message: string,
containsPii: boolean
) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
default:
return;
}
},
logLevel: LogLevel.Verbose
}
}
};
export const msalInstance = new PublicClientApplication(msalConfig);Registration
The instance must be registered in index.js / index.ts. The plugin must always be the first thing initialised; all other things must happen after msal has finished initialisation.
To use control routing, the VueNavigationClient must be registered to the application's router,
const navigationClient = new VueNavigationClient(router);
msalInstance.setNavigationClient(navigationClient);msal should be initialised in a promise,
msalInstance.initialize().then(() => {
// This is a standard account configuration. Account configs can vary with
// what the application requires.
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
msalInstance.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload as AuthenticationResult;
const account = payload.account;
msalInstance.setActiveAccount(account);
}
});
const app = createApp(App);
app.use(router);
// The msal plugin is registered with the msalInstance
app.use(msalPlugin, msalInstance);
app.use(Wra);
router.isReady().then(() => {
// Waiting for the router to be ready prevents race conditions when returning from a loginRedirect or acquireTokenRedirect
app.mount("#app");
});
});Guards
@wra-gov/vue-msal uses the meta object for route authorisation.
Guarded routes should have meta.requiresAuth set to true,
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: Home
},
{
path: "/profile",
name: "Profile",
component: Profile,
meta: {
requiresAuth: true
}
},
{
path: "/profileNoGuard",
name: "ProfileNoGuard",
component: ProfileNoGuard
},
{
path: "/failed",
name: "Failed",
component: Failed
}
];After the routes are created, the registerGuard function should be called,
// Example router
const router = createRouter({
history: createWebHistory(),
routes
});
registerGuard(router, msalInstance, loginRequest);
export default router;Composables
There are 3 composables,
useIsAuthenticated- Checks if the user is part of the Entra ID tentant.
- Returns a
boolean.
useMsal- Provides the user`s account details and a msal instance to use.
- Asynchronous composable, requires checking a
inProgressref.
useMsalAuthentication- Accepts a scope parameter.
- Accepts a authentication method. Either,
- Redirect based - this is the WRA`s recommended way as it supports test engines.
- Popup based - not recommended as it requires users to assign permissions and casuses issues with test engines.
- Provides the user`s account details, and a token for the requested scope.
- Asynchronous composable, requires checking a
inProgressref.
useIsAuthenticated
<template>
<div v-if="!isAuthenticated">
Please sign-in to see your profile information.
</div>
<div v-else>
<p class="my-4">You are signed in.</p>
<wra-button @click="goToProfile">Request Profile Information </wra-button>
</div>
</template>
<script setup lang="ts">
import { useIsAuthenticated } from "@wra-gov/vue-msal";
// Returns a boolean reference
// This boolean can change over time
const isAuthenticated = useIsAuthenticated();
</script>useMsal
Accessing account information using useMsal,
<template>
<span v-if="!!name">Name: {{ name }}</span>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useMsal } from "@wra-gov/vue-msal";
const { accounts } = useMsal();
const name = computed(() => {
if (accounts.value.length > 0) {
const name = accounts.value[0].name ?? "";
// Optional: Only get first name
/* if (name) {
return name.split(" ")[0];
} */
}
return "";
});
</script>Using the msal instance for sign in and sign out functionality,
loginRequest is an object containing the scopes for the login,
// Add scopes that are requested for id token provided by MS Identity Platform.
export const loginRequest = {
scopes: ["User.Read"]
};<template>
<wra-app-bar class="nav-bar" id="top-nav-bar">
<router-link class="links" to="/">Home</router-link>
<div class="flex-grow"></div>
<template v-if="isAuthenticated">
<wra-button v-on:click="logoutPopup">Logout Popup</wra-button>
<wra-button v-on:click="logoutRedirect">Logout Redirect</wra-button>
</template>
<template v-else>
<wra-button @click="loginPopup">Login Popup</wra-button>
<wra-button @click="loginRedirect">Login Redirect</wra-button>
</template>
</wra-app-bar>
</template>
<script setup lang="ts">
import { useIsAuthenticated, useMsal } from "@wra-gov/vue-msal";
import { loginRequest } from "../authConfig";
const { instance } = useMsal();
const isAuthenticated = useIsAuthenticated();
const logoutPopup = () => {
instance.logoutPopup({
mainWindowRedirectUri: "/"
});
};
const logoutRedirect = () => {
instance.logoutRedirect();
};
const loginPopup = () => {
instance.loginPopup(loginRequest);
};
const loginRedirect = () => {
instance.loginRedirect(loginRequest);
};
</script>Using the msal instance through useMsal to get access tokens.
I would recommend using
useMsalAuthenticationover this,
loginRequest is an object containing the scopes for the graph query,
// Add scopes that are requested for id token provided by MS Identity Platform.
export const loginRequest = {
scopes: ["User.Read"]
};graphConfig is an object that contains the graph URLs,
// Add here the endpoints for MS Graph API services you would like to use.
export const graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};<template>
<div v-if="state.resolved">
<p>Name: {{ state.data.displayName }}</p>
<p>Title: {{ state.data.jobTitle }}</p>
<p>Mail: {{ state.data.mail }}</p>
<p>
Phone: {{ state.data.businessPhones ? state.data.businessPhones[0] : "" }}
</p>
<p>Location: {{ state.data.officeLocation }}</p>
</div>
<div v-else><p>Getting state</p></div>
</template>
<script setup lang="ts">
import { useMsal } from "@wra-gov/vue-msal";
import {
InteractionRequiredAuthError,
InteractionStatus
} from "@azure/msal-browser";
import { reactive, onMounted, watch } from "vue";
import { loginRequest } from "../authConfig";
import { callMsGraph } from "../utils/MsGraphApiCall";
import UserInfo from "../utils/UserInfo";
const { instance, inProgress } = useMsal();
const state = reactive({
resolved: false,
data: {} as UserInfo
});
async function getGraphData() {
const response = await instance
.acquireTokenSilent({
...loginRequest
})
.catch(async (e) => {
if (e instanceof InteractionRequiredAuthError) {
await instance.acquireTokenRedirect(loginRequest);
}
throw e;
});
if (inProgress.value === InteractionStatus.None) {
const graphData = await callMsGraph(response.accessToken);
state.data = graphData;
state.resolved = true;
stopWatcher();
}
}
onMounted(() => {
getGraphData();
});
const stopWatcher = watch(inProgress, () => {
if (!state.resolved) {
getGraphData();
}
});
</script>useMsalAuthentication
To use the useMsalAuthentication, an interaction type and scope must be passed in.
Interaction types:
InteractionType.PopupInteractionType.Redirect
// Add scopes that are requested for id token provided by MS Identity Platform.
export const loginRequest = {
scopes: ["User.Read"]
};graphConfig is an object that contains the graph URLs,
// Add here the endpoints for MS Graph API services you would like to use.
export const graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};<template>
<div v-if="state.resolved">
<p>Name: {{ state.data.displayName }}</p>
<p>Title: {{ state.data.jobTitle }}</p>
<p>Mail: {{ state.data.mail }}</p>
<p>
Phone: {{ state.data.businessPhones ? state.data.businessPhones[0] : "" }}
</p>
<p>Location: {{ state.data.officeLocation }}</p>
</div>
</template>
<script setup lang="ts">
import { useMsalAuthentication } from "@wra-gov/vue-msal";
import { InteractionType } from "@azure/msal-browser";
import { reactive, watch } from "vue";
import { loginRequest } from "../authConfig";
import { callMsGraph } from "../utils/MsGraphApiCall";
import UserInfo from "../utils/UserInfo";
const { result, acquireToken } = useMsalAuthentication(
InteractionType.Redirect,
loginRequest
);
const state = reactive({
resolved: false,
data: {} as UserInfo
});
async function getGraphData() {
if (result.value) {
const graphData = await callMsGraph(result.value.accessToken).catch(() =>
acquireToken()
);
state.data = graphData;
state.resolved = true;
}
}
getGraphData();
watch(result, () => {
getGraphData();
});
</script>Options API
All of the composables listed above can be used in options API. However, they have to be placed into a setup method. The setup method places it's returns into the data() method automatically.
<template>
<div v-if="state.resolved">
<p>Name: {{ state.data.displayName }}</p>
<p>Title: {{ state.data.jobTitle }}</p>
<p>Mail: {{ state.data.mail }}</p>
<p>
Phone: {{ state.data.businessPhones ? state.data.businessPhones[0] : "" }}
</p>
<p>Location: {{ state.data.officeLocation }}</p>
</div>
</template>
<script>
import { useMsalAuthentication } from "@/composables/UseMsalAuthentication";
import { apiRequest } from "@/config/auth";
import { InteractionType } from "@azure/msal-browser";
export default {
data() {
return {
state: {
resolved: false,
data: {}
}
};
},
setup() {
const { acquireToken, result, error, inProgress } = useMsalAuthentication(
InteractionType.Redirect,
apiRequest
);
return {
acquireToken,
result,
error,
inProgress
};
},
watch: {
inProgress: async function (newValue) {
if (newValue === false) {
await this.getGraphData();
}
}
},
methods: {
getGraphData() {
if (result.value) {
const graphData = await callMsGraph(result.value.accessToken).catch(() =>
acquireToken()
);
this.state.data = graphData;
this.state.resolved = true;
}
}
}
};
</script>Sample / Test
The test folder contains an sample project, containing a typical WRA project structure with Tailwind and the WRA component library.
main.tssetups up the MSAL pluginsauthConfig.tsprovides thePublicClientApplicationthat is used bymain.tsto setup configuration and hooks.router/router.tssets up role guards.
To run the sample / tests, you must navigate into the test folder. The top level of the repo is for the plugin, not the sample.
cd test
npm install
npm run dev