AngularFirebaseAuth : Calling server api just after firebase auth? - javascript

My auth is based on 2 things :
firebase auth (email/password)
call on a server API to retrieve full customer entity from BDD and from firebaseID (user must exists)
So a user will be "authenticated" if these two conditions are met.
I also have authGuards based on a isAuthenticated() returning an Observable (because on a page refresh, guard must wait for the auth to be finished before redirecting the user anywhere).
Problem : I can't find a way to make that work with all the async and rxjs mess/hell .. Currently it's working but each time isAuthenticated is called, the serverAPI auth is called every time...
How can I refactor that in order to call server only once and all the async/reload stuff still works ?
AuthService :
export class AuthService {
public userRole: UserBoRole;
public authState$: Observable<firebase.User>;
constructor(
private afAuth: AngularFireAuth,
private snackBar: SnackBarService,
private translate: TranslateService,
private router: Router,
private grpcService: GrpcService
) {
this.authState$ = this.afAuth.authState.pipe(
take(1),
mergeMap(user => {
if (!user) {
return of(user);
}
// User is successfully logged in,
// now we need to check if he has a correct role to access our app
// if an error occured, consider our user has not logged in, so we return null
return this.checkProfile().pipe(
take(1),
map(() => {
this.test = true;
return user;
}),
catchError(err => {
console.error(err);
return of(null);
})
);
})
);
// Subscribing to auth state change. (useless here because access logic is handled by the AuthGuard)
this.authState$.subscribe(user => {
console.log('authState$ changed :', user ? user.toJSON() : 'not logged in');
});
}
checkProfile() {
return this.callAuthApi().pipe(
map((customer) => {
if (!customer || customer.hasRole() === "anonymous") {
return Promise.reject(new Error(AuthService.AUTH_ERROR_ROLE));
}
this.userRole = customer.getRole();
})
);
}
isAuthenticated(): Observable<boolean> {
return this.authState$.pipe(map(authState => !!authState));
}
}
AuthGuard :
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
check(): Observable<boolean> {
return this.authService.isAuthenticated().pipe(
catchError(err => {
// notifying UI of the error
this.authService.handleAuthError(err);
// signout user
this.authService.signOut();
// if an error occured, consider our user has not logged in
return of(false);
}),
tap(isAuthenticated => {
if (!isAuthenticated) {
// redirecting to login
this.router.navigate(['login']);
}
})
);
}
canActivateChild(): Observable<boolean> {
return this.check();
}
canActivate(): Observable<boolean> {
return this.check();
}
}
Thanks

You can change your checkProfile() function to return observable instead of observable from http request or promise in case of error. First you will check if the user already authenticated(I assumed that userRole will be fine since you save it after call to back end) and if yes return a newly created observable without call to your back end, otherwise you will make a request and emit your observable based on result of http call. With next example you will make call only once:
checkProfile() {
return new Observable((observer) => {
if (this.userRole) {
observer.next();
observer.complete();
} else {
this.callAuthApi().pipe(
map((customer) => {
if (!customer || customer.hasRole() === "anonymous") {
observer.error(new Error(AuthService.AUTH_ERROR_ROLE));
observer.complete();
}
this.userRole = customer.getRole();
observer.next();
observer.complete();
})
);
}
});
}

Haha, ReactiveX is not easy one. It has a quite steep learning curve.
But it is really powerful.
1. call server only once
You can use shareReplay.
To understand how shareReplay works, have a look here https://ng-rxjs-share-replay.stackblitz.io
//shareReplay example
ngOnInit() {
const tods$ = this.getTodos();
tods$.subscribe(console.log);// 1st sub
tods$.subscribe(console.log);// 2st sub
}
getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.url)
.pipe(
tap(() => console.log('Request')),
shareReplay(1) // compare with comment and uncomment
);
}
Output with shareReplay
Request
[Object, Object, Object]
[Object, Object, Object]
Output without shareReplay
Request
[Object, Object, Object]
Request
[Object, Object, Object]
You may use shareReplay in your auth service code.
//auth.services.ts
import { shareReplay } from 'rxjs/operators';
...
this.user$ = this.afAuth.authState.pipe(
tap(user => {
console.log('login user$ here', user)
}),
switchMap(user => {
if (user) {
//do something
return this.db.object(`users/${user.uid}`).valueChanges();
} else {
return of(null);
}
}),
shareReplay(1) //**** this will prevent unnecessary request****
);
2. async and await
toPromise()
//auth.service.ts
...
getUser() {
return this.user$.pipe(first()).toPromise();
}
//auth.guard.ts
...
async canActivate(next: ActivatedRouteSnapshot
, state: RouterStateSnapshot
): Promise<boolean> {
const user = await this.auth.getUser();
//TODO your API code or other conditional authentication here
if (!user) {
this.router.navigate(['/login']);
}
return !!user;
}
Hope this will help you.

Related

Angular/Firebase: How to put user response in front-end model

I am new to both. My thought process is as follows. When I login in with Angular to my Firebase backend I send a response to the console if it succeeds. In this response I can see all the keys that a firebase user has. What I want to do is to link these to/in a user model in my front-end so I can access it easily when showing a user profile page or something else. (I don't know if this is the correct thought process too. Nudge me in the right way if you know a better solution)
auth.service.ts
constructor(
private angularFireAuth: AngularFireAuth,
) {
this.userData = angularFireAuth.authState;
}
signIn(email: string, password: string) {
return this.angularFireAuth.auth.signInWithEmailAndPassword(email, password);
}
login.component.ts
signIn(email: string, password: string) {
this.spinnerButtonOptions.active = true;
email = this.loginForm.value.email;
password = this.loginForm.value.password;
this.auth.signIn(email, password).then(
res => {
console.log('signed in ', res);
this.router.navigate(['/dashboard']);
}
).catch(
error => {
console.log('something went wrong ', error);
this.formError = true;
this.spinnerButtonOptions.active = false;
}
);
}
How would I got on to do this? I've searched everywhere and can't find any solution. Is this actually the correct way? If there is a better way please let me know!
You can use your auth service to store the data returned from the Firebase backend. Or else you can store it in a shared service where it's been available throughout the all components and modules.
In your auth service :
#Injectable()
export class AuthService{
public usermodel:any;
constructor(private angularFireAuth: AngularFireAuth) {
this.userData = angularFireAuth.authState;
}
signIn(email: string, password: string) {
return this.angularFireAuth.auth.signInWithEmailAndPassword(email, password);
}
setLoggedInUserData(userDetails: any) {
this.usermodel = userDetails;
}
getLoggedInUserData() {
return this.usermodel;
}
}
In you login component.ts :
signIn(email: string, password: string) {
this.spinnerButtonOptions.active = true;
email = this.loginForm.value.email;
password = this.loginForm.value.password;
this.auth.signIn(email, password).then(
res => {
this.authService.setLoggedInUserData(res);
this.router.navigate(['/dashboard']);
}
).catch(
error => {
console.log('something went wrong ', error);
this.formError = true;
this.spinnerButtonOptions.active = false;
}
);
}
In other components where you need to use the use details inject the auth.service.ts and use the getLoggedInUserData() method fetch the logged in users details.
There are several other ways of doing this too. One option is to use the ngrx store implementation. Other ways is to use global data service at the root level of your angular app to store the user details.

How do I bubble up a non asynchronous error

I'm trying to bubble up an error that comes from the verify function in jsonwebtoken, however, it wraps it in another Internal 500 Status error, rather than an Unauthorized error, that I want it to be.
Most of the components are built in loopback-next authentication components. The class below is a provider for a class called AuthenticationStrategy. AuthenticationStrategy is just a class with a passport strategy that returns an Authentication Strategy, which is passed on the metadata of a route. In the class below (the provider of type Authentication strategy, value() is the function returned when calling the provider. The passport strategy verification function has to first be converted to an Authentication strategy first, then returned through the value function to the Provider. When value() is called, the verify function runs on the metadata of the route, in this case, the bearer token. The Function cb below is the same as the 'done' function in a normal passport strategy, and returns (error, object), respectively
import {AuthenticateErrorKeys} from '../error-keys';
import {RevokedTokenRepository, UserRepository} from '../../../repositories';
import {repository} from '#loopback/repository';
import {Strategy} from 'passport-http-bearer'
import {HttpErrors} from '#loopback/rest';
import {StrategyAdapter} from '#loopback/authentication-passport'
import {AuthenticationStrategy} from '#loopback/authentication'
import {inject, Provider} from '#loopback/context'
var verify = require('jsonwebtoken').verify
export const BEARER_AUTH_STRATEGY = 'bearer';
export class PassportBearerAuthProvider implements Provider<AuthenticationStrategy> {
constructor(
#repository(RevokedTokenRepository)
public revokedTokenRepository: RevokedTokenRepository,
#repository(UserRepository)
public userRepository: UserRepository,
){}
value(): AuthenticationStrategy {
const bearerStrategy = new Strategy(this.verify.bind(this));
return this.convertToAuthStrategy(bearerStrategy);
}
async verify (token: string, cb: Function){
try{
if (token && (await this.revokedTokenRepository.get(token))) {
throw new HttpErrors.Unauthorized(AuthenticateErrorKeys.TokenRevoked);
}
const userAuthToken = await verify(token, process.env.JWT_SECRET as string, {
issuer: process.env.JWT_ISSUER,
})
let authUser = await this.userRepository.getAuthUser(userAuthToken.id)
return cb(null, authUser)
}
catch(error) {
if (error.name && error.name === "JsonWebTokenError") {
return cb(new HttpErrors.Unauthorized(AuthenticateErrorKeys.TokenInvalid), null)
}
if (error.name && error.name === "TokenExpiredError") {
return cb(new HttpErrors.Unauthorized(AuthenticateErrorKeys.TokenExpired), null)
}
if (error.code && error.code === "ENTITY_NOT_FOUND") {
return cb(new HttpErrors.Unauthorized(`${AuthenticateErrorKeys.UserDoesNotExist}, id: ${error.entityId}`), null)
}
return cb(error, null)
}
}
// Applies the `StrategyAdapter` to the configured basic strategy instance.
// You'd better define your strategy name as a constant, like
// `const AUTH_STRATEGY_NAME = 'basic'`
// You will need to decorate the APIs later with the same name
convertToAuthStrategy(bearer: Strategy): AuthenticationStrategy {
return new StrategyAdapter(bearer,BEARER_AUTH_STRATEGY);
}
}
The sequence below is run every time someone makes an API request. If above the route, the route is decorated with #authenticate[BEARER_AUTH_TOKEN], the provider above is called, and the verify function is run on the metadata.
export class MySequence implements SequenceHandler {
constructor(
#inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
#inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
#inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
#inject(SequenceActions.SEND) public send: Send,
#inject(SequenceActions.REJECT) public reject: Reject,
#inject(AuthorizationBindings.AUTHORIZE_ACTION)
protected checkAuthorisation: AuthorizeFn,
#inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
) {}
async handle(context: RequestContext) {
try {
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route)
const authUser = await this.authenticateRequest(request).catch(error => {
Object.assign(error, {statusCode: 401, name: "NotAllowedAccess", message: (error.message && error.message.message)? error.message.message: "Unable to Authenticate User" });
throw error
})
console.log(authUser)
const isAccessAllowed: boolean = await this.checkAuthorisation(
authUser && authUser.permissions,
request,
);
if (!isAccessAllowed) {
throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
}
const result = await this.invoke(route, args);
this.send(response, result);
} catch (error) {
this.reject(context, error);
}
}
}
But when it catches the error, the status is 500 and the 401 Unauthorized error is wrapped in the Internal Status error. How can I instead return the 401 error?
I have a lot more cases like this where the error is deeper, so I'm trying to have a rigorous implementation.

HttpClient & Rxjs

I am working on a case where during a network connection we sometimes might have a limited internet connectivity where we unable to get response from the server or failed response as HttpError.
I hereby trying to ping the URL every second to check whether we are getting response or not, for this
I am trying this code, this is working fine in online method but when i am turning my internet of is doesn't return me false value.
fetch-data.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpResponse, HttpErrorResponse } from '#angular/common/http';
import { Posts } from './posts';
import { Observable, interval, throwError, of } from 'rxjs';
import { take, exhaustMap, map, retryWhen, retry, catchError, tap, mapTo, } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class FetchDataService {
public url = 'https://jsonplaceholder.typicode.com/posts';
constructor(private _httpClient: HttpClient) { }
getData() {
const ob = interval(1000);
return ob.pipe(
exhaustMap(_ => {
return this._httpClient.get<Posts[]>(this.url, { observe: 'response' });
}),
map(val => {
if (val.status === 200)
return true;
throw val;
}),
retryWhen(errors => {
return errors.pipe(map(val => {
if (val.status === 0)
return false;
}))
})
);
}
// private handleError(error: HttpErrorResponse) {
// if (error.error instanceof ErrorEvent) {
// // A client-side or network error occurred. Handle it accordingly.
// console.error('An error occurred:', error.error.message);
// } else {
// // The backend returned an unsuccessful response code.
// // The response body may contain clues as to what went wrong,
// console.error(
// `Backend returned code ${error.status}, ` +
// `body was: ${error.error}`);
// if (error.status !== 200)
// return of(false);
// }
// // return an observable with a user-facing error message
// return throwError(
// 'Something bad happened; please try again later.');
// };
}
pulldata.component.html
import { Component, OnInit } from '#angular/core';
import { FetchDataService } from '../fetch-data.service';
import { Observable } from 'rxjs';
import { Posts } from '../posts';
#Component({
selector: 'app-pulldata',
templateUrl: './pulldata.component.html',
styleUrls: ['./pulldata.component.css']
})
export class PulldataComponent implements OnInit {
public data;
public error = '';
constructor(private _fecthDataServe: FetchDataService) { }
ngOnInit() {
this._fecthDataServe.getData().subscribe(val => {
this.data = val;
console.log(this.data);
});
}
}
what would be the best solution to check the internet connectivity in this manner?
My personal preference would be to not do this with HTTP because of data overhead. Every HTTP request will contain cookie data and other headers that are often useless in these kinds of scenarios.
Is it possible for you to use Web Sockets? With these, you can open up a connection to the server that, unlike HTTP, doesn't have to close. It can remain open forever. And you can subscribe to events to get notified about connection losses. Web Sockets also have the added benefit that it's a new protocol based on TCP, it's not HTTP, resulting in a lot less network data will have to be send.
let socket = new WebSocket('wss://yourserver/socket...');
socket.addEventListener('open', () => console.log('connection has been opened'));
socket.addEventListener('close', () => console.log('connection has been closed'));
In your situation, you might also want to check out the Reconnecting WebSocket, which reconnects when the connection drops. You could also write this small wrapper yourself, of course.
Also, what might even be a simpler solution. You can subscribe to online/offline events on the window object: read more on MDN
function updateOnlineStatus(event) {
var condition = navigator.onLine ? "online" : "offline";
// ...do something with the new status
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
Both of these solutions should be easily wrappable in an Angular service, but let me know if that works out and/or if these solutions are an option for you.

angular user-idle-service issue

I am using the user-idle-service (https://www.npmjs.com/package/angular-user-idle) for angular. I am using it for my angular 6 front end application, but am running into issues where my users are getting kicked out prematurely when they are active.
I fire the library as soon as a user logs in to the application. It works most of the time, but sometimes users are getting prematurely kicked out when they are active in their browser. I am using this only for browser based usage.
In my service class I fire the below, as well as configuration below that. Is there anything in the library I am using obviously wrong? :
constructor(private http: Http,
private router: Router,
private cookieService: CookieService,
private userIdle: UserIdleService) {
this.userIdle.setCustomActivityEvents(merge(
fromEvent(window, 'mousemove'),
fromEvent(window, 'resize'),
fromEvent(document, 'keydown'),
fromEvent(document, 'touchstart'),
fromEvent(document, 'touchend'),
fromEvent(window, 'scroll')
));
}
login(credentials: Credentials): Observable<any> {
return this.http.post<any>(this.loginUrl, JSON.stringify(credentials), {
headers: new HttpHeaders({'Content-Type': 'application/json'})
})
.pipe(map(userDetails => {
this.setUserDetailsAndExtendExpirationInLocalStorage(userDetails);
this.startUserIdle();
this.goToHome();
}));
}
startUserIdle() {
this.userIdle.startWatching();
this.subscriptions.push((this.userIdle.onTimerStart().subscribe()));
this.subscriptions.push((this.userIdle.onTimeout().subscribe(() => {
this.logout();
})));
this.subscriptions.push(this.userIdle.ping$.subscribe(() => {
this.refreshToken();
}));
}
refreshToken() {
return this.http.get<any>(this.refreshUrl, {
headers: new HttpHeaders({'Content-Type': 'application/json'})
}).subscribe(() => {
// do stuff
}, () => {
this.logout();
});
}
logout() {
this.goToLogin();
this.removeUserDetails();
this.removeSessionToken();
this.userIdle.resetTimer();
this.userIdle.stopTimer();
this.userIdle.stopWatching();
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.setIsLoggedIn(false);
}
Also in my app.module.ts file I configure it like below:
UserIdleModule.forRoot({idle: 900, timeout: 1, ping: 840})

How to chain/combine Observables

Ok this has been bugging me for a while, wondering if any one could show me a good way of chaining Observables between multiple services.
In the example below in the Auth class what would be a good way of creating an Observable from the this.api.postSignIn() so the signInSubmit() can subscribe to it back in the component? It is worth noting that this.api.postSignIn() is subscribing to an Angular2 http request.
Is this a bit of an anti pattern and is there better ways of doing this?
Basically the functionality I would like to achieve is:
Component - responsible for collecting the sign in data and sending it to the auth service in the correct format. Then once the Auth sign in is complete navigate to the admin page.
Service - Make api call to get token, set token via the token service and set isSignedIn bool then defer control back to the calling component.
#Component({...})
export class SignIn {
private signIn:SignInModel = new SignInModel();
constructor(private auth:Auth, private router:Router) {
}
ngOnInit() {
}
signInSubmit() {
this.auth.signIn(this.signIn)
.subscribe(
() => {
this.router.navigate(['/admin']);
}
)
}
}
#Injectable()
export class Auth {
private isSignedIn:boolean = false;
constructor(private api:Api, private tokenService:TokenService) {
}
public signIn(signIn:SignInModel) {
return this.api.postSignIn(signIn)
.subscribe(
response => {
this.tokenService.set(new TokenModel(response.token));
this.isSignedIn = true;
},
error => {
console.log(error);
}
);
}
public signOut() {
}
}
I would leverage the do and catch operators instead of subscribing within the signIn method.
Here is the refactored signIn method:
public signIn(signIn:SignInModel) {
return this.api.postSignIn(signIn)
.do(
response => {
this.tokenService.set(new TokenModel(response.token));
this.isSignedIn = true;
})
.catch(
error => {
console.log(error);
}
);
}
In your case, you can't subscribe on the returned object of this method since the subscribe method returns a subscription and not an observable. So you can't subscribe on it...

Categories

Resources