Is there a suggested way to use the canActivate property on a route while using firebase authentication?
I am using the latest alpha router and trying to implement the authGuard as provided in the docs but I run into an issue with the asynchronous auth observable that returns after the authGuard has completed (this appears to only happen when you refresh the page after having already logged in). The results is that no component shows because the subscription (see below) doesn't complete until after loggedIn returns false. Should I be trying to find a way to stop execution to wait for a response from the observable or is there another solution I should try?
Here is the example of my authGuard:
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AngularFire, AuthProviders, AuthMethods } from 'angularfire2';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private af: AngularFire, private router: Router) {}
canActivate(): boolean {
var loggedIn = false;
console.log("Authenticating");
this.af.auth.subscribe(auth => {
if (auth) {
loggedIn = true;
} else {
this.router.navigate(['/login']);
}
})
return loggedIn;
}
}
I should note that I am using email/password authentication. I noticed there is another issue created about persisting oauth credentials, this issue may run in the same vein as that one.
I could go back to using logic in the template to show components but I prefer using the guard.
Rather than subscribing directly to this.af.auth, you can try returning it instead, since canActivate() can return a Observable<boolean> :
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { FirebaseAuth, FirebaseAuthState } from 'angularfire2';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: FirebaseAuth, private router: Router) {}
canActivate(): Observable<boolean> {
return this.auth
.take(1)
.map((authState: FirebaseAuthState) => !!authState)
.do(authenticated => {
if (!authenticated) this.router.navigate(['/login']);
});
}
}
thank you @r-park! that got me over the hump (with slight alterations). I suppose I should spend time on rxjs to have a better foundation.
Great solution @r-park! @peterwiebe We are looking to integrate with the router in the future so hang tight, this will get easier!
@r-park Do you have any idea why your example didn't work for me. I'm using the exact code, but for some reasons, when authenticated is true, my HomeComponent is not executed at all, but I can see that the code in AuthGuard is run from the debug logging.
Here is my route setup.
const routes: RouterConfig = [
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LogInComponent }
];
@davideast I'm not sure how that integration would look like, but it would be really nice to have an easy solution for a global "wait-for-auth" thing. It's a PITA at the moment to do stuff on page loads where the uid is needed since we have to subscribe everywhere to af.auth and do the stuff we want in that subscription.
A really simple example: user opens randomurl/issue/123 and wants to increment the view counter for issue 123. Since the auth object is async we can't just simply .set the counter at views/issueID/uid to +1 because auth.uid doesn't exist at that point, so first we have to subscribe to af.auth. I really hope there will be an easier way to do this. (If there already is one please tell me!)
Anyone else having this problem should take a look at this solution if the one above is not exactly working for you. https://github.com/angular/angular/issues/9613#issuecomment-228748731
Almost the same as @r-park but without the .do()
Hello,
The code piece @r-park provided is very useful to me. I didn't get one thing though. Why did you use take(1) there? I was using .asObservable()(and it wasn't working). It looks like I didn't get usage of rxjs there. Can you explain if you have some some? :) You can also post it as answer here if interested.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: FirebaseAuth, private router: Router) {}
canActivate(): Observable<boolean> {
return this.auth
.take(1) // <-- WHY? I thought it would be .asObservable() instead
.map((authState: FirebaseAuthState) => !!authState)
.do(authenticated => {
if (!authenticated) this.router.navigate(['/login']);
});
}
}
@ubenzer I think the rationale is that, logically, the router must make a single decision in response to a navigation event. In the case of an async value, the guard must wait for a single value and then make its decision.
Consider a user clicking a link to navigate somewhere: The guard either says "yes, this is ok to go forward" or "no, cancel the navigation". If a new value appears for the observable after it has made its decision, the router can't re-evaluate the navigation. It has already responded to the user. Therefore, it doesn't really make sense to have the guard try to respond to more than one value from the Observable.
So, the purpose of the .take(1) operator is to say "give me the first value out of this Observable. I don't care about the rest." That way, each invocation of the guard will get a single value to use to make their decision and then move on.
@awiseman Thanks for the explanation. The logic you tell makes sense, router supposed to care about only the first response.
On the other hand, I still didn't get why it doesn't work because I am pretty sure that (1) it fires only once (2) even it had fired more than once, router supposed to got its answer and disregard following events or at least give an error.
Great solution @r-park ! But I had to convert this.auth to an Observable to get the code works
canActivate(): Observable<boolean> {
return Observable.from(this.auth)
.take(1)
.map(state => !!state)
.do(authenticated => {
if (!authenticated) this.router.navigate([ '/login' ]);
})
}
Any solution to avoid that user opens the login page if he is already authenticated?
@fabryx92 you have to create another auth guard, that navigates away from login/register routes, e.g.:
canActivate(): Observable<boolean> {
return this.af.auth.map(authState => {
if (authState) this.router.navigate(['/dashboard']);
return !authState;
}).take(1);
}
How can I check if the user is logged in AND has a verified email in the canActivate()?
@mickverm if you get the auth from the authState it has an emailVerified property. (The auth is pretty much synonymous with the user.)
From @ArtworkAD's example immediately above: https://github.com/angular/angularfire2/issues/282#issuecomment-281040863
```typescript
canActivate(): Observable
return this.af.auth.map(authState => {
if (authState.auth.emailVerified) this.router.navigate(['/dashboard']);
return !authState;
}).take(1);
}
Hell everyone, i start 1 week ago my journey in angular and typescript...
you think that there is a way for check also a user role inside authguard?
i want to implement something like this in my route table :
[...]
import { AuthGuard } from './auth.service';
[...]
export const router: Routes = [
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'signup', component: SignupComponent },
{ path: 'login-email', component: EmailComponent },
{ path: 'members', component: MembersComponent, canActivate: [AuthGuard] },
{ path: 'dashboard', component: DashComponent, canActivate: [AuthGuard('admin')] }, // <-- pass user role in authguard
[...]
and this is my auth.service :
[...]
canActivate(role: any): Observable<boolean> {
return Observable.from(this.auth)
.take(1)
.map(state => !!state)
.do(authenticated => {
if ( !authenticated ) { this.router.navigate([ '/login' ]) } else { check role in Firebase Database }
})
}
you think is possible?
@dewolft this is probably what you're searching for http://stackoverflow.com/q/38402776/401025
@ArtworkAD : i've read about that but i didn't understood where it pick up user role from firebase...
@dewolft Well, it is not picked from the user table that is used for authentication, because it cannot contain any custom attributes like role. You have to create another table (in realtime database) that associates roles with userId's.
thanks Artwork, i've try in this way... but it doesn't work... :( any tips?
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
this.auth.take(1)
.map(state => !!state)
.do(authenticated => {
if ( !authenticated ) {
console.log('non autorizzato' ) ;
this.router.navigate([ '/login' ]);
return false;
} else {
this.af.auth.subscribe(auth => {
if (auth) {
this.UID = auth.auth.uid ;
}
});
console.log('ruolo :' + route.data["roles"] ) ;
console.log('UID :' + this.UID ) ;
this.af.database.object('User/' + this.UID ).take(1).map(utente => {
let roles = route.data["roles"] as Array<string>;
return (roles == null || roles.indexOf(utente.ruolo) != -1); }) ;
}
}) ;
}
Hello guys,
I'm trying to return on canActivate() an Observable<Boolean> using the returned Observable<firebase.User> by AngularFireAuth.authState; however, Observable<firebase.User> cannot be returned because of the Type difference. Neither I can call .map(), take(), do() on it. The only methods I can call are lift(), forEach() and subscribe().
All the above successful examples on this post are probably out of date to the current progress of angularfire2.
So how do I return an Observable<Boolean> from an Observable<firebase.User> ?
Many thanks!
@jorgeandresserrano
Here's an auth.service.ts
import { Injectable } from '@angular/core';
import { AngularFireAuth } from 'angularfire2/auth';
import { AngularFireDatabase } from 'angularfire2/database';
import * as firebase from 'firebase/app';
@Injectable()
export class AuthService {
private _user: firebase.User;
constructor(public afAuth: AngularFireAuth, private db: AngularFireDatabase) {
afAuth.authState.subscribe(user => this.user = user);
}
get user(): firebase.User {
return this._user;
}
set user(value: firebase.User) {
this._user = value;
}
get authenticated(): boolean {
return this._user !== null;
}
get id(): string {
return this.authenticated ? this.user.uid : '';
}
signInWithGoogle(): firebase.Promise<any> {
return this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider())
.then(response => {
this.db.object(`/users/${response.user.uid}`)
.subscribe(user => {
if (!user.$exists()) {
let {displayName, email, emailVerified, photoURL, uid} = response.user;
this.db.object(`/users/${response.user.uid}`).set({
displayName,
email,
emailVerified,
photoURL,
uid
})
}
});
})
.catch(err => console.log('ERRROR @ AuthService#signIn() :', err));
}
signOut(): void {
this.afAuth.auth.signOut();
}
}
and an auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
import { AuthService } from "./auth.service";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.auth.afAuth.authState
.take(1)
.map(authState => !!authState)
.do(authenticated => {
if (!authenticated) {
this.router.navigate(['admin', 'sign-in']);
}
});
}
}
These are adapted from an AuthGuard and AuthService that @erikhaddad created for a few of his conference talk projects.
@markgoho thanks Mark for the prompt reply. I'm using the exact same code and I still get Property 'take' does not exist on type 'Observable<User>'
Have you imported the take operator from rxjs?
That is it!
No. I'm not importing operators from RxJS https://angular.io/docs/ts/latest/tutorial/toh-pt6.html#!#import-rxjs-operators
Should I instead better import Observable from 'rxjs/Rx' ? or one by one?
Always import _only_ what you need. You can see how I imported things in my auth.guard.ts file
@markgoho thank you very much! You're awesome! You just put to an end to my last 4 hours of madness.
@markgoho I'm trying to run your example code of Auth guard and service, but I get some errors about Injection can you share an example with the RouteModule?
if you're using the latest version of AngularFire2, check out my most recent demo app for a working AuthGuard implementation https://github.com/erikhaddad/firechat/blob/master/src/app/auth/auth.guard.ts
@erikhaddad thanks for posting the example using auth.guard. When I replaced signInWithPopup with signInWithRedirect, I get to authenticate on the providers page, but the redirect doesn't hit the canActivate function in the auth.guard.ts; so although I have an authState$ object, I'm not directed after authentication.
Do you have an example with signInWithRedirect?
@craigaps I do not have an example showing signInWithRedirect at this time
Continuing on the example mentioned and presented by example via @erikhaddad, I just want to point out for future people quickly referencing this post, if using rxjs>5.5, the way these operators are imported and handled are now different.
https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md
import { map, take, tap } from 'rxjs/operators';
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
return this.auth.afAuth.authState.pipe(
take(1),
map((authState) => !!authState),
tap(authenticated => {
if (!authenticated) this.router.navigate(['/login'])
})
)
}
@mattychen
Thanks. Your above code snippet works fine. But I didn't understand this line map((authState) => !!authState). Can you explain me step by step how is this actually working.
@vinayitapu, it returns true if authState is not null. returning authState by itself will not return a boolean, so we map it to this new function we've created inside map(). The resultant boolean gets passed into our next function tap(), in which authenticated is a parameter of type boolean.
var x = null;
console.log(!!x)
// returns false
console.log(!x)
//returns true
I dont yet know how to convert angularfire2 to @angular/fire... How do i do the following when importing from @angular/fire
```import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { FirebaseAuth, FirebaseAuthState } from 'angularfire2';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: FirebaseAuth, private router: Router) {}
canActivate(): Observable
return this.auth
.take(1)
.map((authState: FirebaseAuthState) => !!authState)
.do(authenticated => {
if (!authenticated) this.router.navigate(['/login']);
});
}
}```
I dont yet know how to convert
angularfire2to@angular/fire... How do i do the following when importing from@angular/fireimport 'rxjs/add/operator/map'; import 'rxjs/add/operator/take'; import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { FirebaseAuth, FirebaseAuthState } from 'angularfire2'; @Injectable() export class AuthGuard implements CanActivate { constructor(private auth: FirebaseAuth, private router: Router) {} canActivate(): Observable<boolean> { return this.auth .take(1) .map((authState: FirebaseAuthState) => !!authState) .do(authenticated => { if (!authenticated) this.router.navigate(['/login']); }); } }```
Hope this can help you:
https://github.com/angular/angularfire/blob/master/docs/auth/router-guards.md
Most helpful comment
Rather than subscribing directly to
this.af.auth, you can try returning it instead, sincecanActivate()can return aObservable<boolean>: