Skip to content

Commit

Permalink
remove pinia, refactor to use composables instead of plugins, use glo…
Browse files Browse the repository at this point in the history
…bal middleware, upgrade @azure/msal-browser version
  • Loading branch information
magr committed Nov 13, 2023
1 parent 1e0422a commit 2253e85
Show file tree
Hide file tree
Showing 13 changed files with 490 additions and 9,893 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Step 3 :

![05](https://github.com/Akash52/msal-with-nuxt3/assets/31063892/9bc3b61e-6b0e-48af-9702-42055fe0e0ad)

5. Copy the app's essential info, create .env.local a file at the root of your project and set the value of the below .env variables.
5. Copy the app's essential info, create an `.env` file at the root of your project and set the value of the below .env variables.


![IMG_2497](https://github.com/Akash52/msal-with-nuxt3/assets/31063892/7dbea54b-db0f-44e9-b774-c87e87121301)
Expand Down Expand Up @@ -107,7 +107,7 @@ npm run dev
├─ layouts
│  └─ default.vue
├─ middleware
│  └─ auth.ts // Authentication middleware
│  └─ auth.global.ts // Authentication middleware
├─ nuxt.config.ts
├─ package-lock.json
├─ package.json
Expand All @@ -119,7 +119,7 @@ npm run dev
├─ public
│  └─ favicon.ico
├─ stores
│  └─ auth.ts
│  └─ auth.global.ts
├─ tailwind.config.js
└─ tsconfig.json
```
Expand Down
6 changes: 4 additions & 2 deletions components/LoginScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@
</template>

<script setup>
const { $msal } = useNuxtApp();
import {useMSAuth} from "~/composables/useMSAuth";
const msAuth = useMSAuth();
async function login() {
await $msal().signIn();
await msAuth.signIn()
}
</script>

28 changes: 12 additions & 16 deletions components/UserProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
</div>
<div class="justify-center items-center flex mx-auto text-gray-600 font-semibold w-32 h-32 rounded-full bg-blue-200 uppercase :hover:bg-gray-300"
v-else>
{{ user?.name?.match(/[A-Z]/g).join("") }}
{{ userStore.user.name?.match(/[A-Z]/g).join("") }}
</div>

<h3 class="mt-6 text-gray-900 text-sm font-medium">{{ user?.name }}</h3>
<h3 class="mt-6 text-gray-900 text-sm font-medium">{{ userStore.user.name }}</h3>
<dl class="mt-1 flex-grow flex flex-col justify-between">
<dt class="sr-only">Name</dt>
<dd class="text-gray-500 text-sm">{{ user?.username }}</dd>
<dd class="text-gray-500 text-sm">{{ userStore.user.username }}</dd>
<dt class="sr-only">Email</dt>
</dl>
<button @click="logout(user.homeAccountId)"
<button @click="logout(userStore.user.homeAccountId)"
class="absolute bottom-0 right-0 mr-2 mb-2 bg-gray-100 p-2 rounded-lg shadow hover:bg-red-500 text-gray-500 hover:text-white hover:opacity-60 transition-all duration-500 font-extrabold font-mono">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
Expand All @@ -43,27 +43,25 @@
</div>
</template>
<script setup>
import { useUserStore } from "../stores/auth";
import { storeToRefs } from "pinia";
const userStore = useUserStore();
const { user, userRole } = storeToRefs(userStore);
const { $msal } = useNuxtApp();
const isAuthenticated = $msal().isAuthenticated();
import {useMSAuth} from "~/composables/useMSAuth";
import {useAppUser} from "~/composables/useAppUser";
const userStore = useAppUser();
const msAuth = useMSAuth();
const isAuthenticated = msAuth.isAuthenticated();
const profileImage = ref("");
async function logout(accountId) {
if (accountId) {
await $msal().signOut(accountId);
await msAuth.signOut(accountId);
} else {
console.log("No account id");
}
}
const getProfileImage = async () => {
const accessToken = await $msal().acquireTokenSilent({
const accessToken = await msAuth.acquireTokenSilent({
scopes: ["User.Read"],
});
const response = await fetch(
Expand All @@ -84,9 +82,7 @@ const getProfileImage = async () => {
onMounted(async () => {
if (isAuthenticated) {
profileImage.value = await getProfileImage();
userStore.$patch((state) => {
state.userImage = profileImage;
});
userStore.value.userImage = profileImage.value;
}
});
Expand Down
8 changes: 8 additions & 0 deletions composables/useAppUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@


export const useAppUser = () => {
return useState("user", () => ({
user: {} as any,
userImage: null,
}))
}
164 changes: 164 additions & 0 deletions composables/useMSAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {BrowserCacheLocation, EventType, PublicClientApplication} from "@azure/msal-browser";


let tokenExpirationTimer: any;

export const useMSAuth = () => {

const config = useRuntimeConfig();

const msalConfig = {
auth: {
clientId: config.public.clientId,
authority: config.public.authority,
redirectUri: config.public.redirectUri,
postLogoutRedirectUri: config.public.postLogoutRedirectUri,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: true,
},
system: {
tokenRenewalOffsetSeconds: 300,
},
};

let msalInstance = useState('msalInstance',
() => new PublicClientApplication(msalConfig)
);

async function initialize() {
await msalInstance.value.initialize();

// Handle redirect promise after login or redirect
await msalInstance.value
.handleRedirectPromise() // Handles the redirect promise and obtains the response
.then(handleResponse)
.catch((err) => {
throw new Error(err);
});

// Add event callback for login success
msalInstance.value.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS) {
setupTokenExpirationTimer();
}
});

}

// Handle the response after login or redirect
function handleResponse(resp: any) {
if (resp?.account) {
setupTokenExpirationTimer();
} else {
console.log("LOGIN");
}
}

// Set up timer for refreshing access token upon expiration
function setupTokenExpirationTimer() {
const accounts = msalInstance.value.getAllAccounts();
if (accounts.length > 0) {
const account = accounts[0];
if (account.idTokenClaims && account.idTokenClaims.exp) {
const tokenExpirationTime = account.idTokenClaims.exp * 1000;
const currentTime = Date.now();
const timeUntilExpiration = tokenExpirationTime - currentTime;

clearTimeout(tokenExpirationTimer);

tokenExpirationTimer = setTimeout(() => {
refreshAccessToken(account);
}, timeUntilExpiration);
}
}
}

// Refresh access token
async function refreshAccessToken(account: any) {
try {
const response = await msalInstance.value.acquireTokenSilent({
account,
scopes: ["User.Read"],
});
console.log("Refreshed Access Token:", response.accessToken);
setupTokenExpirationTimer();
} catch (err) {
console.error("Token refresh error:", err);
//signOut(account.homeAccountId);
}
}

const loginRequest = {
scopes: ["User.Read"],
};

// Sign in with redirect
async function signIn() {
try {
await msalInstance.value.loginRedirect(loginRequest);
} catch (err) {
console.log("Login error:", err);
}
}

// Acquire access token silently
async function acquireTokenSilent() {
const accounts = msalInstance.value.getAllAccounts();
if (accounts.length > 0) {
const account = accounts[0];
msalInstance.value.setActiveAccount(account);
try {
const response = await msalInstance.value.acquireTokenSilent({
account,
scopes: ["User.Read"],
});
return response.accessToken;
} catch (err) {
return null;
}
} else {
console.error("No accounts found");
return null;
}
}

// Get all MSAL accounts
function getAccounts() {
return msalInstance.value.getAllAccounts();
}

// Check if user is authenticated
function isAuthenticated() {
return getAccounts().length > 0;
}

// Sign out user
function signOut(accountId: string) {
const account = accountId
? msalInstance.value.getAccountByHomeId(accountId)
: null;
if (account) {
msalInstance.value.logoutRedirect({
account,
});
localStorage.clear();
} else {
console.error("Account not found");
}
}


return {
initialize,
msalInstance,
signIn,
getAccounts,
acquireTokenSilent,
isAuthenticated,
signOut,
}

}
19 changes: 9 additions & 10 deletions middleware/auth.ts → middleware/auth.global.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useUserStore } from "~/stores/auth";
import {useMSAuth} from "~/composables/useMSAuth";
import {useAppUser} from "#imports";

export default defineNuxtRouteMiddleware(async (to, from) => {
if (process.server) return;
if (to.name === "/login") return;

const { $msal } = await useNuxtApp();
const accounts = $msal().getAccounts();
const userStore = useUserStore();
const accessToken = await $msal().acquireTokenSilent();
let isAuthenticated = $msal().isAuthenticated() && accessToken;
const msAuth = useMSAuth();
const accounts = msAuth.getAccounts();
const userStore = useAppUser();
const accessToken = await msAuth.acquireTokenSilent();
let isAuthenticated = msAuth.isAuthenticated() && accessToken;

if (isAuthenticated) {
const user = {
Expand All @@ -16,10 +18,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
};

localStorage.setItem("user", JSON.stringify(user));

userStore.$patch((state) => {
state.user = user;
});
userStore.value.user = user;
}
if (to.name !== "login" && !isAuthenticated) {
return navigateTo("/login", { replace: true });
Expand Down
9 changes: 1 addition & 8 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@ export default defineNuxtConfig({

plugins: [{ src: "~/plugins/msal.ts", mode: "client" }],

modules: ["@pinia/nuxt", "nuxt-headlessui"],
modules: ["nuxt-headlessui"],
css: ["~/assets/css/main.css"],

imports: {
dirs: ["./stores"],
},
pinia: {
autoImports: ["defineStore", "acceptHMRUpdate"],
},

runtimeConfig: {
public: {
clientId: process.env.CLIENTID,
Expand Down
Loading

0 comments on commit 2253e85

Please sign in to comment.