MsalProvider - dynamic msal config - dynamic

I follow the tutorial https://learn.microsoft.com/en-us/azure/developer/javascript/tutorial/single-page-application-azure-login-button-sdk-msal. However, i need to modified the code so that it is able to configure it using the clientId that will be available after is rendered (msal config is retrieved from app level). Is it possible to update the msalConfig in child component?
//index.js
ReactDOM.render(
<Provider store={store}>
<MsalProvider instance={new PublicClientApplication(MSAL_CONFIG)}>
<Router>
<App />
</Router>
</MsalProvider>
</Provider>,
document.getElementById("root")
);

Create a file named authConfig.js in the src folder to contain your configuration parameters for authentication, and then add the following code:
export const msalConfig = {
auth: {
clientId: "Enter_the_Application_Id_Here",
authority: "Enter_the_Cloud_Instance_Id_Here/Enter_the_Tenant_Info_Here",
redirectUri: "Enter_the_Redirect_Uri_Here",
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
}
};
// Add scopes here for ID token to be used at Microsoft identity platform endpoints.
export const loginRequest = {
scopes: ["User.Read"]
};
// The endpoints here for Microsoft Graph API services you'd like to use.
export const graphConfig = {
graphMeEndpoint: "Enter_the_Graph_Endpoint_Here/v1.0/me"
};
This way you can configure it using ClientID.
Here is the doc.

Related

nuxt 3 + msal - non_browser_environment error

Introduction
Hello everyone,
I am trying to implement azure active directory B2c into my nuxt 3 application. Because #nuxtjs/auth-next is not yet working for nuxt 3, I am trying to make my own composable that makes use of the #azure/msal-browser npm package.
The reason I am writing this article is because it is not working. The code I created can be seen below:
Error:
Terminal
[nitro] [dev] [unhandledRejection] BrowserAuthError: non_browser_environment: Login and token requests are not supported in non-browser environments. 21:07:32
at BrowserAuthError.AuthError [as constructor]
Browser console
PerformanceClient.ts:100 Uncaught (in promise) TypeError: this.startPerformanceMeasurement is not a function
at PerformanceClient2.startMeasurement
Code:
file: /composables/useAuth.js
import * as msal from '#azure/msal-browser'
let state = {
applicationInstance: null,
}
export const useAuth = () => {
//config auth
const msalConfig = {
auth: {
clientId: '',
authority: '',
knownAuthorities: [``],
redirectUri: '',
knownAuthorities: ['']
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
}
state.applicationInstance = new msal.PublicClientApplication(msalConfig);
return {
signIn
}
}
const signIn = () => {
//handle redirect
state.applicationInstance
.addEventCallback(event => {
if(event.type == "msal:loginSuccess" && event.payload.account)
{
const account = event.payload.account
state.applicationInstance.setActiveAccount(account)
console.log(account)
}
})
//handle auth redirect
state.applicationInstance
.handleRedirectPromise()
.then(() => {
const account = state.applicationInstance.getActiveAccount()
if(!account) {
const requestParams = {
scopes: ['openid', 'offline_access', 'User.Read'],
}
state.applicationInstance.loginRedirect(requestParams)
}
})
}
file: index.vue
<script setup>
const auth = useAuth();
auth.signIn()
</script>
You need to make sure that you try to login only in the browser because Nuxt runs also server side.
You can check if you are client side with process.client or process.server for server side.
<script setup>
if (process.client) {
const auth = useAuth();
auth.signIn() // Try to sign in but only on client.
}
</script>
NuxtJS/VueJS: How to know if page was rendered on client-side only?

Nuxt 3 JWT authentication using $fetch and Pinia

I'm discovering Nuxt 3 since a few days and I'm trying to do a JWT authentication to a distinct API.
As #nuxtjs/auth-next doesn't seem to be up to date and as I read it was possible to use the new global method fetch in Nuxt 3 instead of #nuxtjs/axios (not up to date also), I thought it won't be too hard to code the authentication myself! But it stays a mystery to me and I only found documentation on Vue project (using Pinia to keep user logged in) and I'm a bit at a lost.
What I would like to achieve:
a login page with email and password, login request send to API (edit: done!)
get JWT token and user info from API (edit: done!) and store both (to keep user logged even if a page is refresh)
set the JWT token globally to header $fetch requests (?) so I don't have to add it to each request
don't allow access to other pages if user is not logged in
Then I reckon I'll have to tackle the refresh token subject, but one step at a time!
It will be really awesome to have some help on this, I'm not a beginner but neither a senior and authentication stuff still frightens me :D
Here is my login.vue page (I'll have to use Vuetify and vee-validate after that but again one step at a time!)
// pages/login.vue
<script setup lang="ts">
import { useAuthStore } from "~/store/auth";
const authStore = useAuthStore();
interface loginForm {
email: string;
password: string;
}
let loginForm: loginForm = {
email: "",
password: "",
};
function login() {
authStore.login(loginForm);
}
</script>
<template>
<v-container>
<form #submit.prevent="login">
<label>E-mail</label>
<input v-model="loginForm.email" required type="email" />
<label>Password</label>
<input v-model="loginForm.password" required type="password" />
<button type="submit">Login</button>
</form>
</v-container>
</template>
The store/auth.ts for now.
// store/auth.ts
import { defineStore } from 'pinia'
import { encodeURL } from '~~/services/utils/functions'
export const useAuthStore = defineStore({
id: 'auth,
state: () => ({
// TODO Initialize state from local storage to enable user to stay logged in
user: '',
token: '',
})
actions: {
async login(loginForm) {
const URL_ENCODED_FORM = encodeURL({
email: loginForm.email,
password: loginForm.password,
});
return await $fetch('api_route', {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: 'POST',
body: URL_ENCODED_FORM
}
}
}
})
i'm gonna share everything, even the parts you marked as done, for completeness sake.
Firstly, you will need something to generate a JWT in the backend, you can do that plainly without any packages, but i would recommend this package for that. Also i'll use objection.js for querying the database, should be easy to understand even if you don't know objection.js
Your login view needs to send a request for the login attempt like this
const token = await $fetch('/api/login', {
method: 'post',
body: {
username: this.username,
password: this.password,
},
});
in my case it requests login.post.ts in /server/api/
import jwt from 'jsonwebtoken';
import { User } from '../models';
export default defineEventHandler(async (event) => {
const body = await useBody(event);
const { id } = await User.query().findOne('username', body.username);
const token: string = await jwt.sign({ id }, 'mysecrettoken');
return token;
});
For the sake of simplicity i didn't query for a password here, this depends on how you generate a user password.
'mysecrettoken' is a token that your users should never get to know, because they could login as everybody else. of course this string can be any string you want, the longer the better.
now your user gets a token as the response, should just be a simple string. i'll write later on how to save this one for future requests.
To make authenticated requests with this token you will need to do requests like this:
$fetch('/api/getauthuser', {
method: 'post',
headers: {
authentication: myJsonWebToken,
},
});
i prefer to add a middleware for accessing the authenticated user in my api endpoints easier. this middleware is named setAuth.ts and is inside the server/middleware folder. it looks like this:
import jwt from 'jsonwebtoken';
export default defineEventHandler(async (event) => {
if (event.req.headers.authentication) {
event.context.auth = { id: await jwt.verify(event.req.headers.authentication, 'mysecrettoken').id };
}
});
What this does is verify that if an authentication header was passed, it checks if the token is valid (with the same secret token you signed the jwt with) and if it is valid, add the userId to the request context for easier endpoint access.
now, in my server/api/getauthuser.ts endpoint in can get the auth user like this
import { User } from '../models';
export default defineEventHandler(async (event) => {
return await User.query().findById(event.context.auth.id)
});
since users can't set the requests context, you can be sure your middleware set this auth.id
you have your basic authentication now.
The token we generated has unlimited lifetime, this might not be a good idea. if this token gets exposed to other people, they have your login indefinitely, explaining further would be out of the scope of this answer tho.
you can save your auth token in the localStorage to access it again on the next pageload. some people consider this a bad practice and prefer cookies to store this. i'll keep it simple and use the localStorage tho.
now for the part that users shouldnt access pages other than login: i set a global middleware in middleware/auth.global.ts (you can also do one that isnt global and specify it for specific pages)
auth.global.ts looks like this:
import { useAuthStore } from '../stores';
export default defineNuxtRouteMiddleware(async (to) => {
const authStore = useAuthStore();
if (to.name !== 'Login' && !localStorage.getItem('auth-token')) {
return navigateTo('/login');
} else if (to.name !== 'Login' && !authStore.user) {
authStore.setAuthUser(await $fetch('/api/getauthuser', {
headers: authHeader,
}));
}
});
I'm using pinia to store the auth user in my authStore, but only if the localstorage has an auth-token (jwt) in it. if it has one and it hasnt been fetched yet, fetch the auth user through the getauthuser endpoint. if it doesnt have an authtoken and the page is not the login page, redirect the user to it
With the help of #Nais_One I managed to do a manual authentication to a third-party API with Nuxt 3 app using client-side rendering (ssr: false, target: 'static' in nuxt.config.ts)
I still have to set the API URL somewhere else and to handle JWT token refresh but the authentication works, as well as getting data from a protected API route with the token in header and redirection when user is not logged.
Here are my finals files:
// pages/login.vue
<script setup lang="ts">
import { useAuthStore } from "~/store/auth";
const authStore = useAuthStore();
const router = useRouter();
interface loginForm {
email: string;
password: string;
}
let loginForm: loginForm = {
email: "",
password: "",
};
/**
* If success: redirect to home page
* Else display alert error
*/
function login() {
authStore
.login(loginForm)
.then((_response) => router.push("/"))
.catch((error) => console.log("API error", error));
}
</script>
<template>
<v-container>
<form #submit.prevent="login">
<label>E-mail</label>
<input v-model="loginForm.email" required type="email" />
<label>Password</label>
<input v-model="loginForm.password" required type="password" />
<button type="submit">Login</button>
</form>
</v-container>
</template>
For the auth store:
// store/auth.ts
import { defineStore } from 'pinia'
const baseUrl = 'API_URL'
export const useAuthStore = defineStore({
id: 'auth',
state: () => ({
/* Initialize state from local storage to enable user to stay logged in */
user: JSON.parse(localStorage.getItem('user')),
token: JSON.parse(localStorage.getItem('token')),
}),
actions: {
async login(loginForm) {
await $fetch(`${baseUrl}/login`, {
method: 'POST',
body: loginForm
})
.then(response => {
/* Update Pinia state */
this.user = response
this.token = this.user.jwt_token
/* Store user in local storage to keep them logged in between page refreshes */
localStorage.setItem('user', JSON.stringify(this.user))
localStorage.setItem('token', JSON.stringify(this.token))
})
.catch(error => { throw error })
},
logout() {
this.user = null
this.token = null
localStorage.removeItem('user')
localStorage.removeItem('token')
}
}
})
I also use the middleware/auth.global.ts proposed by Nais_One.
And this fetch-wrapper exemple I found here as well to avoid having to add token to every requests: https://jasonwatmore.com/post/2022/05/26/vue-3-pinia-jwt-authentication-tutorial-example and it seems to work perfectly. (I just didn't test yet the handleResponse() method).
Hope it can help others :)
That temporary alternative https://www.npmjs.com/package/#nuxtjs-alt/auth is up to date
And that https://www.npmjs.com/package/nuxtjs-custom-auth and https://www.npmjs.com/package/nuxtjs-custom-http work with Nuxt 3 $fetch and no need to use axios
Recently a new package was released that wraps NextAuth for Nuxt3. This means that it already supports many providers out of the box and may be a good alternative to look into.
You can install it via:
npm i -D #sidebase/nuxt-auth
Then it is pretty simple to add to your projects as you only need to include the module:
export default defineNuxtConfig({
modules: ['#sidebase/nuxt-auth'],
})
And configure at least one provider (like this example with Github):
import GithubProvider from 'next-auth/providers/github'
export default defineNuxtConfig({
modules: ['#sidebase/nuxt-auth'],
auth: {
nextAuth: {
options: {
providers: [GithubProvider({ clientId: 'enter-your-client-id-here', clientSecret: 'enter-your-client-secret-here' })]
}
}
}
})
Afterwards you can then get access to all the user data and signin/signup functions!
If you want to have a look at how this package can be used in a "real world" example, look at the demo repo in which it has been fully integrated:
https://github.com/sidebase/nuxt-auth-example
I hope this package may be of help to you and others!
Stumbling on the same issue for a personal project and what I do is declare a composable importing my authStore which is basically a wrapper over $fetch
Still a newb on Nuxt3 and Vue but it seems to work fine on development, still have to try and deploy it though
import { useAuthStore } from "../store/useAuthStore";
export const authFetch = (url: string, opts?: any | undefined | null) => {
const { jwt } = useAuthStore();
return $fetch(url, {
...(opts ? opts : {}),
headers: {
Authorization:`Bearer ${jwt}`,
},
});
};
And then I can just use it in my actions or components
// #/store/myStore.ts
export const useMyStore = defineStore('myStore', () => {
async getSomething() {
...
return authFetch('/api/something')
}
})
// #components/myComponent.vue
...
<script setup lang="ts">
const handleSomething = () => {
...
authFetch('/api/something')
}
</script>
Hope it helps someone !

Self Signed Cert error in Nuxt trying to generate static site locally

I'm building a Vue/Nuxt (2) site running against a .NET Web Api. This site is already deployed in a staging capacity and is building and running as a statically generated site on Netlify. Now, I know it's not quite right as my content is not being rendered into the deployed files so effectively it's running as a SPA. Not quite what I saw happening in Dev at the time 5 weeks ago but I didn't think anything of it, I'd fix it later.
I've finally got a chance to work on this project again and proceeded to make the necessary changes so the content should be fetched via my dynamic route builder in nuxt.config.js (existing) and output during build via the asyncData hook in my pages (new).
Nuxt.config
// Generate dynamic page routes
let dynamicRoutes = async () => {
console.log( `${ process.env.API_BASE_URL }/page/main/generate` );
const fetchedConditions = await axios.get( `${ process.env.API_BASE_URL }/page/main/generate` );
const routesForConditions = fetchedConditions.data.map( ( condition ) => {
return {
route: `/conditions/${ condition.id }/${ condition.urlPath }`,
payload: condition
}
} );
console.log( `${ process.env.API_BASE_URL }/faq/top/generate?count=10` );
const fetchedFaqs = await axios.get( `${ process.env.API_BASE_URL }/faq/top/generate?count=10` );
const routesForFaqs = fetchedFaqs.data.map( ( faq ) => {
return {
route: `/frequently-asked-questions/${ faq.categoryId }/${ faq.id }/${ faq.urlPath }`,
payload: faq
}
} );
const routes = [ ...routesForConditions, ...routesForFaqs ];
return routes;
}
export default {
target: 'static',
ssr: false,
generate: {
crawler: true,
routes: dynamicRoutes
},
server: {
port: 3001
}...
Condition page
async asyncData(ctx) {
util.debug('Async data call...');
if (ctx.payload) {
ctx.store.dispatch("pages/storePage", ctx.payload);
return { condition: ctx.payload };
} else {
const pageResponse = await ctx.store.dispatch('pages/getCurrentPage', { pageId: ctx.route.params.id });
return { condition: pageResponse };
}
}
So far so good except now, when I try to generate the site in development i.e. "npm run generate", the dynamic route generator code cannot reach my local API running as HTTPS and fails with a "Nuxt Fatal Error: self signed certificate".
https://localhost:5001/api/page/main/generate 12:06:43
ERROR self signed certificate 12:06:43
at TLSSocket.onConnectSecure (node:_tls_wrap:1530:34)
at TLSSocket.emit (node:events:390:28)
at TLSSocket._finishInit (node:_tls_wrap:944:8)
at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:725:12)
This worked 5 weeks ago and as far as I am aware I have not changed anything that should impact this. No software or packages have been updated (except windows updates perhaps). The API is still using .NET 5.0 and running on Kestrel using the default self signed cert on localhost (which is listed as valid in Windows). I simply added the payload to the routes, added the asyncData hook, and modified the page code accordingly.
I've Googled a few tidbits up but none have resolved the issue and now I'm at a loss. It really shouldn't be this blimmin opaque in 2022.
Tried disabling SSL via a proxy in nuxt.config;
proxy: {
'/api/': {
target: process.env.API_BASE_URL,
secure: !process.env.ENV === 'development'
}
}
Tried modifying my Axios plugin to ignore auth;
import https from 'https';
export default function ( { $axios } ) {
$axios.defaults.httpsAgent = new https.Agent( { rejectUnauthorized: false } );
}
And a variation of;
import https from 'https';
export default function ( { $axios, store } ) {
const agent = new https.Agent( {
rejectUnauthorized: false
} );
$axios.onRequest( config => {
if ( process.env.dev )
{
config.httpsAgent = agent;
}
} );
}
None of these 'worked for other people' solutions is working for me.
Also, the client side apps (public/admin) themselves have no problem working against my API locally, it's only the route builder within nuxt.config or asyncData code which is throwing this error.
Any suggestions would be appreciated. Happy to add other relevant code if needed, just not sure which atm.

Vuex-OIDC on the dev server

On my app I have installed Vuex-oidc package to implement the authentication using the endpoints that I have got from the backend, and everything is working well on my machine, but I got the request to modify the oidc settings because right now the auth is working only from locale, and is not working from the development server, so I was requested to move these settings from my config/oidc.js file to the Nuxt "runtime".
Here is my config/oidc.js file:
export const oidcSettings = {
authority: 'https://***/***.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_***_SIGNUPORSIGNIN',
clientId: '************',
token_endpoint: 'https://***/***.onmicrosoft.com/oauth2/v2.0/token?p=B2C_***_SIGNUPORSIGNIN',
redirectUri: 'http://localhost:3000/oidc-test/oidc-callback',
responseType: 'id_token ',
scope: 'https://***.onmicrosoft.com/***/Read openid'
}
Then I have this oidc.js file in the store which is using my oidcSettings:
import { vuexOidcCreateStoreModule } from 'vuex-oidc'
import { oidcSettings } from '~/config/oidc'
const storeModule = vuexOidcCreateStoreModule(
oidcSettings,
{
namespaced: true,
dispatchEventsOnWindow: true,
publicRoutePaths: ['/', 'oidc-callback-error']
},
// Optional OIDC event listeners
{
userLoaded: user => console.log('OIDC user is loaded:', user),
userUnloaded: () => console.log('OIDC user is unloaded'),
accessTokenExpiring: () => console.log('Access token will expire'),
accessTokenExpired: () => console.log('Access token did expire'),
silentRenewError: () => console.log('OIDC user is unloaded'),
userSignedOut: () => console.log('OIDC user is signed out')
}
)
export const state = () => (storeModule.state)
export const getters = storeModule.getters
export const actions = storeModule.actions
export const mutations = storeModule.mutations
I am not sure what I am supposed to use or to do for accomplish the request, I was thinking to introduce a .env and retrieve oidcSetting from ther. Can it be a good idea?
I can also see that Nuxt has his own runtimes which can be declared in nuxt.config.js, such as "publigRuntimeConfig: {}" and "privateRuntimeConfig: {}", but I cannot find much on it, just the basic way to use them, but I am no sure which of my settings should be private and which should be public, and also I am not sure how to call them in my store/oidc.js file.
Some suggestion?
Do you have these settings
export const oidcSettings = {
authority: 'https://your_oidc_authority',
clientId: 'your_client_id',
redirectUri: 'http://localhost:1337/oidc-callback',
responseType: 'id_token token',
scope: 'openid profile'
}
I can only suggest to visit https://github.com/perarnborg/vuex-oidc/wiki
I am not sure this is the best practice to get the result I want, but it works:
What I have done is to use an .env file and hold all the informations there:
AUTHORITY = https://***/***.onmicrosoft.com/v2.0/.well-known/...
CLIENT_ID = ************
TOKEN_ENDPOINT = https://***/***.onmicrosoft.com/oauth2/v2.0/token?...
REDIRECT_URI = http://localhost:3000/oidc-test/oidc-callback
RESPONSE_TYPE = id_token
SCOPE = https://***.onmicrosoft.com/***/Read openid
Then in my nuxt.config.js I have added require('dotenv').config() in the very top of the file (#nuxtjs/dotenv and dotenv packages must be installed and #nuxtjs/dotenv has to be declared in the modules and in the buildModules in your nuxt.config.js)
Now you can delete the config/oidc.js file and you can edit the store/oidc.js:
Instead of having:
import { oidcSettings } from '~/config/oidc'
You can write:
const oidcSettings = {
authority: process.env.AUTHORITY,
clientId: process.env.CLIENT_ID,
token_endpoint: process.env.TOKEN_ENDPOINT,
redirectUri: process.env.REDIRECT_URI,
responseType: process.env.RESPONSE_TYPE,
scope: process.env.SCOPE
}

Vue Cli 3 and Firebase service worker registration

I've used Vue-cli 3 to create a Vue app and I've been trying to incorporate FCM into it. However, I've been working on it for two days and I still cannot get it working.
First, here's my
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase- app.js');
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');
var config = {
messagingSenderId: "69625964474"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function (payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload)
// Customize notification here
const notificationTitle = 'Background Message Title';
const notificationOptions = {
body: 'Background Message body.',
icon: '/firebase-logo.png'
}
return self.registration.showNotification(notificationTitle, notificationOptions)
});
```
One solution that sorta works is I moved this file into the public folder and register it in App.vue using
const registration = await navigator.serviceWorker.register(`${process.env.BASE_URL}firebase-messaging-sw.js`)
messaging.useServiceWorker(registration)
However, then I'll be having two service workers (the other one from Vue itself).
I tried to modify vue.config.js instead trying to work with Workbox by adding the following config:
module.exports = {
pwa: {
name: 'My App',
themeColor: '#4DBA87',
msTileColor: '#000000',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc is required in InjectManifest mode.
swSrc: 'public/firebase-messaging-sw.js'
// ...other Workbox options...
}
}
}
And then register it again in App.vue:
const registration = await navigator.serviceWorker.register(`${process.env.BASE_URL}service-worker.js`)
messaging.useServiceWorker(registration)
Then I got the following error instead:
If you are confused by the files I mentioned or how the directory of my project looks like, what I did was simply creating a PWA using vue-cli 3. And I left most of the structure untouched.
And I set up firebase in main.js:
import firebase from '#firebase/app'
Vue.config.productionTip = false
const config = {
apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
authDomain: process.env.VUE_APP_AUTH_DOMAIN,
databaseURL: process.env.VUE_APP_DATABASE_URL,
projectId: process.env.VUE_APP_PROJECT_ID,
storageBucket: process.env.VUE_APP_STORAGE_BUCKET,
messagingSenderId: process.env.VUE_APP_MESSAGING_SENDER_ID
}
firebase.initializeApp(config)
Then in App.vue:
import firebase from '#firebase/app'
import '#firebase/messaging'
const messaging = firebase.messaging()
messaging.usePublicVapidKey('PUBLIC_KEY')
The service worker is by default disabled in development mode, so running it in development will cause an HTML error page to be returned, this is the reason you are getting text/html error
You can find detailed explanation here LINK