AngularFire2 - How to maintain logged in state after page refresh - javascript

I'm using AngularFire2 for an app and I've gotten the registration/login functionality working with Firebase, however, every time I refresh the page, the logged in state is reset and won't persist. I can't quite find functionality to do this, though I feel I'm missing something very small.
Can I use the AngularFireAuth to check on page load somewhere?
Here is my auth provider code:
import { Injectable } from '#angular/core';
import {Observable, Subject, BehaviorSubject} from "rxjs/Rx";
import {AngularFireAuth, FirebaseAuthState} from "angularfire2";
import {AuthInfo} from "./auth-info";
import {Router} from "#angular/router";
#Injectable()
export class AuthService {
static UNKNOWN_USER = new AuthInfo(null);
authInfo$: BehaviorSubject<AuthInfo> = new BehaviorSubject<AuthInfo>(AuthService.UNKNOWN_USER);
constructor(private auth: AngularFireAuth, private router:Router) {
}
login(email, password):Observable<FirebaseAuthState> {
return this.fromFirebaseAuthPromise(this.auth.login({email, password}));
}
signUp(email, password) {
return this.fromFirebaseAuthPromise(this.auth.createUser({email, password}));
}
fromFirebaseAuthPromise(promise):Observable<any> {
const subject = new Subject<any>();
promise
.then(res => {
const authInfo = new AuthInfo(this.auth.getAuth().uid);
this.authInfo$.next(authInfo);
subject.next(res);
subject.complete();
},
err => {
this.authInfo$.error(err);
subject.error(err);
subject.complete();
});
return subject.asObservable();
}
logout() {
this.auth.logout();
this.authInfo$.next(AuthService.UNKNOWN_USER);
this.router.navigate(['/login']);
}
}
Thankyou in advance!

AngularFireAuth is an observable and emits FirebaseAuthState values. If a user is signed in and the page is refreshed, AngularFireAuth will emit an authenticated FirebaseAuthState; otherwise, it will emit null.
So something like this should come close to solving your problem:
constructor(private auth: AngularFireAuth, private router:Router) {
auth.subscribe((authState) => {
if (authState) {
const authInfo = new AuthInfo(authState.uid);
this.authInfo$.next(authInfo);
}
});
}

Related

Angular rxjs Subject : subscribing to a subject after emitting values

I read an article in which it states that, if you subscribe to subject when the event has been already emitted You would lose the emitted value. his demonstrated example is below.
let subject: Subject<string> = new Subject();
subject.next('test');
subject.subscribe((event) => {
console.log(event);
});
But ,I have a similar approach implemented, In which first subject will emit the value , then subscribes the subject. Value is there. not lost. code is below.
In the code, we are saving authentication information in a subject. we use that in header component to show logout button and links. Question is why my code is working even though article says it won't?
import { HttpClient, HttpErrorResponse } from "#angular/common/http";
import { Injectable } from "#angular/core";
import { Subject} from "rxjs";
import { User } from "./user.model";
#Injectable({providedIn : "root"})
export class AuthService{
constructor(private httpClient:HttpClient){}
user = new Subject<User>(); //emit new user when login or logout or token expired
login(email:string,password:string){
return this.httpClient
.post<SignUpResponse>('https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyBCK9B2RcL0D4Bn8YaFCxE9rBXejTLNYQY',
{
email:email,
password:password,
returnSecureToken:true
})
.pipe(tap( response => this.handleAuthentication(response.email,response.idToken,response.idToken,response.expiresIn)))
}
private handleAuthentication(email:string,userId:string,token:string,expiresIn:string){
const expirationDate = new Date(new Date().getTime() + +expiresIn * 1000)
const user = new User(email,userId,token,expirationDate);
this.user.next(user);
}
}
Header.component.ts
import { Component,Output, EventEmitter, OnInit, OnDestroy } from "#angular/core";
import { Subscription } from "rxjs";
import { AuthService } from "../auth/auth/auth.service";
import { DataStorageService } from "../shared/data-storage.service";
#Component({
selector : 'app-header',
templateUrl:'./header.component.html'
})
export class HeaderComponent implements OnInit,OnDestroy{
collapsed = true;
private subscription : Subscription;
isAuthenticated : boolean = false;
constructor(private dataStorageService:DataStorageService,
private authService:AuthService){
}
ngOnInit(){
this.subscription = this.authService.user.subscribe( user => {
this.isAuthenticated = !!user;
console.log("User Object"); console.log(user);
console.log("IsAuthenticated value"); console.log(!!user);
})
}
ngOnDestroy(){
this.subscription.unsubscribe();
}
}
The only reason your example subscribe is running is because you load the Header component before you start the login process/method. ngOnInit of component is called before login method in service.
Your code run like this not the way you have described in the quesiton:
user = new Subject<User>();
subject.next('test');
// then this runs in component ngOnInit
this.authService.user.subscribe((event) => {
console.log(event);
});
// when you try to login from inside the component then it runs the login method with next.
login(email:string,password:string){.....
this.user.next(user); // now this is triggered hence you don't loose the user response.

Firebase angularfire2 - How to return data from the current user only?

I'm trying to figure out how to limit a collection to returning just the user's data, not everyone's data.
In the example I'm working from the FirebaseService only shows CRUD examples where the data that's returned is everything.
import { Injectable } from "#angular/core";
import { Platform } from 'ionic-angular';
import 'rxjs/add/operator/toPromise';
import { AngularFirestore } from 'angularfire2/firestore';
import * as firebase from 'firebase/app';
import 'firebase/storage';
#Injectable()
export class FirebaseService {
constructor(
public afs: AngularFirestore,
public platform: Platform
){}
getEvents(){
return new Promise<any>((resolve, reject) => {
this.afs.collection('/events').snapshotChanges() // add +auth.uid ... or something?
.subscribe(snapshots => {
resolve(snapshots)
})
})
}
...
In order to only get the user's events back, I think I need to add:
import { AngularFireAuth } from 'angularfire2/auth';
... and, do something from there. But, I'm at a loss. Any help would be greatly appreciated. Thanks in advance!
You can limit this by adding rules. For example, you are using /users/ node to store user information. You can restrict only for the logged in user matching with userId can access /users/
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId
}
match /users/{userId} {
allow get: if isSignedIn()
&& isOwner(userId);
....
}
}
To get User Id
constructor(
private afAuth: AngularFireAuth )
// then
ngOnInit() {
this.afAuth.authState;
this.afAuth.authState.subscribe(
user => {
this.userInfo = user; <-- You can store user Id information to user variable
},
err => {
console.log(err);
}
}
you can use this.userInfo.uid to make further calls.

Getting User Data by using Guards (Roles, JWT)

The documentation is kinda thin here so I ran into a problem. I try to use Guards to secure Controller or it's Actions, so I gonna ask for the role of authenticated requests (by JWT). In my auth.guard.ts I ask for "request.user" but it's empty, so I can't check the users role. I don't know how to define "request.user". Here is my auth module and it's imports.
auth.controller.ts
import { Controller, Get, UseGuards } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
import { AuthService } from './auth.service';
import { RolesGuard } from './auth.guard';
#Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
#Get('token')
async createToken(): Promise<any> {
return await this.authService.signIn();
}
#Get('data')
#UseGuards(RolesGuard)
findAll() {
return { message: 'authed!' };
}
}
roles.guard.ts
Here user.request is empty, because I never define it. The documentation doesn't show how or where.
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user; // it's undefined
const hasRole = () =>
user.roles.some(role => !!roles.find(item => item === role));
return user && user.roles && hasRole();
}
}
auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { HttpStrategy } from './http.strategy';
import { UserModule } from './../user/user.module';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '#nestjs/passport';
import { JwtModule } from '#nestjs/jwt';
#Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secretOrPrivateKey: 'secretKey',
signOptions: {
expiresIn: 3600,
},
}),
UserModule,
],
providers: [AuthService, HttpStrategy],
controllers: [AuthController],
})
export class AuthModule {}
auth.service.ts
import { Injectable } from '#nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '#nestjs/jwt';
#Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async signIn(): Promise<object> {
// In the real-world app you shouldn't expose this method publicly
// instead, return a token once you verify user credentials
const user: any = { email: 'user#email.com' };
const token: string = this.jwtService.sign(user);
return { token };
}
async validateUser(payload: any): Promise<any> {
// Validate if token passed along with HTTP request
// is associated with any registered account in the database
return await this.userService.findOneByEmail(payload.email);
}
}
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: any) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Documentation: https://docs.nestjs.com/guards
Thanks for any help.
Additionally to your RolesGuard you need to use an AuthGuard.
Standard
You can use the standard AuthGuard implementation which attaches the user object to the request. It throws a 401 error, when the user is unauthenticated.
#UseGuards(AuthGuard('jwt'))
Extension
If you need to write your own guard because you need different behavior, extend the original AuthGuard and override the methods you need to change (handleRequest in the example):
#Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info: Error) {
// don't throw 401 error when unauthenticated
return user;
}
}
Why do this?
If you look at the source code of the AuthGuard you can see that it attaches the user to the request as a callback to the passport method. If you don't want to use/extend the AuthGuard, you will have to implement/copy the relevant parts.
const user = await passportFn(
type || this.options.defaultStrategy,
options,
// This is the callback passed to passport. handleRequest returns the user.
(err, info, user) => this.handleRequest(err, info, user)
);
// Then the user object is attached to the request
// under the default property 'user' which you can change by configuration.
request[options.property || defaultOptions.property] = user;
You can attach multiple guards together (#UseGuards(AuthGuard('jwt'), RolesGuard)) to pass the context between them. Then you will have access 'req.user' object inside 'RolesGuard'.
After I got the selected answer working (thank you), I found this option as well that you can add to the constructor that essentially does the same thing.
http://www.passportjs.org/docs/authorize/
Association in Verify Callback
One downside to the approach described above is that it requires two
instances of the same strategy and supporting routes.
To avoid this, set the strategy's passReqToCallback option to true.
With this option enabled, req will be passed as the first argument to
the verify callback.
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private authService: AuthService) {
super({
passReqToCallback: true
})
}
// rest of the strategy (validate)
}
Does it work if you use req.authInfo?
As long as you don't provide a custom callback to passport.authenticate method, the user data should be attached to the request object like this.
req.authInfo should be the object you returned in your validate method

Refresh id_token ADAL: Angular 4

I'm using Angular 4 with ADAL to authenticate users in my web application, using ng2-adal library which is a wrapper for adal.js.
The problem I'm facing is the following:
So the token expires after a time limit and I have a canActivate route guard that checks if the user is authenticated. If not, it navigates the users to the login page. This is how my route guard is looking:
import { Injectable } from '#angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { AdalService } from 'ng2-adal/dist/core';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(private router: Router, private adalService: AdalService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.adalService.userInfo.isAuthenticated) {
return true;
} else {
this.router.navigate(['/user-login']);
return false;
}
}
}
so whenever the token expires, the user is navigated to the login page, which is annoying for the users. Is there a way to renew the token whenever it expires?
I figured it out. This is how I added it:
import { Injectable } from '#angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { AdalService } from 'ng2-adal/dist/core';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(private router: Router, private adalService: AdalService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.adalService.userInfo.isAuthenticated) {
return true;
} else {
this.adalService.acquireToken(this.adalService.config.clientId).toPromise().then((data) => {
console.log('Generating a new authentication token.');
return true;
},
(error) => {
console.log('No user logged in.');
this.router.navigate(['/user-login']);
return false;
}
}
}
}
I had the same issue and my fix worked.
In app.component.ts, add this code to ngOnit().
this.adalService.handleWindowCallback();
this.adalService.acquireToken(this.adalService.config.loginResource).subscribe(token => {
this.adalService.userInfo.token = token;
if (this.adalService.userInfo.authenticated === false) {
this.adalService.userInfo.authenticated = true;
this.adalService.userInfo.error = '';
}
}, error => {
this.adalService.userInfo.authenticated = false;
this.adalService.userInfo.error = error;
this.adalService.login();
});
When token expires, app component gets called, and acquire token refreshes the token silently. But the this.adalService.userInfo.authenticated is still false leading to redirection or again calling login method. So manually setting it to true fixes the redirection error. this.adalService.config.loginResource this is automactically set by adal-angular itself with the resource that we need token for.
Also add expireOffsetSeconds: 320, to adal configuration data settings along with
tenant: configData.adalConfig.tenant,
clientId: configData.adalConfig.clientId,
redirectUri: window.location.origin,
expireoffsetseconds invalidates the token based on the time that we specify before its actual expiry.

Auth0 service unable to find container using Angular 2

I'm creating an Angular 2 SPA for learning purposes and integrating Auth0 for handeling the authentication. I have an auth.service.ts that is going to be called from difference places in my application, for example in the top-navbar to logout and on the auth-page to handle logins and registrations.
When trying to place the Auth0 container in a div by setting the container option I get the following error: Can't find element with id auth-container
How can I let the auth.service know how/where to look for the auth-container div? Placing all the logic inside the auth.component.ts is assumably not an option because the auth.service will be used for other functionality in other places where the lock variable is also used.
auth.service.ts
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
declare var Auth0Lock: any;
var options = { container: 'auth-container' };
#Injectable()
export class Auth {
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, options);
constructor() {
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
});
}
public authenticated() {
return tokenNotExpired();
};
public logout() {
localStorage.removeItem('id_token');
};
}
auth.component.ts
constructor(public auth: Auth) {
auth.lock.show();
}
auth.component.html
<div id="auth-container"></div>
Well they did not make your life easy but by mistake I made it work.
Try this:
auth.component.ts
ngOnInit() {
this.auth.login()
}
Delete this from your constructor
auth.lock.show();
The auth.service is not a container, it's a service that provides a popup when the login function is invoked.
So, to reuse it wherever you like, you need to inject the auth service into the component where you want to call the auth service from. Then, you just call the method. For example, here is the html for my Start component. You can see that the click event for the signin button is bound to the "submitLogin()" method of the component (the Start component):
<div class="splash-back" *ngIf="!authService.authenticated()">
<div id="splash">
<div id="logo"><span class="silver">GCO</span>TeamKeeper
<p class="silver tagline">The other teams could make trouble for us if they win.</p>
<p class="silver attribution">~ Yogi Berra</p></div>
<div class="call">
<br>
<button class="btn-sign-in" (click) = "submitLogin()">Sign up or Log in</button>
</div>
<!--<mtm-authentication></mtm-authentication>-->
</div>
</div>
And here is the start component code (note the injection of the authentication service in the constructor):
#Component({
selector: 'tk-start',
templateUrl: './start.component.html',
styleUrls: ['./start.component.css']
})
export class StartComponent implements OnInit {
constructor(private authService: UserAuthenticationService) { }
ngOnInit() {
}
submitLogin(){
this.authService.login();
}
}
And to make this example complete, here is my auth service code:
import {Injectable} from "#angular/core";
import { tkConfig } from './user-authentication.config';
import {Router} from "#angular/router";
import {tokenNotExpired} from "angular2-jwt";
let Auth0Lock = require('auth0-lock').default;
#Injectable()
export class UserAuthenticationService {
// Configure Auth0
userProfile: Object;
lock = new Auth0Lock (tkConfig.clientID, tkConfig.domain, {
avatar: null,
theme: {
primaryColor: "#69BE28",
foregroundColor: "#000000"
},
languageDictionary: {
title: "GCO TeamKeeper"
}
}
);
constructor(
private router: Router) {
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
this.router.navigate(['/organization']);
});
})
}
public login() {
// Call the show method to display the widget.
this.lock.show();
};
public authenticated() {
// Check if there's an unexpired JWT
// It searches for an item in localStorage with key == 'id_token'
return tokenNotExpired();
};
public logout() {
// Remove token from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
this.router.navigate(['/start']);
};
}

Categories

Resources