FirebaseUI auth redirecting before Firestore document is created - vue.js

Setup: Vue.js, Vuetify, FirebaseUI, Firestore, Vue-router, Vue CLI
My expectation: Oauth would succeed, set userEmail in localStorage, create a Firestore document in the users collection, then the page would redirect.
Reality: Oath succeeds, userEmail is set in localStorage, page redirects
I have tried using async/await to no avail and returning nothing from signInSuccessWithAuthResult and using signInSuccessUrl for the redirect didn't work either. window.location.href = "/" and location.href.replace("/") also didn't change anything. If I remove the redirect, the document is created which leads me to believe the redirect interrupts the document creation. I am very new to Firebase but I don't see why this isn't working. Any help would be greatly appreciated and if you need more details please comment.
let ui = firebaseui.auth.AuthUI.getInstance();
if (!ui) {
ui = new firebaseui.auth.AuthUI(firebase.auth());
}
let uiConfig = {
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID
],
signInFlow: "popup",
callbacks: {
signInSuccessWithAuthResult: function(authResult) {
let email = authResult.user.isAnonymous
? "guest"
: authResult.user.email;
localStorage.setItem("userEmail", email);
if (
authResult.additionalUserInfo.isNewUser &&
!authResult.user.isAnonymous
) {
db.collection("users")
.add({
email: authResult.user.email,
expire: new firebase.firestore.Timestamp.now(),
positions: [0, 0, 0],
premium: false
})
.then(() => {
window.location.pathname = "/";
});
}
window.location.pathname = "/";
return false;
}
}
};
ui.start("#firebaseui-auth-container", uiConfig);

I think there are a couple of problems with the flow here, one is general with how async works and the other is specific to FirebaseUI. I'll cover both.
First let's look at the sequence in which your callback code will run:
signInSuccessWithAuthResult: function(authResult) {
// 1. BEGINS RUNNING HERE
let email = authResult.user.isAnonymous
? "guest"
: authResult.user.email;
localStorage.setItem("userEmail", email);
if (
authResult.additionalUserInfo.isNewUser &&
!authResult.user.isAnonymous
) {
// 2. THIS FUNCTION KICKS OFF BUT WILL RETURN IMMEDIATELY
db.collection("users")
.add({
email: authResult.user.email,
expire: new firebase.firestore.Timestamp.now(),
positions: [0, 0, 0],
premium: false
})
.then(() => {
// 4. THIS WOULD RUN LAST, BUT PROBABLY IS ABORTED/SKIPPED DUE TO 3
window.location.pathname = "/";
});
}
// 3. THIS REDIRECT WILL BE TRIGGERED WHILE THE .add() IS EXECUTING async
window.location.pathname = "/";
return false;
}
}
};
So to fix that, you'd need to remove the redirect at 3) and only redirect after the collection has been saved.
BUT! FirebaseUI will automatically redirects to the "success" page when the callback returns. So even if you did the above change, the FirebaseUI redirect would randomly abort the db.collection.add() and reload the page to '/success' the default.
To stop this you have to block the return of the callback, by using async/await, and remove your manual redirect and configure the success url.
const uiConfig = {
signInSuccessUrl: '/success',
callbacks: {
signInSuccessWithAuthResult: async function(authResult) {
// ...
await db.collection("users").add({ ... });
}
}
};
Adding async to the callback allows you to block and await for the db.collection.add to return. Then you shouldn't redirect manually as the signInSuccessUrl does it for you so you'd get a second race-condition.
I've simplified it here, so ensure you don't add any other non-awaited async tasks else they'd race too. (e.g. LocalStorage.setItem() is blocking, but if you used AsyncStorage.setItem() you'd have the same issue and should await that too).

Related

Vue + MSAL2.x + Azure B2C Profile Editing

First, I am not finding Vue specific examples using MSAL 2.x and we'd like to use the PKCE flow. I am having issues with the way the router guards are run before the AuthService handleResponse so I must be doing something wrong.
In my main.js I am doing this...
// Use the Auth services to secure the site
import AuthService from '#/services/AuthServices';
Vue.prototype.$auth = new AuthService()
And then in my AuthConfig.js I use this request to login:
loginRequest : {
scopes: [
"openid",
"profile",
process.env.VUE_APP_B2C_APISCOPE_READ,
process.env.VUE_APP_B2C_APISCOPE_WRITE
]
},
The docs say it should redirect to the requesting page but that is not happening. If user goes to the protected home page they are redirected to login. They login, everything is stored properly so they are actually logged in, but then they are sent back to the root redirect URL for the site, not the Home page.
When a user wants to login we just send them to the protected home page and there is a login method called in the router guard which looks like this:
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const IsAuthenticated = await Vue.prototype.$auth.isAuthenticated()
console.log(`Page changing from ${from.name} to ${to.name}, requiresAuth = ${requiresAuth}, IsAuthenticated = ${IsAuthenticated}`)
if (requiresAuth && !IsAuthenticated)
{
next(false)
console.log('STARTING LOGIN')
Vue.prototype.$auth.login()
// Tried this
// Vue.prototype.$auth.login(to.path)
} else {
next()
}
})
In AuthServices.js I have this...
// The user wants to log in
async login(nextPg) {
// Tell B2C what app they want access to and their invitation ID if they are new
if (store.getters.userEmail != null) {
aCfg.loginRequest.loginHint = store.getters.userEmail
}
aCfg.loginRequest.state = "APP=" + store.getters.appCode
if (store.getters.appointmentLink != null && store.getters.appointmentLink != '') {
aCfg.loginRequest.state += ",ID=" + store.getters.appointmentLink
}
// Tried this
// if (nextPg && nextPg != '') {
// aCfg.loginRequest.redirectUrl = process.env.VUE_APP_B2C_REDIRECT_URL + nextPg
// }
return await this.msalInst.loginRedirect(aCfg.loginRequest)
}
I tried puting a nextPg parameter in the login method and adding a redirectUrl property to the login request but that gives me an error saying it is not one of the configured redirect URLs.
Also, I'm trying to make the user experience better when using the above technologies. When you look at the MSAL2.x SPA samples I see that when returning from a Profile Edit, a user is logged out and they are required to log in again. That sounds like a poor user experience to me. Sample here: https://github.com/Azure-Samples/ms-identity-b2c-javascript-spa/blob/main/App/authRedirect.js
Do I need to just create my own profile editing page and save data using MSGraph to prevent that? Sorry for the noob questions. Ideas?
Update - My workaround which seems cheesy is to add these two methods to my AuthService.js:
storeCurrentRoute(nextPath) {
if (!nextPath) {
localStorage[STOR_NEXT_PAGE] = router.history.current.path
} else {
localStorage[STOR_NEXT_PAGE] = nextPath
}
console.log('Storing Route:', localStorage[STOR_NEXT_PAGE])
}
reEstablishRoute() {
let pth = localStorage[STOR_NEXT_PAGE]
if (!!pth && router.history.current.path != pth) {
localStorage[STOR_NEXT_PAGE] = ''
console.log(`Current path is ${router.history.current.path} and reEstablishing route to ${pth}`)
router.push({ path: pth })
}
}
I call storeCurrentRoute() first thing in the login method and then in the handleResponse() I call reEstablishRoute() when its not returning from a profileEdit or password change. Seems like I should be able to make things work without this.
Update Number Two - When returning from B2C's ProfileEdit User Flow the MSAL component is not logging me out properly. Here is my code from my handlePolicyChange() method in my AuthService:
} else if (response.idTokenClaims[clmPolicy] === aCfg.b2cPolicies.names.editProfile) {
Vue.nextTick(() => {
console.log('BACK FROM Profile Change')
Vue.prototype.$swal(
"Success!",
"Your profile has been updated.<br />Please log in again.",
"success"
).then(async () => {
this.logout()
})
})
}
:
// The user wants to log out (all accounts)
async logout() {
// Removes all sessions, need to call AAD endpoint to do full logout
store.commit('updateUserClaims', null)
store.commit('updateUserEmail', null)
let accts = await this.msalInst.getAllAccounts()
for(let i=0; i<accts.length; i++) {
const logoutRequest = {
account: accts[i],
postLogoutRedirectUri: process.env.VUE_APP_B2C_REDIRECT_URL
};
await this.msalInst.logout(logoutRequest);
}
return
}
It is working fine until the call to logout() which runs without errors but I looked in my site storage (in Chrome's debug window > Application) and it looks like MSAL did not clear out its entries like it does on my normal logouts (which always succeed). Ideas?
As part of the MSAL auth request, send a state Parameter. Base64 encode where the user left off inside this parameter. MSAL exposes extraQueryParameters which you can put a dictionary object inside and send in the auth request, put your state Key value pair into extraQueryParameters.
The state param will be returned in the callback response, use it to send the user where you need to.

Auth0 route guard not working with Nuxt middleware

What is the correct pattern to implement Auth0 route guards in Nuxt?
I've adapted the Auth0 sample code to create the following middleware:
import {getInstance} from '~/plugins/auth';
export default function () {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (!authService.isAuthenticated) {
authService.loginWithRedirect({
appState: {targetUrl: 'http://localhost:3000'},
});
}
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch('loading', loading => {
if (loading === false) {
return fn();
}
});
}
Notice that before the authentication status of Auth0 can be accessed, we must wait for the the instance to finish loading. The Auth0 sample code does this by using $watch.
My middleware code "works" but has the issue of briefly displaying the protected pages before the async $watch triggers. Is there any way to wait and block the route from continuing to render until Auth0 has finished loading and its auth status can be accessed?
I've also tried using almost the exact same code Auth0 provides without my own modifications within the beforeRouteEnter hook of the Nuxt pages. This has the same issue which begs the question as to why the Auth0 example presumably works in VueJS using beforeRouteEnter but not in Nuxt?
Solved it!
A middleware can be asynchronous. To do this return a Promise or use async/await.
https://nuxtjs.org/docs/2.x/directory-structure/middleware/
I simply wrapped my middleware script in a promise. I resolved it if the user is able to pass, otherwise I redirected them to the Auth0 login.
import {getInstance} from '~/plugins/auth';
export default function () {
return new Promise(resolve => {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (!authService.isAuthenticated) {
return authService.loginWithRedirect({
appState: {targetUrl: 'http://localhost:3000'},
});
}
resolve();
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch('loading', loading => {
if (loading === false) {
return fn();
}
});
});
}
It was also important to return the loginWithRedirect to make sure that it didn't go on to resolve the promise outside of the if block.

VueJs auh0 social authentication, how to customize handle redicrect

Using VueJs SDK .
My redirect url is example.com/callback, inside routes I have callback component which suppose to send user data to backend and check if user exists and log user in otherwise create new user and get back jwt from our backend application .
Problem is following, inside created/mounted cycle I can not reach auth0Client and so user properties.
this.$auth.auth0Client
I have to do this hack
const interval = setInterval(async () => {
count++;
if (this.$auth.auth0Client) {
clearInterval(interval);
let loggedUser = await this.$auth.auth0Client.getUser();
if(loggedUser) {
const email = loggedUser.email;
const name = loggedUser.name;
try {
//backend logic
} catch(e) {
console.log(e)
}
}
} else {
if (count > 80) {
clearInterval(interval);
}
}
}, 100)
My problem/question is how to handle this better as it seems not very logical and nice.
I want to send authenticated user data to our backend and do the rest there,
Auth0 seems to lack the details for this type of usecases
Thanks
I solve my own problem by using Vue event system
this.auth0Client = await createAuth0Client({
domain: options.domain,
client_id: options.clientId,
audience: options.audience,
redirect_uri: redirectUri,
responseType: 'token'
});
this.$eventBus.$emit('auth0Client',this.auth0Client) // creating event here after auth0Client
handling it in callback component
this.$eventBus.$on('auth0Client', async (auth0Client) => {
//logic here
})
Do not forget to register your event bus in main js
Vue.prototype.$eventBus = new Vue();
Hope it will help

Cloudflare Workers - SPA with Vuejs

Hello I have deployed my Vue.js app to Cloudflare workers using the following commands:
wrangler generate --site
wrangler publish --env dev
This is my wrangler.toml:
account_id = "xxx"
name = "name"
type = "webpack"
workers_dev = true
[site]
bucket = "./dist"
entry-point = "workers-site"
[env.dev]
name = "name"
route = "xxx.com/*"
zone_id = "XXX"
account_id = "XXX"
The website is fine and live on "xxx.com" but when I refresh the page on any other route I get this error message:
could not find es-es/index.html in your content namespace
Or for example:
could not find category/65/index.html in your content namespace
On nginx I had to create a .htaccess, but I have no idea on how to make it work here.
This is my index.js in case it helps:
import { getAssetFromKV, mapRequestToAsset } from '#cloudflare/kv-asset-handler'
/**
* The DEBUG flag will do two things that help during development:
* 1. we will skip caching on the edge, which makes it easier to
* debug.
* 2. we will return an error message on exception in your Response rather
* than the default 404.html page.
*/
const DEBUG = false
addEventListener('fetch', event => {
try {
event.respondWith(handleEvent(event))
} catch (e) {
if (DEBUG) {
return event.respondWith(
new Response(e.message || e.toString(), {
status: 500,
}),
)
}
event.respondWith(new Response('Internal Error', { status: 500 }))
}
})
async function handleEvent(event) {
const url = new URL(event.request.url)
let options = {}
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)
try {
if (DEBUG) {
// customize caching
options.cacheControl = {
bypassCache: true,
}
}
return await getAssetFromKV(event, options)
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
})
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 })
}
}
/**
* Here's one example of how to modify a request to
* remove a specific prefix, in this case `/docs` from
* the url. This can be useful if you are deploying to a
* route on a zone, or if you only want your static content
* to exist at a specific path.
*/
function handlePrefix(prefix) {
return request => {
// compute the default (e.g. / -> index.html)
let defaultAssetKey = mapRequestToAsset(request)
let url = new URL(defaultAssetKey.url)
// strip the prefix from the path for lookup
url.pathname = url.pathname.replace(prefix, '/')
// inherit all other props from the default request
return new Request(url.toString(), defaultAssetKey)
}
}
As you know, Vue.js (like many other SPA frameworks) expects that for any path that doesn't map to a specific file, the server falls back to serving the root /index.html file. Vue will then do routing in browser-side JavaScript. You mentioned that you know how to accomplish this fallback with .htaccess, but how can we do it with Workers?
Good news: In Workers, we can write code to do whatever we want!
In fact, the worker code already has a specific block of code to handle "404 not found" errors. One way to solve the problem would be to change this block of code so that instead of returning the 404 error, it returns /index.html.
The code we want to change is this part:
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
})
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 })
}
We want to change it to:
} catch (e) {
// Fall back to serving `/index.html` on errors.
return getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/index.html`, req),
})
}
That should do the trick.
However, the above solution has a slight problem: For any HTML page (other than the root), it will do two lookups, first for the specific path, and only after that will it look for /index.html as a fallback. These lookups are pretty fast, but maybe we can make things faster by being a little bit smarter and detecting HTML pages upfront based on the URL.
To do this, we want to customize the mapRequestToAsset function. You can see a hint about this in a comment in the code:
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)
Let's go ahead and use it. Replace the above comment with this:
options.mapRequestToAsset = req => {
// First let's apply the default handler, which we imported from
// '#cloudflare/kv-asset-handler' at the top of the file. We do
// this because the default handler already has logic to detect
// paths that should map to HTML files, for which it appends
// `/index.html` to the path.
req = mapRequestToAsset(req)
// Now we can detect if the default handler decided to map to
// index.html in some specific directory.
if (req.url.endsWith('/index.html')) {
// Indeed. Let's change it to instead map to the root `/index.html`.
// This avoids the need to do a redundant lookup that we know will
// fail.
return new Request(`${new URL(req.url).origin}/index.html`, req)
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return req
}
}
Now the code detects specifically HTML requests and replaces them with the root /index.html, so there's no need to waste time looking up a file that doesn't exist only to catch the resulting error. For other kinds of files (images, JS, CSS, etc.) the code will not modify the filename.
There appears to be a built-way to do this now:
import { getAssetFromKV, serveSinglePageApp } from '#cloudflare/kv-asset-handler'
...
let asset = await getAssetFromKV(event, { mapRequestToAsset: serveSinglePageApp })
https://github.com/cloudflare/kv-asset-handler#servesinglepageapp

VueJS data doesnt change on URL change

My problem is that when I go from one user page to another user page the info in component still remains from first user. So if I go from /user/username1 to /user/username2 info remains from username1. How can I fix this ? This is my code:
UserProfile.vue
mounted() {
this.$store.dispatch('getUserProfile').then(data => {
if(data.success = true) {
this.username = data.user.username;
this.positive = data.user.positiverep;
this.negative = data.user.negativerep;
this.createdAt = data.user.createdAt;
this.lastLogin = data.user.lastLogin;
data.invites.forEach(element => {
this.invites.push(element);
});
}
});
},
And this is from actions.js file to get user:
const getUserProfile = async ({
commit
}) => {
try {
const response = await API.get('/user/' + router.currentRoute.params.username);
if (response.status === 200 && response.data.user) {
const data = {
success: true,
user: response.data.user,
invites: response.data.invites
}
return data;
} else {
return console.log('Something went wrong.');
}
} catch (error) {
console.log(error);
}
};
Should I add watch maybe instead of mounted to keep track of username change in url ?
You can use watch with the immediate property, you can then remove the code in mounted as the watch handler will be called instead.
watch: {
'$route.params.username': {
handler: function() {
this.$store.dispatch('getUserProfile').then(data => {
if(data.success = true) {
this.username = data.user.username;
this.positive = data.user.positiverep;
this.negative = data.user.negativerep;
this.createdAt = data.user.createdAt;
this.lastLogin = data.user.lastLogin;
data.invites.forEach(element => {
this.invites.push(element);
});
}
});
},
deep: true,
immediate: true,
},
}
Your page is loaded before the data is retrieved it seems, you need put a "loading" property in the data and have a v-if="!loading" for your component then it will only render once the display is updated. Personally I would avoid watch if I can it is not great for performance of for fine grained handling.
Yes you should add wach on statement that contain user info.(you may have a problem to watch on object, so you can save user info in json, but im not sure). When user changing - call action, after recived response call mutation that should change a state, then watch this state.
And you might use better syntax to receive data from store. That is really bad idea call dispatch directly from your mouted hook, use vuex documentation to make your code better.