I've run into an interesting issue where my Angular Guard doesn't seem to do anything when I try to redirect in cases where the user is trying to access routes that they shouldn't be.
As you can see, I've placed console.log calls throughout the process and I see the messages I'd expect, but the navigation never occurs and the originally requested page loads like normal. This has really got me stumped - any help would be much appreciated!
The Guard
// angular dependencies
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
// rxjs utils
import { Observable } from 'rxjs';
import { take, map, tap } from 'rxjs/operators';
// service(s)
import { AuthService } from './services/auth/auth.service';
// interface(s)
import User from './interfaces/user.interface';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private _authService: AuthService, private _router: Router) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const url = state.url;
console.log('Auth Guard hit', { url: url });
return this._authService.user.pipe(
take(1),
map(user => {
console.log(user);
return !!user;
}),
tap(authenticated => {
console.log(`Authenticated: ${authenticated}`);
// not authenticated
if (!authenticated) {
// accessing sign in or sign up pages
if (['/login', '/register'].includes(url)) {
console.log('Allow through');
return true;
}
// accessing application
else {
console.log('Should bounce to login');
return this._router.createUrlTree(['/login']);
}
}
// authenticated
else {
// accessing sign in or sign up pages
if (['/login', '/register'].includes(url)) {
console.log('Should bounce to dashboard');
return this._router.createUrlTree(['/dashboard']);
}
// accessing application
else {
console.log('Allow through');
return true;
}
}
})
);
}
}
AuthService
// angular dependencies
import { Injectable } from '@angular/core';
// firebase dependencies
import { AngularFireAuth } from '@angular/fire/auth';
import { AngularFirestore } from '@angular/fire/firestore';
// RXJS helpers
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
// interfaces
import User from '../../interfaces/user.interface';
@Injectable({ providedIn: 'root' })
export class AuthService {
public user: Observable<User> = null;
constructor(private _auth: AngularFireAuth, private _firestore: AngularFirestore) {
this.user = this._auth.authState.pipe(
switchMap(({ uid }) => {
if (uid) {
return this._firestore.doc(`users/${uid}`).valueChanges();
} else {
return of(null);
}
})
);
}
public async createAccount(email: string, password: string, forename: string, surname: string, relevantTags: string[] = []) {
// create the user in the auth system
let authUser;
try {
authUser = await this._auth.auth.createUserWithEmailAndPassword(email, password);
} catch (error) {
console.error('Failed to create user in auth system', { reason: error.message });
throw error;
}
// flesh out the user data
const data: User = {
uid: authUser.user.uid,
forename: forename,
surname: surname,
displayName: `${forename} ${surname}`,
roles: ['user'], // everyone has user role, can be promoted by admin as required
relevantTags: relevantTags,
emailVerified: false
};
// create the user in the database
try {
this._firestore.doc(`users/${data.uid}`).set(data);
} catch (error) {
console.error('Failed to create user in database', { reason: error.message });
throw error;
}
// attempt to send the initial verification email
try {
this.sendVerification();
} catch (error) {
console.warn('Failed to send verification email', { reason: error.message });
throw error;
}
}
public async signIn(email: string, password: string) {
// attempt to sign in
let result: firebase.auth.UserCredential;
try {
result = await this._auth.auth.signInWithEmailAndPassword(email, password);
} catch (error) {
console.error('Failed to log in', { reason: error.message });
throw error;
}
// store the user data for access by controllers
try {
this.user = this._firestore.doc<User>(`users/${result.user.uid}`).valueChanges();
} catch (error) {
console.error('Failed to set user on service', { reason: error.message });
this._auth.auth.signOut();
throw new Error('Failed to log in');
}
}
public async signOut() {
// attempt to sign out
try {
await this._auth.auth.signOut();
} catch (error) {
console.error('Failed to log out', { reason: error.message });
throw new Error('Failed to log out');
}
}
public async sendVerification() {
// attempt to send verification email
try {
this._auth.auth.currentUser.sendEmailVerification();
} catch (error) {
console.error('Failed to send verification', { reason: error.message });
throw new Error('Failed to send verification');
}
}
}
The App
As you can see from the screenshot below, the logs are correct, and we should be redirected to the dashboard, yet the Login component and route are still activated.
Angular Guard should resolve with a boolean. So instead of using tap operator, use map operator and always return a boolean, even when you're redirecting. Try changing that code to below:
return this._authService.user.pipe(
take(1),
map(user => {
console.log(user);
return !!user;
}),
map(authenticated => {
console.log(`Authenticated: ${authenticated}`);
// not authenticated
if (!authenticated) {
// accessing sign in or sign up pages
if (['/login', '/register'].includes(url)) {
console.log('Allow through');
return true;
}
// accessing application
else {
console.log('Should bounce to login');
this._router.navigateByUrl('/login');
return false;
}
}
// authenticated
else {
// accessing sign in or sign up pages
if (['/login', '/register'].includes(url)) {
console.log('Should bounce to dashboard');
this._router.navigateByUrl('/dashboard');
return false;
}
// accessing application
else {
console.log('Allow through');
return true;
}
}
})
);
Update: Since v7.1, the guard should resolve to either a boolean or UrlTree object. If a UrlTree object is returned, the guard will route to that Url instead. Router.parseUrl
or Router.createUrlTree
methods can be used to create UrlTree object.
return this._authService.user.pipe(
take(1),
map(user => {
console.log(user);
return !!user;
}),
map(authenticated => {
console.log(`Authenticated: ${authenticated}`);
// not authenticated
if (!authenticated) {
// accessing sign in or sign up pages
if (['/login', '/register'].includes(url)) {
console.log('Allow through');
return true;
}
// accessing application
else {
console.log('Should bounce to login');
return this._router.parseUrl('/login');
}
}
// authenticated
else {
// accessing sign in or sign up pages
if (['/login', '/register'].includes(url)) {
console.log('Should bounce to dashboard');
this._router.parseUrl('/dashboard');
}
// accessing application
else {
console.log('Allow through');
return true;
}
}
})
);