Reputation: 1219
I am trying to use Angular2 router guards to restrict access to some pages in my app. I am using Firebase Authentication. In order to check if a user is logged in with Firebase, I have to call .subscribe()
on the FirebaseAuth
object with a callback. This is the code for the guard:
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFireAuth } from "angularfire2/angularfire2";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AngularFireAuth, private router: Router) {}
canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
this.auth.subscribe((auth) => {
if (auth) {
console.log('authenticated');
return true;
}
console.log('not authenticated');
this.router.navigateByUrl('/login');
return false;
});
}
}
When a navigate to a page that has the guard on it, either authenticated
, or not authenticated
is printed to the console (after some delay waiting for the response from firebase). However, the navigation is never completed. Also, if I am not logged in I am redirected to the /login
route. So, the issue I am having is return true
doesn't display the requested page to the user. I'm assuming this is because I am using a callback, but I am unable to figure out how to do it otherwise. Any thoughts?
Upvotes: 112
Views: 96236
Reputation: 414
using async await... you wait for the promise to resolve
async getCurrentSemester() {
let boolReturn: boolean = false
let semester = await this.semesterService.getCurrentSemester().toPromise();
try {
if (semester['statusCode'] == 200) {
boolReturn = true
} else {
this.router.navigate(["/error-page"]);
boolReturn = false
}
}
catch (error) {
boolReturn = false
this.router.navigate(["/error-page"]);
}
return boolReturn
}
Here is my auth gaurd (@angular v7.2)
async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
let security: any = null
if (next.data) {
security = next.data.security
}
let bool1 = false;
let bool2 = false;
let bool3 = true;
if (this.webService.getCookie('token') != null && this.webService.getCookie('token') != '') {
bool1 = true
}
else {
this.webService.setSession("currentUrl", state.url.split('?')[0]);
this.webService.setSession("applicationId", state.root.queryParams['applicationId']);
this.webService.setSession("token", state.root.queryParams['token']);
this.router.navigate(["/initializing"]);
bool1 = false
}
bool2 = this.getRolesSecurity(next)
if (security && security.semester) {
// ---- watch this peace of code
bool3 = await this.getCurrentSemester()
}
console.log('bool3: ', bool3);
return bool1 && bool2 && bool3
}
route is
{ path: 'userEvent', component: NpmeUserEvent, canActivate: [AuthGuard], data: { security: { semester: true } } },
Upvotes: 2
Reputation: 105
In order to show another way of implementation. As per documentation, and mentioned by other answers return type of CanActivate can also be a Promise that resolves to boolean.
Note: The example shown is implemented in Angular 11, but is applicable to Angular 2+ versions.
Example:
import {
Injectable
} from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
CanActivateChild,
Router,
RouterStateSnapshot,
UrlTree
} from '@angular/router';
import {
Observable
} from 'rxjs/Observable';
import {
AuthService
} from './auth.service';
@Injectable()
export class AuthGuardService implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot, state: RouterStateSnapshot
): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
return this.checkAuthentication();
}
async checkAuthentication(): Promise < boolean > {
// Implement your authentication in authService
const isAuthenticate: boolean = await this.authService.isAuthenticated();
return isAuthenticate;
}
canActivateChild(
childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot
): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
return this.canActivate(childRoute, state);
}
}
Upvotes: 5
Reputation: 15050
In my case I needed to handle different behavior depends on the response status error. This is how it works for me with RxJS 6+:
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AngularFireAuth, private router: Router) {}
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | boolean {
return this.auth.pipe(
tap({
next: val => {
if (val) {
console.log(val, 'authenticated');
return of(true); // or if you want Observable replace true with of(true)
}
console.log(val, 'acces denied!');
return of(false); // or if you want Observable replace true with of(true)
},
error: error => {
let redirectRoute: string;
if (error.status === 401) {
redirectRoute = '/error/401';
this.router.navigateByUrl(redirectRoute);
} else if (error.status === 403) {
redirectRoute = '/error/403';
this.router.navigateByUrl(redirectRoute);
}
},
complete: () => console.log('completed!')
})
);
}
}
In some cases this might not work, at least the next
part of tap
operator. Remove it and add old good map
like below:
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | boolean {
return this.auth.pipe(
map((auth) => {
if (auth) {
console.log('authenticated');
return true;
}
console.log('not authenticated');
this.router.navigateByUrl('/login');
return false;
}),
tap({
error: error => {
let redirectRoute: string;
if (error.status === 401) {
redirectRoute = '/error/401';
this.router.navigateByUrl(redirectRoute);
} else if (error.status === 403) {
redirectRoute = '/error/403';
this.router.navigateByUrl(redirectRoute);
}
},
complete: () => console.log('completed!')
})
);
}
Upvotes: 3
Reputation: 1056
In the most recent version of AngularFire, the following code works (related to the best answer). Note the usage of "pipe" method.
import { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {AngularFireAuth} from '@angular/fire/auth';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
constructor(private afAuth: AngularFireAuth, private router: Router) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.afAuth.authState.pipe(
map(user => {
if(user) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
})
);
}
}
Upvotes: 8
Reputation: 2452
You can return true|false as a promise.
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {AuthService} from "../services/authorization.service";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private router: Router, private authService:AuthService) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return new Promise((resolve, reject) => {
this.authService.getAccessRights().then((response) => {
let result = <any>response;
let url = state.url.substr(1,state.url.length);
if(url == 'getDepartment'){
if(result.getDepartment){
resolve(true);
} else {
this.router.navigate(['login']);
resolve(false);
}
}
})
})
}
}
Upvotes: 22
Reputation: 4597
To expand on the most popular answer. The Auth API for AngularFire2 has changes somewhat. This is new signature to achieve a AngularFire2 AuthGuard:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AngularFireAuth } from 'angularfire2/auth';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(
private auth: AngularFireAuth,
private router : Router
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean>|boolean {
return this.auth.authState.map(User => {
return (User) ? true : false;
});
}
}
Note: This is a fairly naive test. You can console log the User instance to see if you would like to test against some more detailed aspect of the user. But should at least help protect routes against user's who are not logged in.
Upvotes: 6
Reputation: 579
You might use Observable
to handle the async logic part. Here is the code I test for example:
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { DetailService } from './detail.service';
@Injectable()
export class DetailGuard implements CanActivate {
constructor(
private detailService: DetailService
) {}
public canActivate(): boolean|Observable<boolean> {
if (this.detailService.tempData) {
return true;
} else {
console.log('loading...');
return new Observable<boolean>((observer) => {
setTimeout(() => {
console.log('done!');
this.detailService.tempData = [1, 2, 3];
observer.next(true);
observer.complete();
}, 1000 * 5);
});
}
}
}
Upvotes: 43
Reputation: 658027
canActivate
needs to return an Observable
that completes:
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AngularFireAuth, private router: Router) {}
canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
return this.auth.map((auth) => {
if (auth) {
console.log('authenticated');
return true;
}
console.log('not authenticated');
this.router.navigateByUrl('/login');
return false;
}).first(); // this might not be necessary - ensure `first` is imported if you use it
}
}
There is a return
missing and I use map()
instead of subscribe()
because subscribe()
returns a Subscription
not an Observable
Upvotes: 146