I use keycloak-js to authenticate my vue.js like this (taken from there: https://www.keycloak.org/securing-apps/vue), it´s my main.js:
import { store } from "./store/store";
let keycloak = Keycloak(initOptions);
keycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {
if (!auth) {
window.location.reload();
} else {
console.log("Authenticated");
new Vue({
vuetify, router, store,
render: h => h(App, { props: { keycloak: keycloak } })
}).$mount('#app')
}
const decoded = VueJwtDecode.decode(keycloak.token)
const roles = decoded.realm_access.roles
store.commit("storeRoles", roles)
//Token Refresh
setInterval(() => {
if(store.state.userData.logged_out) { ### done in vuex
keycloak.logout()
} else {
keycloak.updateToken(70).then((refreshed) => {
if (refreshed) {
console.log('Token refreshed' + refreshed);
} else {
console.log('Token not refreshed, valid for '
+ Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');
}
}).catch(() => {
console.log('Failed to refresh token');
});
}
}, 2000)
}).catch(() => {
console.log("Authenticated Failed");
});
I currently commit a mutation in my store to handle this via a button.
if(store.state.userData.logged_out) { ### done in vuex
keycloak.logout()
}
## Handles by a button in my view
this.$store.commit("logout", true)
This seems to be a bit hacky and I have to wait 2 seconds before my user actually gets logged out. Is there a way to directly access this keycloak instance from a component?
I solved it by adding the keycloak instance to the Vue.prototype (thanks #augstin gorni for the hint)
https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
let keycloak = Keycloak(initOptions);
Vue.prototype.$keycloak = keycloak
Now it´s globally accessible
logout() {
this.$keycloak.logout()
}
}
In Vue 3 you can use like below;
app.config.globalProperties.$keycloak = keycloak;
Related
I am implementing firebase authentication to Nuxt js application and I am so close. The problem is I want to commit a vuext mutation inside firebase's default function onAuthStateChanged(). But when ever I load the page it shows the following error:
"Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '$store')"
Can you guys please help me out with this problem.
Thanks.
import firebase from '#/plugins/firebase'
import {
getAuth,
signInWithEmailAndPassword,
onAuthStateChanged
} from "firebase/auth"
export const state = () => ({
user: null,
authIsReady: false
})
export const mutations = {
updateUser(state, payload) {
state.user = payload
console.log('user is updated', state.user)
},
setAuthIsReady(state, payload) {
state.authIsReady = payload
console.log(state.authIsReady)
}
}
export const actions = {
async signIn(context, {
email,
password
}) {
console.log('sign in action')
const res = await signInWithEmailAndPassword(getAuth(), email, password)
if (res) {
context.commit('updateUser', res.user)
} else {
throw new Error('could not complete sign in')
}
}
}
// this function is causing the problem
const unsub = onAuthStateChanged(getAuth(), (user) => {
this.$store.commit('updateUser', user)
unsub()
})
The firebase.js file that I'm importing "auth" from below, is just all the regular setting up Firebase in Nuxt stuff... and the important lines are:
const auth = getAuth()
export { auth }
Try the code below ... I have mine in a file named "fireauth.js" in the plugins folder (don't forget to import the "fireauth.js" file in your nuxt.config.js)
import {
auth
} from "~/plugins/firebase.js";
export default (context) => {
const {
store
} = context
return new Promise((resolve, reject) => {
auth.onAuthStateChanged((user) => {
if (user) {
return resolve(store.dispatch('onAuthStateChangedAction', user))
}
return resolve()
})
})
}
In your store/index.js file add the following async function in your actions setting:
async onAuthStateChangedAction(vuexContext, authUser) {
if (!authUser) { //in my case I'm just forcing user back to sign in page, only authorized users allowed//redirect from here this.$router.push({
path: '/signin',
})
}else {
//call your commits or do whatever you want to do
vuexContext.commit("setUser", authUser.email);
}
},
The first part of the code ensures that when the auth state changes in Firestore, this change is communicated to the action that you just created in the store. The second part of the code, the async function in the store accomplishes whatever you want it to do within the store.
I have a Vue.js application and on the /callback route I am trying to have it do a few things. So far I am not having any luck with it because I am seeing things run async. I understand that it is normally how Vue/Javascript works however I am trying to force it to not be async.
The main issue I am having is sometimes the this.$store... are running before the items are set. This is an issue because of how things run on other Vuex actions. Mainly the getCompany one requires the loadToken one to complete as it is pulling the values from the local storage which is being set above.
I don't want to change this and how it works because of how the Vue router is set up to pull the token from local storage on each page reload. This token is used to connect to the backend so it needs to be pulled from local storage each reload as I don't want a user to log in just because they reload the page.
Code:
created() {
setTimeout(() => {
localStorage.setItem('token', this.$auth.token)
localStorage.setItem('user_data', JSON.stringify(this.$auth.user))
// Load company data
this.$store.dispatch('loadToken')
this.$store.dispatch('getCompany')
if(this.$auth == null || this.$auth.id_token['https://hello.io/account_signup_type/is_new']) {
this.$router.push('/setup')
} else {
// Load user data from Auth0
// Go to chat page
this.$router.push('/chat')
}
}, 500)
}
edit main.js code
import { Auth0Plugin } from '#/auth/auth0-plugin';
// Install the authentication plugin
Vue.use(Auth0Plugin, {
domain,
clientId,
audience,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
auth0-plugin
/**
* External Modules
*/
import Vue from 'vue';
import createAuth0Client from '#auth0/auth0-spa-js';
/**
* Vue.js Instance Definition
*/
let instance;
export const getInstance = () => instance;
/**
* Vue.js Instance Initialization
*/
export const useAuth0 = ({
onRedirectCallback = () =>
window.history.replaceState({}, document.title, window.location.pathname),
redirectUri = `${window.location.origin}/callback`,
...pluginOptions
}) => {
if (instance) return instance;
instance = new Vue({
data() {
return {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: {},
error: null,
token: null,
id_token: null
};
},
methods: {
async handleRedirectCallback() {
this.isLoading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (error) {
this.error = error;
} finally {
this.isLoading = false;
}
},
loginWithRedirect(options) {
return this.auth0Client.loginWithRedirect(options);
},
logout(options) {
return this.auth0Client.logout(options);
},
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
}
},
async created() {
this.auth0Client = await createAuth0Client({
...pluginOptions,
// responseType: 'id_token',
domain: pluginOptions.domain,
client_id: pluginOptions.clientId,
audience: pluginOptions.audience,
redirect_uri: redirectUri,
});
try {
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
const { appState } = await this.auth0Client.handleRedirectCallback();
onRedirectCallback(appState);
}
} catch (error) {
this.error = error;
} finally {
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.$auth.getTokenSilently().then(token => this.token = token)
this.$auth.getIdTokenClaims().then(id_token => this.id_token = id_token)
this.isLoading = false;
}
},
});
return instance;
};
/**
* Vue.js Plugin Definition
*/
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
edit - updated router.beforeEach
router.beforeEach(async (to, from, next) => {
const auth = getInstance()
if(to.path == '/callback' && auth != null) {
console.log('Callback')
console.log(`Token: ${auth.token}`)
console.log(`User: ${JSON.stringify(auth.user)}`)
localStorage.setItem('token', auth.token)
localStorage.setItem('user_data', JSON.stringify(auth.user))
await store.dispatch('loadToken')
await store.dispatch('getCompany')
return next()
}
if(to.path != '/login' && to.path != '/setup') {
await store.dispatch('loadToken')
await store.dispatch('getCompany')
.then(() => {
return next()
})
} else {
return next()
}
})
edit - adding guide that I followed from Auth0 to get the code I have now - mostly
https://auth0.com/blog/complete-guide-to-vue-user-authentication/
The problem is that there is race condition because dispatch calls return promises that weren't chained before accessing the result of their work.
A good practice is to chain every promise, unless proven other wise.
The code that created contains actually belongs to the router in general because authentication logic is application-wide.
It's unnecessary to access global dependencies on this component instance. This is done for historical reasons because Vue originally was used in non-modular environment. In order to use outside components, global dependencies such as store need to be explicitly imported. In case this cannot be done, this needs to be fixed.
In this case auth instance is available through getInstance. In case the authentication shouldn't be done on each navigation, this needs to be done on condition, e.g.:
import { getInstance } from '.../auth';
import store from '.../store';
...
router.beforeEach(async (to, from, next) => {
const auth = getInstance();
if (...) {
...
await store.dispatch('loadToken')
await store.dispatch('getCompany')
...
next('/setup')
...
} else {
next()
}
})
getInstance doesn't serve a good purpose because it just exposes a variable. Instead, instance could be exported and imported directly, the behaviour would be the same due to how ES modules work.
Also global store already holds application logic and commonly used to handle authentication, including local storage operations.
I'm trying to implement an SSO react-admin login
Most of the react-admin examples a is simple username/password login and get token then store at storage. But for me, token will return from Auth Provider server and redirect to http://example.com/#/login?token=TOKEN, and make react-admin to parse the URL, and set token in localStorage. Then update React admin store to mark a user as "is logged in,"
but for me I failed to simulate logged in as a hook after validating token from app main js can you help me how to do that
A workaround like below is works for me, but this is quite dirty solution:
const url = window.location;
(() => {
if (localStorage.getItem("access_token") === "null" || localStorage.getItem("access_token") === "undefined") {
localStorage.removeItem("access_token");
}
})();
const access_token = () => {
if (localStorage.getItem("access_token")) {
return localStorage.getItem("access_token")
} else if (new URLSearchParams(url.search).get("access_token")) {
return new URLSearchParams(url.search).get("access_token")
}
}
localStorage.setItem("access_token", access_token());
export const authProvider = {
login: () => {
return Promise.resolve();
},
logout: () => {
localStorage.removeItem("access_token");
return Promise.resolve();
},
checkError: (status) => {
if (status.response.status === 401 || status.response.status === 403) {
localStorage.removeItem("access_token");
return Promise.reject();
}
return Promise.resolve();
},
checkAuth: () => {
return localStorage.getItem("access_token") ? Promise.resolve() : Promise.reject();
},
getPermissions: () => Promise.resolve(),
};
There is no login method call at all, just direct setting token to the localStorage and check with checkAuth method. Maybe, sombody will give better solution.
I inherited an app which is bootstrapped in a way that is rather perplexing to me (unlike like the docs or anything I have seen), my task is to create a global loader instead of implementing on each page separately.
I thought this would be simple where I could just manage the state in the store and have a shared "Header" component respond to changes in the "loading" property in the store. Boy was I wrong, it seems as though the way the app is setup with factories I don't have access to the store inside my interceptor.
The code is a bit advanced for me and I would be very appreciative if anyone can explain either how to restructure the code or alternatively how to access the store inside the interceptor.
My main.js looke like this:
import 'babel-polyfill' // IE support
import cssVars from 'css-vars-ponyfill'; // css variables support for IE
import 'innersvg-polyfill'; // svg support for IE
// until here, all polyfills
import "vue-material-design-icons/styles.css";
import VueI18n from 'vue-i18n'
import {localizationFactory} from "./localization";
import {apiFactory, apiPluginFactory} from './api/api';
import {storeFactory} from "./store/store";
import {configServiceFactory} from "./services/configService";
import {Services, Security, Utils} from 'em-common-vue';
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import {filtersFactory} from './filters/index';
const appsService = new Services.appsService(process.env);
const loginDetails = {
loginHost: appsService.getLoginStorage()
};
console.log("top of main.js before factory functions", this)
Security.ServiceFactory(loginDetails).then($security => {
console.log("inside secutiy service factor 'then'", this)
Vue.config.productionTip = false;
Vue.use(Utils.EventBusPlugin);
Vue.use($security);
var vInstance = new Vue();
const $api = apiFactory(vInstance, $security);
configServiceFactory($security, $api).then($config => {
Vue.use($config);
Vue.use( apiPluginFactory($api) );
// for now
const store = storeFactory($api, null);
Vue.use(VueI18n);
filtersFactory($config.$service);
localizationFactory($config.$service).then(messages => {
const i18n = new VueI18n({
locale: 'en', // set locale
messages, // set locale messages
});
new Vue({
router,
store,
i18n,
render: h => h(App),
computed: {
title: {
set(val) {
document.querySelector('title').innerText = val;
},
get(val) {
return document.querySelector('title').innerText;
}
}
},
mounted() {
cssVars({
silent: false, // default,
watch: true,
onWarning() {
console.log("onWarning",arguments); // 1
},
onError() {
console.log("onError", arguments); // 1
}
});
if (!document.querySelector('title')) {
let title = document.createElement('title');
document.head.append(title);
}
this.title = this.$config.get().title;
}
}).$mount('#app')
});
});
}).catch(err => {
console.error(err);
if (err.loginUrl) {
const nextUrl = appsService.getLogin(window.location.href);
window.location.href = nextUrl;
} else { // for now
alert(err);
}
});
The apiFactory which is where the interceptor resides looks like this:
import axios from 'axios';
import {UsersApi} from "./usersApi";
import {PartnersApi} from "./partnersApi";
import {TrialsApi} from "./trialsApi";
export function apiFactory(vue, $security) {
console.log("inside api factory")
console.log(this)
const $http = axios.create({
baseURL: process.env.VUE_APP_API_URL
});
$http.interceptors.request.use(function (config) {
console.log("inside interceptor ", this)
// this.$store.commit('toggleLoader')
const token = $security.loginFrame.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function (error) {
vue.$eventBus.$emit('toastMessageHandler', {message: error.message, type: 'error'});
return Promise.reject(error);
});
$http.interceptors.response.use(function (response) {
return response;
}, function (error) {
if (error.response && error.response.status === 401) {
vue.$eventBus.$emit('authErr', error);
} else if (error.response && error.response.status === 403) {
alert('You are not authorized to access this application. If you believe you are seeing this message in error, please contact support#emergingmed.com.');
}
else if (error.response && error.response.status !== 409){
vue.$eventBus.$emit('toastMessageHandler', {message: error.message, type: 'error'});
}
else if (error.response && error.response.status === 500){
vue.$eventBus.$emit('toastMessageHandler', {message: 'There was a problem with this operation - please contact support#emergingmed.com.', type: 'error'});
}
return Promise.reject({error});
})
const $api = {
users: new UsersApi($http),
partners: new PartnersApi($http),
trials: new TrialsApi($http)
};
return $api;
}
// register $api as vue plugin
export function apiPluginFactory($api) {
return {
install(vue) {
vue.prototype.$api = $api;
},
};
}
When I try to run this code this.$store.commit('toggleLoader') (commented out in the actual code) I get an error that store is undefined. In general this structure does not seem to me idiomatic to vue (or maybe even JS) but I am not sure how I would restructure it.
1)How might I access my store in the interceptor code?
2)How might the app be restructured?
I am trying to load a notification token (notificationToken) that I've stored within Firebase to a React Native component.
Once the notificationToken is loaded to my redux state, I want to check for my device permissions to see if the notificationToken has expired within the function getExistingPermission() that I run in the componentDidMount().
If the token has expired, then I'll replace the token within Firebase with the new token. If it's the same, then nothing happens (which is intended functionality).
When I'm running my function getExistingPermission() to check if the token is up-to-date the Firebase listener that pulls the notificationToken does not load in time, and so it's always doing a write to the Firebase database with a 'new' token.
I'm pretty sure using async/await would solve for this, but have not been able to get it to work. Any idea how I can ensure that the notificationToken loads from firebase to my redux state first before I run any functions within my componentDidMount() function? Code below - thank you!
src/screens/Dashboard.js
Should I use a .then() or async/await operator to ensure the notificationToken loads prior to running it through the getExistingPermission() function?
import {
getExistingPermission
} from '../components/Notifications/NotificationFunctions';
componentDidMount = async () => {
// Listener that loads the user, reminders, contacts, and notification data
this.unsubscribeCurrentUserListener = currentUserListener((snapshot) => {
try {
this.props.watchUserData();
} catch (e) {
this.setState({ error: e, });
}
});
if (
!getExistingPermission(
this.props.notificationToken, //this doesn't load in time
this.props.user.uid)
) {
this.setState({ showNotificationsModal: true });
}
};
src/components/Notifications/NotificationFunctions.js
The problem is probably not here
export const getExistingPermission = async (
notificationToken,
uid,
) => {
const { status: existingStatus } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);
if (existingStatus !== 'granted') {
console.log('status not granted');
return false;
} else {
let token = await Notifications.getExpoPushTokenAsync();
/* compare to the firebase token; if it's the same, do nothing,
if it's different, replace */
if (token === notificationToken) {
console.log('existing token loaded');
return true;
} else {
console.log('token: ' + token);
console.log('notificationToken: ' + notificationToken);
console.log('token is not loading, re-writing token to firebase');
writeNotificationToken(uid, token);
return false;
}
}
};
src/actions/actions.js
// Permissions stuff
watchPermissions = (uid) => (
(dispatch) => {
getPermissions(uid + '/notificationToken', (snapshot) => {
try {
dispatch(loadNotificationToken(Object.values([snapshot.val()])[0]));
}
catch (error) {
dispatch(loadNotificationToken(''));
// I could call a modal here so this can be raised at any point of the flow
}
});
}
);
// User Stuff
export const watchUserData = () => (
(dispatch) => {
currentUserListener((user) => {
if (user !== null) {
console.log('from action creator: ' + user.displayName);
dispatch(loadUser(user));
dispatch(watchReminderData(user.uid)); //listener to pull reminder data
dispatch(watchContactData(user.uid)); //listener to pull contact data
dispatch(watchPermissions(user.uid)); //listener to pull notificationToken
} else {
console.log('from action creator: ' + user);
dispatch(removeUser(user));
dispatch(logOutUser(false));
dispatch(NavigationActions.navigate({ routeName: 'Login' }));
}
});
}
);
export const loadNotificationToken = (notificationToken) => (
{
type: 'LOAD_NOTIFICATION_TOKEN',
notificationToken,
}
);
Tony gave me the answer. Needed to move the permissions check to componentDidUpdate(). For those having a similar issue, the component looks like this:
src/screens/Dashboard.js
componentDidUpdate = (prevProps) => {
if (!prevProps.notificationToken && this.props.notificationToken) {
if (!getExistingPermission(
this.props.notificationToken,
this.props.user.uid
)) {
this.setState({ showNotificationsModal: true });
}
}
};
Take a look at redux subscribers for this: https://redux.js.org/api-reference/store#subscribe . I implement a subscriber to manage a small state machine like STATE1_DO_THIS, STATE2_THEN_DO_THAT and store that state in redux and use it to render your component. Only the subscriber should change those states. That gives you a nice way to handle tricky flows where you want to wait on action1 finishing before doing action2. Does this help?