I'm trying to implement an angular guard to check if the user was signed in recently so they can change password, update email etc.
Using either angular fire 2 or angular I have a handle to the user (firebase.User). But how do I check if they qualify for "recently logged in" to perform sensitive operations?
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '#angular/router';
import { Observable } from 'rxjs/Rx';
import { Injectable, OnInit } from '#angular/core';
import { AuthService } from './auth.service';
import * as firebase from 'firebase/app';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {
}
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
return this.authService.user$.map((user) => {
if (user) {
// how to validate that this user is a recent login?
return true;
} else {
console.log('not authenticated');
this.router.navigateByUrl('/login');
return false;
}
}).first();
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.canActivate(route, state);
}
}
Why not just always re-authenticate before any sensitive information. It is common practice and Firebase provides APIs for re-authentication. You can check the auth_time in the Firebase ID token for the time of the last sign-in but Firebase Auth doesn't document the criteria for the recently logged in requirement and likely won't as they reserve the right to change it for security reasons. You are better off just requiring reauthentication, or you can try to updatePassword/updateEmail but if you get that specific error, reauthenticate and then try again.
Related
I am trying to check firebase emailVerified without logging out the user (Angular 6 web app), but auth().currentUser.reload() returns "undefined"
Tried:
import { auth } from 'firebase/app';
auth().currentUser.reload().then((u) => {
console.log(u);
});
Tried:
import { AngularFireAuth } from '#angular/fire/auth';
this.afAuth.auth.currentUser.reload().then((u) => {
console.log(u);
});
These related issues DO NOT HELP.
Update the email verification status without reloading page
Firebase: Observe email verification status in real time
Auth.auth().currentUser?.reload() doesn't refresh currentUser.isEmailVerified
The firebase core doesn't include the auth module. You need to add the auth module:
import firebase from 'firebase/app';
import 'firebase/auth';
If no user is currently authenticated, currentUser will be null and will not have a reload() function. Trying to call it would give you and error.
The reload() function returns a promise so you can use .then, but the promise doesn't return the currentUser object. To view the currentUser after the reload, just refer to the currentUser...
if (firebase.auth().currentUser) {
firebase.auth().currentUser.reload().then(() => {
console.log(JSON.stringify(firebase.auth().currentUser));
});
} else {
console.log('No authenticated user');
}
I am developing a school transport application with Ionic 4 and Firebase. Firebase has the authentication functionality, and there is a register of different users in the application (such as driver, student, company / adm), with the use of ID references (where the id generated by the authentication is referenced according to the user) . With that, I got into a problem that I'm breaking my head. How do I redirect each type of user to a respective page, according to their functionality, after logging in?
You can redirect using guard and local Storage
import { Injectable } from '#angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '#angular/router';
import { Observable } from 'rxjs';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private router: Router) {
}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const token = localStorage.getItem('token');
const role = localStorage.getItem('id');
if (role === 'driver' && !!token) {
this.router.navigate(['/driver']);
return true
} else if (role === 'student' && !!token) {
this.router.navigate(['/student']);
return true
} else if (role === 'student' && !!token) {
this.router.navigate(['/student']);
return true
} else if (role === 'admin' && !!token) {
this.router.navigate(['/admin']);
return true
} else {
this.router.navigate(['/login']);
return false
}
return false
}
}
Following Google's official Angular 4.3.2 doc here, I was able to do a simple get request from a local json file. I wanted to practice hitting a real endpoint from JSON placeholder site, but I'm having trouble figuring out what to put in the .subscribe() operator. I made an IUser interface to capture the fields of the payload, but the line with .subscribe(data => {this.users = data}) throws the error Type 'Object' is not assignable to type 'IUser[]'. What's the proper way to handle this? Seems pretty basic but I'm a noob.
My code is below:
import { Component, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { IUsers } from './users';
#Component({
selector: 'pm-http',
templateUrl: './http.component.html',
styleUrls: ['./http.component.css']
})
export class HttpComponent implements OnInit {
productUrl = 'https://jsonplaceholder.typicode.com/users';
users: IUsers[];
constructor(private _http: HttpClient) { }
ngOnInit(): void {
this._http.get(this.productUrl).subscribe(data => {this.users = data});
}
}
You actually have a few options here, but use generics to cast it to the type you're expecting.
// Notice the Generic of IUsers[] casting the Type for resulting "data"
this.http.get<IUsers[]>(this.productUrl).subscribe(data => ...
// or in the subscribe
.subscribe((data: IUsers[]) => ...
Also I'd recommend using async pipes in your template that auto subscribe / unsubscribe, especially if you don't need any fancy logic, and you're just mapping the value.
users: Observable<IUsers[]>; // different type now
this.users = this.http.get<IUsers[]>(this.productUrl);
// template:
*ngFor="let user of users | async"
I'm on the Angular doc team and one open todo item is to change these docs to show the "best practice" way to access Http ... which is through a service.
Here is an example:
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import { IProduct } from './product';
#Injectable()
export class ProductService {
private _productUrl = './api/products/products.json';
constructor(private _http: HttpClient) { }
getProducts(): Observable<IProduct[]> {
return this._http.get<IProduct[]>(this._productUrl)
.do(data => console.log('All: ' + JSON.stringify(data)))
.catch(this.handleError);
}
private handleError(err: HttpErrorResponse) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
let errorMessage = '';
if (err.error instanceof Error) {
// A client-side or network error occurred. Handle it accordingly.
errorMessage = `An error occurred: ${err.error.message}`;
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
errorMessage = `Server returned code: ${err.status}, error message is: ${err.message}`;
}
console.error(errorMessage);
return Observable.throw(errorMessage);
}
}
The component would then look like this:
ngOnInit(): void {
this._productService.getProducts()
.subscribe(products => this.products = products,
error => this.errorMessage = <any>error);
}
My users can like a list of heroes, so I have this structure in my firebase rules/datas:
"user_flags": {
"$uid": {
".write": "auth.uid == $uid",
".read": "auth.uid == $uid",
"liked": {
"$heroIdx": {
".validate": "newData.isString()"
}
}
}
}
In my code I want to subscribe to the "liked heroes" ref, so that's what I do:
import { Injectable } from '#angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import { AngularFireAuth } from 'angularfire2/auth';
#Injectable()
export class UserFlagsService {
likedHeroes$: Observable<string[]>;
constructor(
private afAuth: AngularFireAuth,
private db: AngularFireDatabase
) {
this.likedHeroes$ = afAuth.authState.flatMap(user => {
return user && user.uid
? this.db.list(`user_flags/${user.uid}/liked`)
.map(heroes => heroes.map(hero => <string>hero.$value))
: Observable.of([])
});
}
}
Everything works fine until the user signs out... Even with the check on user and user.uid the query user_flags/MY_ID_HERE/liked seems to be triggered and I get a "permission denied".
I tried to use subscribe and watch for signout to unsubscribe but it didn't work either... The query was still triggered and failed with "permission denied"
How should I handle this ? I want my service to return a reliable observable so I can subscribe to it in my components.
Thanks a lot for your help
I am assuming that you want to ensure that the data you are rendering in the view disappears on sign out?
If this is the case, I would suggest using the switchMap operator from RXJS and the following pattern:
this.userProvider = this.afAuth.authState;
this.likedHeroes$ = this.userProvider.switchMap((auth) => {
if(auth){
return this.af.list('user_flags/' + auth.uid + '/liked');
}
});
You need to add the following to import the switchMap operator:
import 'rxjs/add/operator/switchMap';
Hit me with some comments if you want me to fill this out some more or if my assumption about what you're trying to achieve is incorrect. I've been trying to figure out the best way to do this stuff as well.
You should also check out this video from the Angular Firebase YouTube, it might help you with some of the issues in your question.
I managed to make it work by creating a BehaviorSubject and unsubscribing the event before the "signout" is triggered.
Here is my provider:
import { Injectable } from '#angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
import { UserProvider } from '../../providers/user/user';
#Injectable()
export class UserFlagsProvider {
likedHeroes$: Observable<string[]>;
subHeroesLiked: Subscription;
constructor(
protected userProvider: UserProvider,
protected db: AngularFireDatabase
) {
const heroesLikedSubject: BehaviorSubject<string[]> = new BehaviorSubject([]);
this.likedHeroes$ = heroesLikedSubject.asObservable();
this.userProvider.user$.subscribe(user => {
if (user) {
this.subHeroesLiked = this.db.list(`user_flags/${user.uid}/liked`).subscribe(heroesSlugs => {
heroesLikedSubject.next(heroesSlugs.map(hero => <string>hero.$key));
});
}
});
this.userProvider.signingOut$.subscribe(() => {
this.subHeroesLiked.unsubscribe();
});
}
}
And my userProvider
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { AngularFireAuth } from 'angularfire2/auth';
#Injectable()
export class UserProvider {
user$: Observable<firebase.User>;
user: firebase.User;
signingOut$: Subject<any> = new Subject();
constructor(private afAuth: AngularFireAuth) {
this.user$ = afAuth.authState;
this.user$.subscribe(user => this.user = user);
}
// [...]
signout() {
this.signingOut$.next();
return this.afAuth.auth.signOut();
}
}
Hope it helps someone.
FYI, this doesn't work (for now) with angularfire2-offline for an unknown reason.
If someone sees a better way to do it I'm interested
I have an ionic2 app and am using Firebase and angularFire2. I'd like to get the current authentication state and current auth object/user from firebase using angularFire2.
Here's what's working so far - I can authenticate the user and subscribe to the FirebaseAuthState to get the facebook user object.
constructor(platform: Platform, private auth: FirebaseAuth) {
auth.subscribe((user: FirebaseAuthState) => {
if (user) {
// I could store user in localstorage, but I'd like to see an All Firebase solution
this.rootPage = TabsPage;
} else {
this.rootPage = LoginPage;
}
});
Now I can just set localstorage here and cache my user object to remember auth state. However, I am curious to see how I can use Firebase only without me implementing my own custom local storage key. I see that Firebase stores a localStorage key of it's own so knows that its logged in.
How can I get the auth object from code? Additionally, I tried the listed example in the AngularFire2 documentation to render the auth state in the template - but that gives me an error.
import {FirebaseAuth} from 'angularfire2';
#Component({
selector: 'auth-status',
template: `
<div *ng-if="auth | async">You are logged in</div>
<div *ng-if="!(auth | async)">Please log in</div>
`
})
class App {
constructor (#Inject(FirebaseAuth) public auth: FirebaseAuth) {}
}
Import: import { AngularFireAuth } from 'angularfire2/auth';
Inject: constructor(public afAuth: AngularFireAuth) { }
Check:
this.afAuth.authState.subscribe(res => {
if (res && res.uid) {
console.log('user is logged in');
} else {
console.log('user not logged in');
}
});
now in order to get the user info, you have to subscribe for getting the auth information. Ex
constructor(public af: AngularFire) {
this.af.auth.subscribe(auth => console.log(auth));// user info is inside auth object
}
auth object will be null if auth state does not exist
current authentication state is available from the injected FirebaseAuth. You can get the actual auth data auth.getAuth()
See: https://github.com/angular/angularfire2/blob/master/src/providers/auth.ts#L99
You can simply import AngularFireAuth and do this:
this.angularFireAuth.authState.subscribe(userResponse => {
if (userResponse) {
console.log('here is your user data');
localStorage.setItem('user', JSON.stringify(userResponse));
console.log(userResponse);
} else {
localStorage.setItem('user', null);
}
});
Here i am also using localStorage to save all data of my current user active on my app.