Nuxt: Auth + Axios setup - javascript

I'm struggling with the configuration of auth + Axios.
I'm currently working on our social login (FB and google). It half works.
First of all, I have my Axios instance configured as a plugin. We do have two instances, one that we use for general API requests and another one that we will use for logged user requests.
plugins/axios.js
export default function({ $axios, redirect, app }) {
$axios.defaults.baseURL = process.env.baseUrl
$axios.defaults.headers = {
Authorization: `Bearer ${process.env.APIKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
lang: app.i18n.locale
}
$axios.onError(error => {
const code = parseInt(error.response && error.response.status)
if (code === 400) {
redirect('/400')
}
if (code === 404) {
redirect('/404')
}
if (code === 500) {
redirect('/500')
}
})
}
plugins/auth-axios.js
export default function({ $axios }, inject) {
const authAxios = $axios.create()
// authAxios.setToken('123', 'Bearer')
inject('authAxios', authAxios)
}
The first client works perfectly in the whole app.
Now I'm configuring the auth plugin. When I log in through Facebook or google I need to take the response data of the logged user from the social network and send it to my API, expecting a user with the token as the response. This will be the user that I'll set in the auth plugin. Every time I log in with Facebook it appears to work well except when It arrives at the plugin code. $auth.loggedIn is always false.
I've made it work forcing $auth.fetchUser at the beginning of the code but it doesn't work well at all. It shows an Axios error when console.log($auth). This is my auth code:
plugins/auth.js
export default async function({ app: { $auth, $axios } }) {
$auth.fetchUser()
console.log($auth)
if (!$auth.loggedIn) {
return
}
const authStrategy = $auth.strategy.name
if (authStrategy === 'facebook' || authStrategy === 'google') {
if ($auth.user.google_id || $auth.user.fb_id) return
try {
const url = `/client/social`
var postData
if (authStrategy == 'facebook')
postData = {
name: $auth.user.name,
email: $auth.user.email,
fb_id: $auth.user.id,
avatar: $auth.user.picture.data.url,
birthday: $auth.user.birthday
}
else
postData = {
name: $auth.user.given_name,
surname: $auth.user.family_name,
email: $auth.user.email,
google_id: $auth.user.sub,
avatar: $auth.user.picture,
locale: $auth.user.locale
}
const { data } = await $axios.post(url, postData)
$auth.setToken('client', data.access_token)
$auth.setUser(data.client)
} catch (e) {
console.log(e)
}
}
}
The console.log($auth) error:
'$state': {
user: null,
loggedIn: false,
strategy: 'facebook'
},
error: TypeError: Cannot set property 'Authorization' of undefined
at Function.setHeader (server.js:1556:42)
at Oauth2Scheme._setToken (server.js:1014:31)
at Oauth2Scheme.mounted (server.js:1001:12)
at Auth.mounted (server.js:516:42)
at Auth.init (server.js:459:18)
at module.exports../.nuxt/auth/plugin.js._webpack_exports_.default (server.js:939:16)
at createApp (server.js:2432:87)
}
And my auth module config:
import dotenv from 'dotenv'
dotenv.config()
export const auth = {
plugins: [
// {
// src: '~/plugins/axios',
// ssr: true
// },
{
src: '~/plugins/auth-axios',
ssr: true
},
'~/plugins/auth.js'
],
redirect: {
login: '/',
callback: '/callback'
},
strategies: {
local: false,
facebook: {
client_id: '#############',
userinfo_endpoint:
'https://graph.facebook.com/v2.12/me?fields=about,name,picture{url},email,birthday',
scope: ['public_profile', 'email', 'user_birthday']
},
google: {
client_id:
'#####################'
}
}
}
Seems that auth when login is trying to set the Axios token (also when I log out it tries to remove it) but it fails. If I go to the Chrome dev tools and debug it to see which Axios instance is trying to use for that. Every time is the main Axios instance and it's supposed to be accessible there.
Screenshot from DevTools:
Screenshot from DevTools
Does someone know what I'm missing? Can I prevent auth facebook strategy to update any Axios instance? Or, can I specify which Axios instance to update (set / remove token)?
EDIT: Forcing the $auth.fetchUser my auth plugin code does work but with the error mentioned before. When I try to logOut it doesn't work due to the same error when trying to remove the token automatically)
I'm going crazy with this issue for two weeks now.
Thanks so much!

Related

AWS-Amplify, How do I redirect to another URL after sign-in in angular?

I'm using the latest AWS-Amplify's authentication component. It can logged in successfully but after login I need to sent the route to another url which I can't able to achieve, it keeping the same url after logged in. BUT I need to set a custom url where it automatically redirect if a user login successfully.
Note : I'm not using aws-amplify-angular package I'm using these packages,
"#aws-amplify/ui-angular": "^2.4.4",
"aws-amplify": "^4.3.21",
Also I checked this import {AuthenticatorService} from '#aws-amplify/ui-angular'; service but here I didn't find any response with observable type, I think that's why I don't get any event or something instantly after user login successfully. I need to route imminently after a successful login. So I need an event so that I can do that .
My main.ts :
import { Amplify } from 'aws-amplify'
import awsmobile from './aws-exports'
Amplify.configure(awsmobile)
auth.component.html : [ there is no code in ts ]
<amplify-authenticator [signUpAttributes]="['email']"></amplify-authenticator>
& the routes setup like this,
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
canActivate: [AuthGuard]
},
{
path: 'auth',
component: AuthComponent
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}
];
I didn't get any good solution with using this packages. Please help with this issue or am I missed something in my configuration .
I didn't want to use aws-amplify-angular package but I need to because there is no good solution out there, without this package I tried to use angular hooks because when change detection runs I can get authenticated & unauthenticated states from AuthenticatorService which came from import { AuthenticatorService } from '#aws-amplify/ui-angular'; but using hooks (AfterContentChecked, AfterViewChecked) it works sometimes,sometimes not & sometimes occurred errors.
[ I did't use both hooks at a time, I tried separately]
So, Finally I need to install aws-amplify-angular for using the AmplifyService's & then I do this changes bellow,
constructor(private router: Router, private amplifyService: AmplifyService) { }
ngOnInit() {
this.amplifyService.authStateChange$.subscribe(({state, user}) => {
if (state === 'signedIn') {
this.router.navigate(['/home'])
}
})
}
Don't forget to add AmplifyService to put in providers array in app.module.ts.
In the AuthenticatorService import of the latest version of #aws-amplify/ui-angular, you can subscribe and listen for authStatus which is authenticated, deauthenticated and configured, if you listen from ngOnInit or from the constructor you can wait for the necessary status to do redirection.
It also works for the signOut
constructor(public amplifyService: AuthenticatorService, public router: Router) {
this.amplifyService = amplifyService;
amplifyService.subscribe((data: any) => {
if (data.authStatus === "authenticated") {
this.router.navigate(['/home']);
};
})
}
ngOnInit(): void {
}
to make signOut you just have to use this function in your navbar
logOut() {
this.amplifyService.signOut()
this.router.navigate(['/login']);
}
This way you can maintain the latest version of aws-amplify packages and not get errors or failures from compiling outdated packages or packages that conflict with other aws packages.
import { Hub } from 'aws-amplify';
ngOnInit() {
// Used for listening to login events
Hub.listen('auth', ({ payload: { event, data } }) => {
if (event === 'signIn') {
const { signInUserSession = {} } = data;
const { accessToken = {} } = signInUserSession;
const { jwtToken, payload = {} } = accessToken;
const { 'cognito:groups': roles, username, exp } = payload;
localStorage.setItem('accessToken', jwtToken);
const payloadToSave = {
Role: roles[0],
Username: username,
exp,
};
localStorage.setItem('payload', JSON.stringify(payloadToSave));
this.zone.run(() => this.router.navigate(['/dashboard']));
} else {
console.log('Hide loader');
}
});
//currentAuthenticatedUser: when user comes to login page again
Auth.currentAuthenticatedUser()
.then(() => {
this.router.navigate(['/dashboard'], { replaceUrl: true });
})
.catch(err => {
console.log(err);
});
}

How to create a middleware for check role in Nuxtjs

I'm trying to create a middleware for check role of my users.
// middleware/is-admin.js
export default function (context) {
let user = context.store.getters['auth/user']
if ( user.role !== 'admin' ) {
return context.redirect('/errors/403')
}
}
In my .vue file, I'm putting this on:
middleware: [ 'is-admin' ]
It works.
Now, I'd like to check if the user also has another role. So, I create a new middleware:
// middleware/is-consultant.js
export default function (context) {
let user = context.store.getters['auth/user']
if ( user.role !== 'consultant' ) {
return context.redirect('/errors/403')
}
}
And in my .vue file:
middleware: [ 'is-admin', 'is-consultant' ]
Unfortunately, when I do that, if I visit the route with an administrator role, it does not work anymore.
Can you tell me how I can create a middleware that checks multiple roles with Nuxt.js?
Thank you!
The idea is that every page has its authority level. Then in middleware you can compare your current user authority level with the current page authority level, and if it's lower redirect the user. It's very elegant solution that was proposed by Nuxt.js creator. GitHub issue.
<template>
<h1>Only an admin can see this page</h1>
</template>
<script>
export default {
middleware: 'auth',
meta: {
auth: { authority: 2 }
}
}
</script>
Then in your middleware/auth.js:
export default ({ store, route, redirect }) => {
// Check if user is connected first
if (!store.getters['user/user'].isAuthenticated) return redirect('/login')
// Get authorizations for matched routes (with children routes too)
const authorizationLevels = route.meta.map((meta) => {
if (meta.auth && typeof meta.auth.authority !== 'undefined')
return meta.auth.authority
return 0
})
// Get highest authorization level
const highestAuthority = Math.max.apply(null, authorizationLevels)
if (store.getters['user/user'].details.general.authority < highestAuthority) {
return error({
statusCode: 401,
message: 'Du måste vara admin för att besöka denna sidan.'
})
}
}
You can use this feature in Nuxt
export default function ({ $auth, redirect }) {
if (!$auth.hasScope('admin')) {
return redirect('/')
}
}
The scope can be anything you want e.g Consultant, Editor etc.
Check the documentation
Updated
Since you are using Laravel
You can have a role column in your user table
e.g
$table->enum('role', ['subscriber', 'admin', 'editor', 'consultant', 'writer'])->default('subscriber');
Then create a API resource, check the documentation for more
To create a user resource, run this artisan
php artisan make:resource UserResource
Then in your resource, you can have something like this
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'gender' => $this->gender,
'country' => $this->country,
'avatar' => $this->avatar,
'role' => $this->role,
];
}
Then you can import it to your controller like this
use App\Http\Resources\UserResource;
You can get the resource like this
$userdata = new UserResource(User::find(auth()->user()->id));
return response()->json(array(
'user' => $userdata,
));
In Nuxt
To do authentication in Nuxt
Install nuxt auth and axios
Using YARN : yarn add #nuxtjs/auth #nuxtjs/axios
Or using NPM: npm install #nuxtjs/auth #nuxtjs/axios
Then register them in your nuxtconfig.js
modules: [
'#nuxtjs/axios',
'#nuxtjs/auth',
],
In your nuxtconfig.js, add this also
axios: {
baseURL: 'http://127.0.0.1:8000/api'
},
auth: {
strategies: {
local: {
endpoints: {
login: { url: '/login', method: 'post', propertyName: 'access_token' },
logout: { url: '/logout', method: 'post' },
user: { url: '/user', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: 'Bearer',
globalToken: true
// autoFetchUser: true
}
}
}
The URL been the endpoints
Check Documentation for more
To restrict certain pages in Nuxt to Specific User.
> Create a middlweare e.g isadmin.js
Then add this
export default function ({ $auth, redirect }) {
if (!$auth.hasScope('admin')) {
return redirect('/')
}
}
Then go to the Page, add the middleware
export default {
middleware: 'isadmin'

Accessing nuxt $store inside Dynamic Component

I'm developing a Promise-based modal component which provides the possibility of specifing a component as body of the modal itself. To achieve that result, I thought that a good solution would be using a dynamic component inside the modal template.
However, inside a NUXT application, if the component refers to the Vuex instance (this.$store), it turns out to be undefined (or better there is no $store object attribute). In the same way, any injection done inside plugins results undefined (e.g. inject('api', api) create the attribute $api, but it results undefined).
If I just use the component in the 'standard' way (e.g. placing it inside the page or another component template), everything works fine.
There should be some 'extra injection' that I should do before passing the component in a programmatic way.
Can anyone help me?
The NUXT project structure (simplified):
/pages/index.vue
/plugins/api.js
/store/auth.js
/components/HelloComponent.vue
/plugins/api.js
let api = {}
api.call = function (request, auth, unpack, axios = this.axios) {
if (!request) Error('backend.js:call invalid params:', request, auth, unpack, axios)
if (auth) {
if (request.headers)
request.headers['Authorization'] = 'Bearer ' + this.auth.accessToken
else
request.headers = { 'Authorization': 'Bearer ' + this.auth.accessToken }
}
return axios(request).then((response) => unpack ? response.data : response)
}
api.getAPI = function (api, params, auth = true, unpack = true) {
if (!api) Error('api.js:getAPI invalid params:', api)
console.log('api.js:getAPI api:', api)
return this.call({ method: 'get', url: api, params: params }, auth, unpack)
}
api.postAPI = function (api, params, data, auth = true, unpack = true) {
if (!api) Error('api.js:postAPI invalid params:', api, data)
console.log('api.js:postAPI api:', api)
return this.call({ method: 'post', url: api, params: params, data: data }, auth, unpack)
}
/*******************************************************/
/* NUXT plugin and reference injection */
/*******************************************************/
export default function (context, inject) {
console.log('[CALL] api.js')
/* assign global $axios instance */
api.axios = context.$axios
/* assign auth instance to access tokens */
api.auth = context.store.state.auth
/* inject backend reference into application instance */
inject('api', api)
}
/pages/index.vue
<template>
<div>
<span>
{{ $store.auth.state.name }} // -> Displays 'Chuck'
</span>
/* Object.keys(this).includes('$store): false'; Object.keys(this).includes('$auth): true' */
<component :is="cComponent" /> // -> this.$store is undefined; auth: undefined
<hello-component /> // -> Displays 'Chuck'; auth: Object {...}
</div>
</template>
<script>
import HelloComponent from '../components/HelloComponent.vue'
export default {
components: {
HelloComponent
},
created () {
this.$store.commit('auth/setName', 'Chuck')
},
computed: {
cComponent () {
return HelloComponent
}
}
}
</script>
/components/HelloComponent.vue
<template>
<span>
{{ $store.auth.state.name }}
</span>
</template>
<script>
export default {
created() {
console.log('auth:', this.$auth)
}
}
</script>
/store/auth.js
export const state = () => ({
accessToken: null,
refreshToken: null,
name: null,
})
export const mutations = {
setAccessToken(state, token) {
console.info('auth.js:setAccessToken', token)
state.accessToken = token
},
setRefreshToken(state, token) {
console.info('auth.js:setRefreshToken', token)
state.refreshToken = token
},
setName(state, name) {
console.info('auth.js:setName', name)
state.user = name
},
}
if you have no access of this pointer in Nuxt project, And you really need to access store, then simply use
window.$nuxt.$store instead of this.$store;
Hope it will solve your problem

How to make Nuxt-auth and Nuxt-i18n to be friends

I want to use nuxt#auth module and nuxt-i18n together. The problem appears when I want to have different routes for login page. For example:
pages: {
login: {
en: '/authorization',
fr: '/autorisation'
}
}
Routes are working well, but when I try to use nuxt#auth with all page restricted, the default redirect asks for /login page. In nuxt.config.js file I can rewrite redirect route, but I can set only one redirect.
auth: {
redirect: {
login: '/fr/autorisation'
}
}
How can I show to auth module to ask for route of selected language? Thanks in advance!
Hope it'll be helpful for somebody =)
export default ({ app, $auth }) => {
$auth.onRedirect((to, from) => {
return app.localePath(to)
})
}
It seems that there was a solution for that problem https://github.com/nuxt-community/auth-module/pull/185, but I can't access to onRedirect method in the current release.
I did a workaround. I added auth-lang-redirect.js plugin, which overrides redirect option defined in the nuxt.config.js file.
export default ({ app }) => {
var redirect = app.$auth.$storage.options.redirect
for (var key in redirect) {
redirect[key] = '/' + app.i18n.locale + redirect[key]
}
app.$auth.$storage.options.redirect = redirect
}
Notice that I don't use nuxt-i18n module, but you should get the point.
You have to register this plugin in nuxt.config.js like this:
auth: {
strategies: { ... },
redirect: {
login: '/login',
logout: '/',
callback: '/login',
home: '/user/profile'
},
plugins: ['#/plugins/auth-lang-redirect.js']
},
export default function({ app, $auth }) {
const redirect = { ...$auth.$storage.options.redirect };
const localizeRedirects = () => {
for (const key in redirect) {
$auth.$storage.options.redirect[key] = app.localePath(redirect[key]);
}
};
localizeRedirects();
app.i18n.onLanguageSwitched = () => {
localizeRedirects();
};
}

Configure Identity Server 4 With Ionic 2

I'm trying to configure Identity Server to work with Ionic 2. I'm a bit confused on how to configure the Redirect urls. For when I'm testing in the browser.
I'm in the process of updating and integrating an OIDC Cordova component.
The old component git hub is here:
https://github.com/markphillips100/oidc-cordova-demo
I've created a typescript provider and registered it with my app.module.ts
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import { Component } from '#angular/core';
import * as Oidc from "oidc-client";
import { Events } from 'ionic-angular';
import { environment } from "../rules/environments/environment";
export class UserInfo {
user: Oidc.User = null;
isAuthenticated: boolean = false;
}
#Injectable()
export class OidcClientProvider {
USERINFO_CHANGED_EVENT_NAME: string = ""
userManager: Oidc.UserManager;
settings: Oidc.UserManagerSettings;
userInfo: UserInfo = new UserInfo();
constructor(public events:Events) {
this.settings = {
//authority: "https://localhost:6666",
authority: environment.identityServerUrl,
client_id: environment.clientAuthorityId,
//This doesn't work
post_logout_redirect_uri: "http://localhost/oidc",
redirect_uri: "http://localhost/oidc",
response_type: "id_token token",
scope: "openid profile",
automaticSilentRenew: true,
filterProtocolClaims: true,
loadUserInfo: true,
//popupNavigator: new Oidc.CordovaPopupNavigator(),
//iframeNavigator: new Oidc.CordovaIFrameNavigator(),
}
this.initialize();
}
userInfoChanged(callback: Function) {
this.events.subscribe(this.USERINFO_CHANGED_EVENT_NAME, callback);
}
signinPopup(args?): Promise<Oidc.User> {
return this.userManager.signinPopup(args);
}
signoutPopup(args?) {
return this.userManager.signoutPopup(args);
}
protected initialize() {
if (this.settings == null) {
throw Error('OidcClientProvider required UserMangerSettings for initialization')
}
this.userManager = new Oidc.UserManager(this.settings);
this.registerEvents();
}
protected notifyUserInfoChangedEvent() {
this.events.publish(this.USERINFO_CHANGED_EVENT_NAME);
}
protected clearUser() {
this.userInfo.user = null;
this.userInfo.isAuthenticated = false;
this.notifyUserInfoChangedEvent();
}
protected addUser(user: Oidc.User) {
this.userInfo.user = user;
this.userInfo.isAuthenticated = true;
this.notifyUserInfoChangedEvent();
}
protected registerEvents() {
this.userManager.events.addUserLoaded(u => {
this.addUser(u);
});
this.userManager.events.addUserUnloaded(() => {
this.clearUser();
});
this.userManager.events.addAccessTokenExpired(() => {
this.clearUser();
});
this.userManager.events.addSilentRenewError(() => {
this.clearUser();
});
}
}
I'm trying to understand how I would configure the redirect urls so I can authenticate normally in the browser. Normally you would configure a redirect
url to take your process the token and claims after login.
this.settings = {
authority: environment.identityServerUrl,
client_id: environment.clientAuthorityId,
post_logout_redirect_uri: "http://localhost:8100/oidc",
redirect_uri: "http://localhost:8100/oidc",
response_type: "id_token token",
scope: "openid profile AstootApi",
automaticSilentRenew: true,
filterProtocolClaims: true,
loadUserInfo: true,
//popupNavigator: new Oidc.CordovaPopupNavigator(),
//iframeNavigator: new Oidc.CordovaIFrameNavigator(),
}
Ionic 2 doesn't use urls for routing, Supposing I have a component AuthenticationPage which handles storing the authentication token.
How can I configured a redirect url so it navigates to the authentication page, so I can test this in the browser?
TL;DR
I had to do a few things to get this working.
I didn't realize at first but My Redirect Urls had to be matching for what my client has stored in identity server.
new Client
{
ClientId = "myApp",
ClientName = "app client",
AccessTokenType = AccessTokenType.Jwt,
RedirectUris = { "http://localhost:8166/" },
PostLogoutRedirectUris = { "http://localhost:8166/" },
AllowedCorsOrigins = { "http://localhost:8166" },
//...
}
So the OIDC client in Typescript needed to be updated too.
this.settings = {
authority: environment.identityServerUrl,
client_id: environment.clientAuthorityId,
post_logout_redirect_uri: "http://localhost:8166/",
redirect_uri: "http://localhost:8166/",
response_type: "id_token token",
}
Also since I didn't feel like setting up routing in Ionic I needed to figure out a way to a url to communicate with Ionic (For Browser testing purpose, normal commucation will be done through cordova).
So I pointed the redirct url to be the url ionic is hosting my application and on app.Component.ts in the Constructor I added code to try to get my authentication token.
constructor(
public platform: Platform,
public menu: MenuController,
public oidcClient: OidcClientProvider
)
{
//Hack: since Ionic only has 1 default address, attempt to verify if this is a call back before calling
this.authManager.verifyLoginCallback().then((isSuccessful) => {
if (!isSuccessful) {
this.authManager.IsLoggedIn().then((isLoggedIn) => {
if (isLoggedIn) {
return;
}
this.nav.setRoot(LoginComponent)
});
}
});
}
Edit Verify login call back should just the oidc client call back which will read the token from the get params
verifyLoginCallback(): Promise<boolean> {
return this.oidcClient.userManager.signinPopupCallback()
.then(user => {
return this.loginSuccess(user).
then(() => true,
() => false);
}, err => { console.log(err); return false; });
}
NOTE the Login component is just a modal which represents login landing page, which just uses a login button to initialize the popup. You can hook this into any user driven event to trigger the login, but you must use a user driven event if you want to support the web without triggering a popup blocker
<ion-footer no-shadow>
<ion-toolbar no-shadow position="bottom">
<button ion-button block (click)="login()">Login</button>
</ion-toolbar>
</ion-footer>
login(): Promise<any> {
return this.oidcClient.signinPopup().then((user) => {
this.events.publish(environment.events.loginSuccess);
}).catch((e) => { console.log(e); });
}
I'm sure there is a better do the redirect to a different route, This is just a quick and dirty hack

Categories