VueJS (resource, router and #websanova/vue-auth) login with JWT tokens refresh token error - authentication

I'm building an app using VueJS and Electron, now I'm trying to create a login using the #websanova/vue-auth package and everything goes well (login, logout, route protection, etc..) the only thing I'm stuck on is that everytime I log in and refresh or restart electron, it kicks me back to the login page. The weird thing is, it refreshed the token successfully, if I look in the localstorage it is updated and when I try to make a manual request using a REST client the token works. I just can't get into the app using that token.
I'm using the latest versions of VueJS, vue-router, vue-resource and #websanova/vue-auth as of today (19-sep-2016).
The API side is a Laravel 5.3 installation and I'm using the tymondesigns/jwt-auth package to handle the JWT tokens.
this is how I use my routes:
'/': {
auth: true,
name: 'dashboard',
component: HomeView
},
'/login': {
auth: false,
name: 'login',
component: LoginView
}
The views are being compiled using browserify and vueify.
My login function is like this:
this.$auth.login({
body: this.body,
success: function () {
this.loading = false;
},
error: function () {
this.error.status = true;
this.loading = false;
this.body.password = '';
},
rememberMe: true
});
If you need more information in order to be able to help me, just let me know.
Edit: If you want to take a look, here are the links to the repo's:
API: https://github.com/luukhoeben/rmi-app-api
Electron app: https://github.com/luukhoeben/rmi-app-electron
Thanks,
Luuk

If you set
Vue.use(Auth, {
router: Router,
http: Vue.$http,
tokenExpired: () => {
return false
}
})
Setting tokenExpired to return false, like this, you will skip the token refresh all together, which is not that bad. Downside is that your clients will have an expired token at some point and will be forced to re-login.
Another method is to try and check when the token will expire and refresh based on that.

Related

Playwright: unable to login via API setting cookie (able to do it with Cypress)

I'm trying to implemented login via API following Playwright's guidelines but somehow nothing seems to be working.
As a comparison I've built the same in Cypress and it works out of the box:
Context:
Playwright Version: 1.30
Operating System: Mac
Node.js version: v16.19.0
Browser: Chromium
I am unable to make a simple API login that works perfectly using Cypress instead. Let me share the 2 code snippets for comparison:
Simple test case:
API request to the login end-point - Auth token is retrieved
set the auth token as a cookie
navigate to a page that is accessible only if authenticated
Code Snippet
Cypress (working fine)
const body = {
username: 'username...',
password: 'password',
rememberMe: true,
};
describe('Login via API to management console', () => {
it('Login via API to management console', () => {
cy.request({
method: 'POST',
url: loginEndPoint,
headers: {
'Content-Type': 'application/json',
},
body,
}).then((response) => {
cy.setCookie('Authorization', `Token ${response.body.data.token}`);
});
cy.visit(`/management`);
});
});
Playwright (not working)
test('Login via API', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
const loginResponse = await context.request.post(`https://${process.env.MANAGEMENT_URL}/web/api/v2.1/users/login`, {
data: {
username: process.env.MANAGEMENT_USER,
password: process.env.MANAGEMENT_PASSWORD,
rememberMe: true,
}
});
const {
data: { token },
} = await loginResponse.body().then((b) => {
return JSON.parse(b.toString());
});
expect(token).toMatch(/^[a-z0-9]{80}$/)
await context.addCookies([{ name: 'Authorization', value: `Token ${token}`, path: '/', domain: `https://${process.env.MANAGEMENT_URL}` }]);
await page.goto(`https://${process.env.MANAGEMENT_URL}/management/`);
await expect(page).toHaveURL(/management/);
});
Describe the bug
Both scripts are successful at retrieving the authentication token but somehow either I'm doing something wrong with setting the cookie in Playwright or there is an issue. I'd assume the 2 scripts should be comparable.
Furthermore: I've tried to execute login via UI using global-setup, saving the storage-state, loading it before running the test and it fails also in this case... so there is something that is not setting properly the state in this case or the cookie in the previous one.
Not entirely sure why the cookie approach wasn’t working, perhaps the https:// part should be removed from the domain?
That being said, in Playwright you shouldn’t even need to do that especially within a single test, looking at the Playwright docs on signing in via the API and related page about the request context particularly under cookie management. The associated request and browser contexts share cookies, so once you complete the login request, the browser should already have the cookie state too and be logged in, so you should be able to just remove getting the token and adding the cookie. Or you can login with the API in the global setup even, as that doc showed. Just make sure in that case to save the storage state, and specify the same file in your config.
I see you tried the global setup approach (through the UI, but you can use the API since you have it), not sure what happened there. I would say to ensure that you specified the storageState in the config; I would be curious how you loaded it as mentioned, and if you’re still having problems maybe share the code you’re using for that piece?
Hope that helps or we can troubleshoot further!

Nuxt auth with keycloak: ssr this.$auth.loggedIn always false on page load

I have a setup with nuxt and keycloak as auth strategy which in general is working. I can login via keycloak and then will have this.$auth.loggedIn === true on the page. When navigating via vue-router, this.$auth.loggedIn will also be true when switching to a new page.
But when I then reload the page (CMD+r/F5), server side rendering will have false for this.$auth.loggedIn, while on client side it will be true. This forced me to do a lot of <client-only> blocks in the templates to prevent ssr mismatches.
I wonder if it is possible that on first page load server side rendering can return a page with authorized content? I would think this should be possible since cookies with auth info are set and sent to the server.
Or is that never possible and efficient server side rendering can only be used for non-authorized content?
Versions:
nuxt: 2.15.8
#nuxtjs/auth-next: 5.0.0-1643791578.532b3d6
nuxt.config.js:
auth: {
strategies: {
keycloak: {
scheme: 'oauth2',
endpoints: {
authorization: `${ process.env.KEYCLOAK }/protocol/openid-connect/auth`,
userInfo: `${ process.env.KEYCLOAK }/protocol/openid-connect/userinfo`,
token: `${ process.env.KEYCLOAK }/protocol/openid-connect/token`,
logout: `${ process.env.KEYCLOAK }/protocol/openid-connect/logout`,
},
token: {
property: 'access_token',
type: 'Bearer',
maxAge: 1800,
},
refreshToken: {
property: 'refresh_token',
maxAge: 60 * 60 * 24 * 30,
},
responseType: 'code',
grantType: 'authorization_code',
clientId: process.env.CLIENT_ID,
scope: ['openid', 'profile', 'email', 'roles'],
codeChallengeMethod: 'S256',
redirect: {
logout: '/',
callback: '/',
home: '/',
},
},
},
},
Having a Vue component with this:
created() {
console.log(this.$auth.loggedIn);
},
Will return false for SSR and true on client side on page load/refresh when logged in.
After manually implementing a server side authenticator, I found out that the problem was my local docker setup.
Didn't think this was the problem before, so I forgot to mention it.
I have a local docker container with keycloak and a local docker container with nuxt.
Long story short, it seems that the nuxt server wasn't able to communicate with keycloak, hence wasn't able to fetch the user. After changing some addresses so that keycloak was available on the same address from the browser and from within my nuxt server docker container, the nuxt server did get $auth.loggedIn=true automatically on page load if the is was logged in.
Not sure if I didn't see it, but I wished nuxt auth would give me an error if the nuxt server failed to communicate with the authorization server. Would have saved me a lot of debugging.

How to prevent visitor from seeing app before redirection?

I am building a SPA using Vue-CLI with a client-side OAuth 2.0 javascript library called JSO. It uses HTML 5.0 localStorage to cache Access Tokens.
In my full app, I have everything functioning properly with the exception of the following issue:
When the user arrives at my app for the first time, he catches a quick glimpse of the app and then automatically is redirected to a third party authentication login screen. I don't want that "quick glimpse" to happen -- I need to have the user immediately redirected to the third party login page BEFORE he sees any part of my app.
So, I thought I'd set up Global Before Guards using Vue-Router like so:
From: Main.js
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: true
}
},
...more routes...and they all require auth...
]
router.beforeEach((to, from, next) => {
const token = window.localStorage.getItem('my-token-example')
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
if (token == null) {
client.getToken()
next(false)
}
} else {
next()
}
})
new Vue({
created: function() {
//check for response parameters if user has an auth token (uses JSO plugin)
//if token received, then it is stashed into localStorage
client.callback()
},
render: h => h(App),
router: router
}).$mount('#app')
Example App.vue:
<template>
<div id="app">
<Header />
<routerView />
</div>
</template>
Info on client.callback():
I catch the headers response when user is returning to app
Info on client.getToken():
I get the token payload
Notes: The JSO auth docs state:
"You may also ensure that a token is available early in your application, to force all user interaction and redirection to happen before your application is fully loaded. To do that make a call to getToken, and wait for the callback before you continue.
REMEMBER to ALWAYS call the callback() function to process the response from the OAuth server, before you use getToken(), if not you will end up in an redirect_loop"
Does anyone have any suggestions on how I can prevent the user from seeing my app before he is redirected to the auth login site? Update: I think I see the problem...is the global guards only affecting the section of the app within the <RouterView /> component? Hence, we see the header and banner of my app before redirection?
I solved this. I had to simply do a v-if on my app.vue file like: <div v-if="token !== null>
That hides the app template until token is received.

Refresh token Vuex before enter in Routing

I have an frontend Web app interfaces with API built in Laravel with Passport.
My problem is when I refresh my page (in SPA written with Vuejs/Vuex) I should refresh my token, for refresh session with my Api.
I tried in main.js but he problem is that the request is async and the response arrived after routing.
main.js
if (localStorage.getItem('refresh_token')) {
store.dispatch('refresh_token').then(function(response){
console.log(response);
});
}
new Vue({
router,
store,
env,
render: h => h(App)
}).$mount('#app')
The function refresh token, make a call to my Api, and with response set the new token and the new refresh token.
But my problem is that I make this call in this way I can make the first async call in my "dashboard" with old token and then with the new.
So I've tried in different ways but I don't know if there is a best practice.
So my question is:
Where I should refresh token in Vuejs App with vuex store?
I suggest putting this in the mounted property of you toplevel Vue component. If you have other components that depend on your token being refreshed, you can couple this with a state variable in your store that signals the refresh is completed. For example, with a top level component App.vue:
...
mounted () {
store.dispatch('refresh_token')
}
...
Adding the state variable to your vueex store:
const store = new Vuex.Store({
state: {
sessionRefreshed: false
},
..
mutations: {
[REFRESH_TOKEN] (state) {
// existing mutations, and..
state.sessionRefreshed = true
},
},
..
actions: {
refreshToken ({ commit }) {
myAsyncFetchToken().then(() => commit(REFRESH_TOKEN))
},
}
This ensures your entire application is aware of the state of your refresh without forcing it to be synchronous. Then if you have components which require the token to be refreshed, you can show loading widgets, placeholders, etc., and use a watcher to do things when the state changes.
How about using router#beforeEach guard? I use it to figure out if authentication token is stored in a cookie before accessing any "restricted" component. If token is not set I redirect to /login.
I realize that my scenario is exactly what you are are asking for but I hope you can use it to augment your implementation.

Checking auth token valid before route enter in Vue router

I have a simple use case, where my application is using vue-router and vuex. Then store contains a user object which is null in the beginning. After the user is validated from the server it sends back an user object which contains a JWT auth token which is assigned to the user object in the store. Now lets assume that the user came back after 3 hours and tried to visit a route or perform any other action, considering that the auth token has expired by then, what would be the best way to check that(need to call axios post to check it) and redirect user to the login page. My app will have loads of components so I know I can write logic to check the token valid in the mounted hook of each component but that would mean repeating it all of the components. Also I don't want to use the beforeEach navigation guard because I cannot show any visual feedback to the user like checking... or loading....
I do something similar in one of my projects, it's actually deceptively difficult to handle these types of situations, but you can add a beforeEnter guard to your protected routes, then redirect if the authentication failed.
const guard = function(to, from, next) {
// check for valid auth token
axios.get('/api/checkAuthToken').then(response => {
// Token is valid, so continue
next();
}).catch(error => {
// There was an error so redirect
window.location.href = "/login";
})
};
Then on your route you can do:
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
guard(to, from, next);
}
},
You may notice I've used location.href rather than router.push. I do that because my login form is csrf protected, so I need a new csrf_token.
Your other issue is going to be if the user tries to interact with your page without changing the route (i.e. they click a button and get a 401 response). For this I find it easiest to check authentication on each axios request and redirect to login when I receive a 401 response.
In terms of adding a loading spinner during the guard check you can simply add a loading flag to your vuex store then import your store into your router. Honestly though I wouldn't bother, on a decent production server the check will be done so quickly that the user is unlikely to ever see it.
Try Vue.JS Mixins
You can define a Global Mixin and use it via Vue.use(myMixin) - then all Components will inherit this mixin. If you define a mounted or probably better activated hook on the mixin, it will be called on every component.
There you can use everything a component can do - this will point to your component. And if the component also defines a hook itself, the mixin hook of the same type will run before the components own hook.
Or try a single top-level login component
We used a little different solution - we have a single component which handles everything login-related, which exists outside of the router-view in the parent index.html. This component is always active and can hide the div router-view and overlay a loading message or a login-screen. For an intranet-application this component will also use polling to keep the session alive as long as the browser stays open.
You can load of your router-navigation to this component. - So a child-component which wants to trigger a router-navigation just sets a global reactive property navigateTo which is watched by the top level authentication component. This will trigger an authentication check, possibly a login-workflow and after that the top-level component will call $router.push() With this approach you have complete control over any navigation.
You can use interceptors to silently get the auth token when some request happens.
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const rToken = window.localStorage.getItem('rToken');
return axios.post('url/to/get/refresh/token', { rToken })
.then(({data}) => {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('rToken', data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
return axios(originalRequest);
});
}
return Promise.reject(error);
});
Because you use vuex, you can add some state like isLoading or isChecking.
And in your router.beforeEach, you can check and set isLoading or isChecking follow your current checking state. Then you can show loading message follow this state.
In our route.js we check in beforeEnter hooks the user has token or
not.
route.js
{
path: '/dashboard',
name: dashboard,
meta: {
layout: 'home-layout'
},
components: {
default: Dashboard,
header: UserHeader
},
beforeEnter: ifAuthenticated,
}
route.js
const ifAuthenticated = (to, from, next) => {
if (localStorage.getItem(token)) {
next();
return;
}
router.push({
name: 'login',
params: {
returnTo: to.path,
query: to.query,
},
});
};