Angular Auth0 after login secured auth guard page doesn't show after redirect - auth0

I am testing Auth0 with angular for the first time and have run into an issue. I let my user login trough Auth0 and after they have logged in I redirect them to my dashboard page that is protected with Auth0 guard. I have managed to redirect the user but after they log in the URL shows that the user is on the dashboard route however they can't see the actual dashboard content. I tried to see what is going on and I think the problem is that the user is logged in but the auth guard boolean is never set to true which makes my program continuously refresh the URL. When I take the auth guard off the route the redirect works and shows the content. I have played around with the code hoping to figure it out but so far no luck.
I will share my code if I have to share more code please let me know.
app.module.ts
const appRoutes: Routes = [
{ path: 'app-dashboard', component: DashboardComponent,
canActivate: [AuthGuard]
},
{ path: 'login', component: LoginComponent },
{ path: 'app-shell-start', component: ShellStartComponent },
{
path: '',
redirectTo: '/app-shell-start',
pathMatch: 'full'
},
];
auth.guard.ts
#Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, public router:Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean|UrlTree> | boolean {
console.log("start")
return this.auth.isAuthenticated$.pipe(
tap(loggedIn => {
console.log("loggedIn",loggedIn)
this.router.navigate(['']);
if (!loggedIn) {
this.auth.login(state.url);
}
})
);
}
}
auth.services.spec.ts
import { TestBed } from '#angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
auth.service.ts
#Injectable({
providedIn: 'root'
})
export class AuthService {
// Create an observable of Auth0 instance of client
auth0Client$ = (from(
createAuth0Client({
domain: "techradartest.eu.auth0.com",
client_id: "KxM7ICZKf08Mt5czJnxxD3a47HwH3jE1",
redirect_uri: "http://localhost:4200/app-dashboard" // `${window.location.origin}`
})
) as Observable<Auth0Client>).pipe(
shareReplay(1), // Every subscription receives the same shared value
catchError(err => throwError(err))
);
// Define observables for SDK methods that return promises by default
// For each Auth0 SDK method, first ensure the client instance is ready
// concatMap: Using the client instance, call SDK method; SDK returns a promise
// from: Convert that resulting promise into an observable
isAuthenticated$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.isAuthenticated())),
tap(res => this.loggedIn = res)
);
handleRedirectCallback$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.handleRedirectCallback()))
);
// Create subject and public observable of user profile data
private userProfileSubject$ = new BehaviorSubject<any>(null);
userProfile$ = this.userProfileSubject$.asObservable();
// Create a local property for login status
loggedIn: boolean = null;
constructor(private router: Router) {
// On initial load, check authentication state with authorization server
// Set up local auth streams if user is already authenticated
this.localAuthSetup();
// Handle redirect from Auth0 login
this.handleAuthCallback();
}
// When calling, options can be passed if desired
// https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser
getUser$(options?): Observable<any> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getUser(options))),
tap(user => this.userProfileSubject$.next(user))
);
}
private localAuthSetup() {
// This should only be called on app initialization
// Set up local authentication streams
const checkAuth$ = this.isAuthenticated$.pipe(
concatMap((loggedIn: boolean) => {
if (loggedIn) {
// If authenticated, get user and set in app
// NOTE: you could pass options here if needed
return this.getUser$();
}
// If not authenticated, return stream that emits 'false'
return of(loggedIn);
})
);
checkAuth$.subscribe();
}
login(redirectPath: string = '/') {
// A desired redirect path can be passed to login method
// (e.g., from a route guard)
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log in
client.loginWithRedirect({
redirect_uri: "http://localhost:4200/app-dashboard", //`${window.location.origin}`,
appState: { target: redirectPath }
});
});
}
private handleAuthCallback() {
// Call when app reloads after user logs in with Auth0
const params = window.location.search;
debugger;
if (params.includes('code=') && params.includes('state=')) {
console.log("loggedIn handleAuthCallback")
let targetRoute: string; // Path to redirect to after login processsed
const authComplete$ = this.handleRedirectCallback$.pipe(
// Have client, now call method to handle auth callback redirect
tap(cbRes => {
// Get and set target redirect route from callback results
targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/';
}),
concatMap(() => {
// Redirect callback complete; get user and login status
return combineLatest([
this.getUser$(),
this.isAuthenticated$
]);
})
);
// Subscribe to authentication completion observable
// Response will be an array of user and login status
authComplete$.subscribe(([user, loggedIn]) => {
// Redirect to target route after callback processing
console.log("targetRoute: ",targetRoute)
this.router.navigate([targetRoute]);
});
}
}
logout() {
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log out
client.logout({
client_id: "KxM7ICZKf08Mt5czJnxxD3a47HwH3jE1",
returnTo: `${window.location.origin}`
});
});
}
}
I am using Angular 9

Related

Sequelize model property undefined Express.js controller after auth with passport-jwt

I am using passport-jwt to verify access to a given route in express.js, and then return a Sequelize model to the final controller. The code looks like:
The auth strategy:
const passportStrategy = passport => {
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.auth.ACCESS_TOKEN_SECRET
};
passport.use(
new Strategy(options, async (payload, done) => {
try {
const user = await User.findOne({ where: { email: payload.email }});
if (user) {
return done(null, {
user
});
}
return done(null, false);
}
catch (error) {
return done(error, false)
}
})
);
};
The route with the auth middleware
router.get('/:user_id/psy', passport.authenticate('jwt', { session: false }), patientsController.getPatientPsy);
The controller function
const getPatientPsy = async (req, res) => {
const authenticatedUser = req.user;
if (authenticatedUser.userType !== "patient") {
res.status(500).send("Big time error");
}
}
If I console.log(authenticatedUser) in the getPatientPsy() controller it successfully prints the Sequelize model with it's dataValues and so on, but when I try to access any property, be it userType or any other it consistently returns undefined.
In the passport-jwt authentication once a User has been found that matches the extracted JWT token, afaik it is returned synchronously and made it available in the req.user object, and I can print it with console.log, but why can't I access the model's properties?
I've tried to make the getPatientPsy() controller a sync function but it doesn't work either.
Thank you.
All right this is embarrassing, by default Passport.js returns the done(null, user) in the req.user property, and since I am returning { user }, I had to access through req.user.user.

Auth0 login redirect to page keeps on loading when using Firefox as browser

I use an Auth0 login that handles the login for my platform after logging in it redirects you to the platform. With chrome, this works perfectly but in Firefox it keeps reloading the redirect page. I used the standard auth0 code to implement the logic into my page. I'm not sure what is going wrong and even where to start looking for a solution? Does anyone ever experienced the same bug and can help me fix the issue and also explain what is going wrong?
auth.ts file :
export class AuthService {
// Create an observable of Auth0 instance of client
auth0Client$ = (from(
createAuth0Client({
domain: authConfig.domain,
client_id: authConfig.clientId,
redirect_uri: `${window.location.origin}`,
audience: authConfig.audience,
})
) as Observable<Auth0Client>).pipe(
shareReplay(1), // Every subscription receives the same shared value
catchError(err => throwError(err))
);
// Manage Acces Token
// Observable method to retrieve access token and make it available for use in application
getTokenSilently$(options?): Observable<string> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getTokenSilently(options)))
);
}
// Define observables for SDK methods that return promises by default
// For each Auth0 SDK method, first ensure the client instance is ready
// concatMap: Using the client instance, call SDK method; SDK returns a promise
// from: Convert that resulting promise into an observable
isAuthenticated$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.isAuthenticated())),
tap(res => (this.loggedIn = res))
);
handleRedirectCallback$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.handleRedirectCallback()))
);
// Create subject and public observable of user profile data
private userProfileSubject$ = new BehaviorSubject<any>(null);
userProfile$ = this.userProfileSubject$.asObservable();
// Create a local property for login status
loggedIn: boolean = null;
constructor(private router: Router) {
// On initial load, check authentication state with authorization server
// Set up local auth streams if user is already authenticated
this.localAuthSetup();
// Handle redirect from Auth0 login
this.handleAuthCallback();
}
// When calling, options can be passed if desired
// https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser
getUser$(options?): Observable<any> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getUser(options))),
tap(user => this.userProfileSubject$.next(user))
);
}
private localAuthSetup() {
// This should only be called on app initialization
// Set up local authentication streams
const checkAuth$ = this.isAuthenticated$.pipe(
concatMap((loggedIn: boolean) => {
if (loggedIn) {
// If authenticated, get user and set in app
// NOTE: you could pass options here if needed
return this.getUser$();
}
// If not authenticated, return stream that emits 'false'
return of(loggedIn);
})
);
checkAuth$.subscribe();
}
login(redirectPath: string = '/') {
// A desired redirect path can be passed to login method
// (e.g., from a route guard)
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log in
client.loginWithRedirect({
redirect_uri: `${window.location.origin}`,
appState: { target: redirectPath },
});
});
}
private handleAuthCallback() {
// Call when app reloads after user logs in with Auth0
const params = window.location.search;
if (params.includes('code=') && params.includes('state=')) {
let targetRoute: string; // Path to redirect to after login processsed
const authComplete$ = this.handleRedirectCallback$.pipe(
// Have client, now call method to handle auth callback redirect
tap(cbRes => {
// Get and set target redirect route from callback results
targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/';
}),
concatMap(() => {
// Redirect callback complete; get user and login status
return combineLatest([this.getUser$(), this.isAuthenticated$]);
})
);
// Subscribe to authentication completion observable
// Response will be an array of user and login status
authComplete$.subscribe(([user, loggedIn]) => {
// Redirect to target route after callback processing
this.router.navigate([targetRoute]);
});
}
}
logout() {
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log out
client.logout({
client_id: authConfig.clientId,
returnTo: window.location.origin,
});
});
}
}
auth-guard.ts
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean | UrlTree> | boolean {
return this.auth.isAuthenticated$.pipe(
tap(loggedIn => {
if (!loggedIn) {
this.auth.login(state.url);
}
})
);
}
}
I had this issue and found that it was because my browser was blocking third party cookies.
So my app was running on my own domain (or on localhost in dev mode), but I was using an auth0.com subdomain to log in with Auth0 and the browser was blocking cookies from auth0.com, which sent the auth0 JS in my app into an infinite loop of trying to check the logged in status, failing, refreshing the page, trying to check the logged in status, failing, refreshing the page, and so on.
It could be your browser's cookie settings or an ad blocking or privacy extension.

JWT authentication with AXIOS

Using Vue webpack template, trying to make JWT authentication. What I've done so far:
"src/auth/index.js":
// Send a request to the login URL and save the returned JWT
login (creds, redirect) {
axios.post(LOGIN_URL, creds, (data) => {
localStorage.setItem('access_token', data.access_token)
this.user.authenticated = true
// Redirect to a specified route
if (redirect) {
router.push(redirect)
}
}).error((err) => {
context.error = err
})
},
I'm calling this function from LoginPage.vue:
methods: {
login () {
var credentials = {
username: this.credentials.username,
password: this.credentials.password
}
// We need to pass the component's this context
// to properly make use of http in the auth service
auth.login(this, credentials, 'requests')
}
}
When I'm submitting the form, data is submitted, but I get the following error in a console:
TypeError: __WEBPACK_IMPORTED_MODULE_1_axios___default.a.post(...).error is not a function
Also JWT token is not saving in my local storage, what am I doing wrong?
Rewrote login function:
login (context, creds, redirect) {
axios.post(LOGIN_URL, creds)
.then((response) => {
localStorage.setItem('access_token', response.data.access_token)
this.user.authenticated = true
if (redirect) {
router.push(redirect)
}
}).catch((err) => {
context.error = err.response.data
})
},
Everything is working now.

benefit of deserializeUser method in passport.js

I have just started with passport.js. From this article, I got what is the flow of all the passport methods and implemented the same in my application and it is working. Here is my server.js and I am using passport-local strategy. Angular app and rest APIs on the same server
import { registerControllersFromFolder } from 'giuseppe';
import { MessageManager } from './messaging/MessageManager';
import express = require('express');
import bodyParser = require('body-parser');
import session = require("express-session");
import http = require('http');
// class to hold user info
class User {
userId: number;
userName: string;
constructor(userId: number, userName: string) {
this.userId = userId;
this.userName = userName;
}
}
// server class to create http server
export class Server {
// list of apis for which authentication is not required
private static publicApiList: string[] = ["/services/login", "/login", "/favicon.ico"];
// request interceptor that will check user authentication
private static isAuthenticated = (req, res, next) => {
console.log("Authenticating :", req.originalUrl);
if (req.isAuthenticated() || Server.publicApiList.indexOf(req.originalUrl) > -1) {
// express routing
if (req.originalUrl.startsWith("/services")) {
console.log("Express Routing");
return next();
} else { // angular routing -> return index.html
console.log("Angular Routing");
return res.sendFile(__dirname + "/public/index.html");
}
} else {
console.log("User not authenticated.")
res.redirect('/');
}
};
static startServer() {
let userList: User[] = [new User(1, "Sunil"), new User(2, "Sukhi")];
let app = express();
// passport library
let passport = require('passport');
let LocalStrategy = require('passport-local').Strategy;
// middlewares
app.use(express.static(__dirname + "/public"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({ resave: false, saveUninitialized: true, secret: "secretKey123!!" }));
// passport middleware invoked on every request to ensure session contains passport.user object
app.use(passport.initialize());
// load seriliazed session user object to req.user
app.use(passport.session());
// Only during the authentication to specify what user information should be stored in the session.
passport.serializeUser(function (user, done) {
console.log("Serializer : ", user);
done(null, user);
});
// Invoked on every request by passport.session
passport.deserializeUser(function (user, done) {
let validUser = userList.filter(user => user.userId === user.userId)[0];
console.log("D-serializer : ", validUser);
done(null,validUser);
});
// passport strategy : Only invoked on the route which uses the passport.authenticate middleware.
passport.use(new LocalStrategy({
usernameField: 'name',
passwordField: 'password'
},
function (username, password, done) {
console.log("Strategy : Authenticating if user is valid :", username)
let user = userList.filter(user => username === user.userName);
console.log("Valid user : ", user)
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
return done(null, user[0]);
}
));
// intercept request for authentication
app.use(Server.isAuthenticated);
app.post('/services/login', passport.authenticate('local', {
successRedirect: '/profile',
failureRedirect: '/login'
}));
app.get('/services/logout', (req: any, res: any) => {
req.logout();
console.log("User Logout");
res.send("{status:'logout'}")
});
// http server creation
let server = http.createServer(app);
registerControllersFromFolder({ folderPath: './api' })
.then(router => {
app.use(router);
/* start express server */
})
.catch(err => {
/* error happened during loading and registering */
});
server.listen(7000, () => {
console.log('Up and running on port 7000');
});
}
}
exports.startServer = Server.startServer;
// Call a module's exported functions directly from the command line.
require('make-runnable');
When I hit localhost:7000 it serves the index.html page as I have used
app.use(express.static(__dirname + "/public"));
and this is an angular app and because of angular routing login module will get loaded by default. I have used a middleware that checks request authentication and if true then based on request prefix (angular or express) routing is done.
For the login request defined local strategy method is called and if this is true it calls serializer method that takes the responsibility which data should be stored in the request session. and then sucessRedirect or failureRedirect is called.
For subsequent request, As I have used middleware that checks if req.isAuthenticated is true if so then request is served otherwise the user is redirected to login page. I know in every subsequent request deserializeUser method is called that contains the object that was stored by serializeUser method in the login request. As per the document, this makes a call to the database to check valid user.
But I am confused but is the actual use case of deserializeUser method? Where can I take the benefit of this method and if I am intercepting ecah request and check req.isAuthenticted() then why to call database in deserializeUser method?>
As stated in this answer
The first argument of deserializeUser corresponds to the key of the
user object that was given to the done function (see 1.). So your
whole object is retrieved with help of that key. That key here is the
user id (key can be any key of the user object i.e. name,email etc).
In deserializeUser that key is matched with the in memory array /
database or any data resource.
The fetched object is attached to the request object as req.user
Thus, the benefit of deserializeUser is that you have the user object available on every request thereafter.
You ask why you need to use deserializeUser if you call req.isAuthenticated, and the answer lies in the implementation of req.isAuthenticated:
req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
return (this[property]) ? true : false;
};
To me, it looks like req.isAuthenticated is looking for req[user] to be set, and thus, deserializeUser must be called before it can work.

Passport middleware, check if the user already has a living session from

I am building a web application using angular-fullstack. The stack is using express-sessions for session storage (in Mongodb) and passport.js for authentication.
I want to limit each user to a single login session. I am trying find a way to check if a user already has a living session when they login.
Is there a way to programmatically call a route to query mongodb from the passport middleware?
'use strict';
import path from 'path';
import passport from 'passport';
import {Strategy as LocalStrategy} from 'passport-local';
import express from 'express';
import session from 'express-session';
import _ from 'lodash';
import Session from '../../api/session/session.model';
var app = express();
require('run-middleware')(app);
function localAuthenticate(User, email, password, done, req) {
User.findOne({
email: email.toLowerCase()
}).exec()
.then(user => {
if (!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
// HERE is where I am trying to check if a user
// already has a living session when they login
// I tried to use the runMiddleware
// to query mongodb for all the existing sessions
// but I get this error: http://pastebin.com/YTeu5AwA
app.runMiddleware('/sessions',{},function(code,data){
console.log(code) // 200
console.log(data) // { user: '20', name: 'Moyshale' }
});
// Is there a way to access and use an existing route?
user.authenticate(password, function(authError, authenticated) {
if (authError) {
return done(authError);
}
if (!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
} else {
return done(null, user);
}
});
})
.catch(err => done(err));
}
export function setup(User, config) {
passport.use(new LocalStrategy({
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password' // this is the virtual field on the model
}, function(req, email, password, done) {
return localAuthenticate(User, email, password, done, req);
}));
}
Ok, I figured it out and I'll try and explain what I did. My specific implementation required me to set up user 'seats', where each user is part of a group and each group is limited in N number of logins at a single time.
As I mentioned in the question, I am using the angular fullstack yeoman generator, so this solution is specific to that setup.
I created a 'sessions' API endpoint so that I could query and modify the sessions stored in the mongo db. I included a 'seat' record with type Number into the sessions model. This is used to keep track of the users seat status for each session. Each user is given a 'loginSeat' value which is used to populate this filed. Also the session now has a seatAllowed of type Boolean, true: the user is allowed to access the site, false: the user is not allowed access to the site.
'use strict';
import mongoose from 'mongoose';
var SessionSchema = new mongoose.Schema({
_id: String,
session: String,
expires: Date,
seat: Number,
seatAllowed: Boolean // true: the user is allowed to access the site, false: the user is not allowed access to the site
});
export default mongoose.model('Session', SessionSchema);
I modified server/auth/login/passport.js so that when a user logs into the site, all other users with a matching seat are bumped out.
'use strict';
import path from 'path';
import passport from 'passport';
import {Strategy as LocalStrategy} from 'passport-local';
import _ from 'lodash';
import Sessions from '../../api/session/session.model';
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
return updated.save()
.then(updated => {
return updated;
});
};
}
function localAuthenticate(User, email, password, done, req) {
User.findOne({
email: email.toLowerCase()
}).exec()
.then(user => {
if (!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
// When a user logs into the site we flag their seat as allowed
var updateSession = {'seat': user.loginSeat, 'seatAllowed': true};
Sessions.findById(req.session.id).exec()
.then(saveUpdates(updateSession))
// When a user logs into the site, we disallow the seats of all other sessions with matching seat
Sessions.find().exec()
.then(sessions => {
// Check for existing user logged in with matching login seat
for (var i = 0; i < sessions.length; i++) {
if (sessions[i].seat === user.loginSeat && sessions[i].id !== req.session.id) {
console.log('DISALOW SEAT:');
var updateSession = {'seatAllowed': false};
Sessions.findById(sessions[i].id).exec()
.then(saveUpdates(updateSession));
}
}
});
user.authenticate(password, function(authError, authenticated) {
if (authError) {
return done(authError);
}
if (!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
} else {
return done(null, user);
}
});
})
.catch(err => done(err));
}
export function setup(User, config) {
passport.use(new LocalStrategy({
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password' // this is the virtual field on the model
}, function(req, email, password, done) {
return localAuthenticate(User, email, password, done, req);
}));
}
Each time the client makes a request the isAuthenticated function is triggered. This is where I check for the seaAllowed boolean for the current session, if true, allow the user to access the site, otherwise logout the user:
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
return updated.save()
.then(updated => {
return updated;
});
};
}
/**
* Attaches the user object to the request if authenticated
* Otherwise returns 403
*/
export function isAuthenticated() {
return compose()
// Validate jwt
.use(function(req, res, next) {
// Allow access_token to be passed through query parameter as well
if (req.query && req.query.hasOwnProperty('access_token')) {
req.headers.authorization = 'Bearer ' + req.query.access_token;
}
validateJwt(req, res, next);
})
// Attach user to request
.use(function(req, res, next) {
User.findById(req.user._id).exec()
.then(user => {
if (!user) {
return res.status(401).end();
}
req.user = user;
///////////////////////////
// Login seat limitation //
///////////////////////////
// Check if the user seat is allowed
Sessions.findById(req.session.id).exec()
.then(thisSession => {
// TODO access the session in a better way
if (thisSession.seatAllowed === false || thisSession.seatAllowed === undefined) {
res.redirect('/login');
}
})
next();
})
.catch(err => next(err));
});
}
Thats it.