How to wait for firebase auth before starting angular app - javascript

I would like to display a small loading logo while the firebase authentication is retrieving a user token, before starting "for real" the single page application.
So far I have an authentication service :
constructor(
public afAuth: AngularFireAuth,
) {
this.afAuth.onAuthStateChanged(user => {
if (user) {
this.setCredentials(user)
}
})
}
setCredentials(user: firebase.User) {
return user.getIdTokenResult(true).then(idTokenResult => {
this.credentials = {
userId: idTokenResult.claims.id,
role: idTokenResult.claims.role,
token: idTokenResult.token,
};
// STARTS THE APPLICATION NOW ?
})
}
Is it possible to achieve such behavior ? I've read about APP_INITIALIZER without success. I want to avoid localstorage / session storage and instead rely solely on this initialization.
Update :
created an init function :
export function initApp(auth: AuthService, afAuth: AngularFireAuth) {
return () => {
return new Promise((resolve) => {
afAuth.user.pipe(
take(1),
).subscribe(user => {
if (user) {
auth.setCredentials(user)
.then(() => resolve())
} else {
resolve();
}
})
});
}
}
And edited AppModule providers:
providers: [
interceptorProviders /* my interceptors */,
{
provide: APP_INITIALIZER,
useFactory: initApp,
deps: [AuthService, AngularFireAuth],
multi: true
}
]
Still need to figure out how to add a waiting logo but it's another question. I'll update asap.

Answering to my own question
To summarize I wanted to make sure my token claims (role, and user id per say) associated with a firebase user were stored in my auth service before dealing with routing, because components inside these routes would use those credentials.
In the end I did not follow the APP_INITIALIZER that is not really a good solution.
Auth Service
private _credentials: BehaviorSubject<Credentials> = new BehaviorSubject<Credentials>(null);
public readonly credentials$: Observable<Credentials> = this._credentials.asObservable();
constructor(private afAuth: AngularFireAuth) {
this.afAuth.authState.subscribe(user => {
this._credentials.next(null);
if (user) {
user.getIdTokenResult().then(data => {
const credentials = {
role: data.claims.role,
token: data.token,
userId: data.claims.userId
}
this._credentials.next(credentials);
console.log(credentials);
})
} else {
this._credentials.next({role: null, token: null, userId: null});
}
})
}
get credentials(): Credentials {
return this._credentials.value;
}
Display a waiting spinner in app.component
Below prevents routes from displaying if credentials not set.
In the template :
<div *ngIf="!(credentials$ | async)" class="logged-wrapper">
<div class="spinner-wrapper">
<mat-spinner class="spinner"></mat-spinner>
</div>
</div>
<router-outlet *ngIf="(credentials$ | async)"></router-outlet>
In the component :
credentials$: Observable<any>;
constructor(
private auth: AuthService,
) {
this.credentials$ = this.auth.credentials$;
}
Auth Guard
The takewhile allows me to make sure my credentials are set before going further.
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot):Promise<boolean> {
return new Promise((resolve) => {
this.auth.credentials$.pipe(
takeWhile(credentials => credentials === null),
).subscribe({
complete: () => {
const credentials = this.auth.credentials
if (!credentials.role) {
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } })
resolve(false);
}
if (next.data.roles && next.data.roles.indexOf(credentials.role) === -1) {
this.router.navigate(['/']);
resolve(false);
}
resolve(true)
}
})
})
}

You should use your authentication service in a CanActivate router guard: https://angular.io/api/router/CanActivate
This means your AppModule will initially load and then your child route (ex. MainModule with router path '') has the guard. Then in AppModule you can check for the status of the service and show a loading information until MainModule is activated (when firebase auth is finished)

Related

firebaseAuth.onAuthStateChanged return user null

I'm trying to implement authentification using firebase auth, the things is that when it comes to persistance i'm stuck.
I tried to use local storage as you can see below :
AuthService.ts :
import {HttpClient} from '#angular/common/http';
import { Injectable } from '#angular/core';
import {AngularFireAuth} from '#angular/fire/auth';
import {Router} from '#angular/router';
import firebase from 'firebase/app';
#Injectable({
providedIn: 'root'
})
export class AuthService {
userData: any;
constructor(private http: HttpClient, private firebaseAuth: AngularFireAuth, private router: Router) {
this.firebaseAuth.useEmulator('http://localhost:9099');
this.firebaseAuth.authState.subscribe(user => {
if (user) {
this.userData = user;
localStorage.setItem('user', JSON.stringify(this.userData));
JSON.parse(localStorage.getItem('user'));
} else {
localStorage.setItem('user', null);
JSON.parse(localStorage.getItem('user'));
}
});
this.firebaseAuth.onAuthStateChanged( user => {
console.log(user);
});
}
signIn(email: string, password: string): void {
this.firebaseAuth.setPersistence(firebase.auth.Auth.Persistence.LOCAL).then(() => {
this.firebaseAuth.signInWithEmailAndPassword(email, password)
.then(result => {
this.router.navigate(['/dashboard/accueil']);
console.log('Nice, it worked!');
})
.catch(error => {
console.log('Something went wrong:', error.message);
});
}).catch(error => {
console.log('Something went wrong:', error.message);
});
}
signOut(): void {
this.firebaseAuth.signOut().then(() => {
localStorage.removeItem('user');
this.router.navigate(['/dashboard/connexion']);
});
}
forgetPassword(email: string): void {
this.firebaseAuth.sendPasswordResetEmail(email).then(() => {
window.alert('Password reset email sent, check your inbox.');
}).catch((error) => {
window.alert(error);
});
}
get isLoggedIn(): boolean {
const user = JSON.parse(localStorage.getItem('user'));
return (user !== null) ? true : false;
}
}
But the problem is that firebaseAuth.onAuthStateChanged return null after refreshing the page with F5 even authState. It's like onAuthStateChange is losing his last state after refresh.
Note that i'm using firebase Emulator.
app.module.ts
Firebase module has been well import
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
You can take a look at the confing here :
environnement.ts
firebase: {
apiKey: 'xxxxxxxxxxxx',
projectId: 'xxxx'
}
Maybe it's link to this error that i'm getting in the web console when i'm refreshing the page :
POST https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=xxxxxxx 400
Note that key is equal to my apiKey state in the config.
When i look closer here what the error say :
{
"error": {
"code": 400,
"message": "INVALID_ID_TOKEN",
"errors": [
{
"message": "INVALID_ID_TOKEN",
"domain": "global",
"reason": "invalid"
}
]
}
}
Any help would be greatly appreciate.
EDIT
Here what i get after refresh my page from onAuthStateChanged :
There is no 2 calls, only 1 that return false.
Firebase Auth will persist the login state by default, so you don't have to manually save the token into local storage. As Frank said, onAuthStateChanged will fire will user being null initially, but if the user is signed in, it will fire again shortly after that with the actual user value.
Following to the comment section of the accepted answer, I decided to highlight the fact that this appears to be en emulator issue.
This was driving me crazy. No matter what I did, even jumping up and down, the currentUser was always null. I saw the same behavior: onAuthStateChanged() got called only once and all I got was a null-user.
After removing my provider for the Auth-emulator, everything started to work without issues.
providers: [
// ...
{ provide: REGION, useValue: envFirebase.region},
// THE PROBLEM:
{
provide: USE_AUTH_EMULATOR,
useValue: envFirebase.useEmulators ? ['http://localhost:9099'] : undefined
},
],

Angular await service between components, Behavior Subject

I have an issue in my Angular web store when i refresh the window, i create a service that takes the user data from the server and then inject to the 'products' section with BehaviorSubject, my goal is to make just one request to the server:
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
#Injectable({
providedIn: 'root'
})
export class DataService {
private userId = new BehaviorSubject<any>('');
currentUserId = this.userId.asObservable();
constructor() { }
sendUserId(message: string){
this.userId.next(message)
}
}
This works fine but the problem is when i refresh the window in products section, in console i can see that the service takes the user data but when i getProducts() it throws an error, it seems like getProducts() makes the request before the service had the response, i need the user Id to make the products request. My question: Is there a way to await the response of BehaviorSubject and then make the getProducts() request?. This is the code in the products section:
ngOnInit(): void {
this._dataService.currentUserId.subscribe(userId => this.userId = userId);
if(this.userId.length === 0){
this.userService.getUserProfile().subscribe(
res => {
this.userDetails = res['user'];
this.userId = this.userDetails._id;
this.getProducts();
},
err => {
console.log(err);
}
);
} else {
this.getProducts();
}
}
As you can see, i do a condition to check if userId exists, if not i have to make a new user request, this fix the bug but i think there's a better way to solve this. Thanks in advance.
How about placing all your logic within the observer's next function as below:
this._dataService.currentUserId.subscribe(userId => {
if (userId.length === 0)
{
this.userService.getUserProfile().subscribe(
res => {
this.userDetails = res['user'];
this.userId = this.userDetails._id;
this.getProducts();
},
err => {
console.log(err);
}
);
} else
{
this.getProducts();
}
});

Object is exist with value, but when access the property returned undefined

i find weird things. I have AuthService which saves authentication needs of my apps, included authentication token.
#IonicPage()
#Component({
selector: 'page-login',
templateUrl: 'login.html',
})
export class LoginPage {
constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl:ModalController,public auth: AuthService) {
}
ionViewDidLoad() {
console.log(this.auth)
console.log(this.auth.loggedIn)
if(this.auth.loggedIn){
console.log(this.auth);
this.navCtrl.push("TabsPage");
}
}
}
when i call
console.log(this.auth)
it returned authentication
buth when i call
console.log(this.auth.loggedIn)
it return null
this my auth.service.ts
import { Injectable, NgZone, Component } from '#angular/core';
import { Storage } from '#ionic/storage';
// Import AUTH_CONFIG, Auth0Cordova, and auth0.js
import { AUTH_CONFIG } from './auth.config';
import Auth0Cordova from '#auth0/cordova';
import * as auth0 from 'auth0-js';
#Injectable()
export class AuthService {
Auth0 = new auth0.WebAuth(AUTH_CONFIG);
Client = new Auth0Cordova(AUTH_CONFIG);
accessToken: string;
user: any;
loggedIn: boolean;
loading = true;
constructor(
public zone: NgZone,
private storage: Storage
) {
this.storage.get('profile').then(user => this.user = user);
this.storage.get('access_token').then(token => this.accessToken = token);
this.storage.get('expires_at').then(exp => {
this.loggedIn = Date.now() < JSON.parse(exp);
this.loading = false;
});
}
login() {
this.loading = true;
const options = {
scope: 'openid profile offline_access'
};
// Authorize login request with Auth0: open login page and get auth results
this.Client.authorize(options, (err, authResult) => {
if (err) {
throw err;
}
// Set access token
this.storage.set('access_token', authResult.accessToken);
this.accessToken = authResult.accessToken;
// Set access token expiration
const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
this.storage.set('expires_at', expiresAt);
// Set logged in
this.loading = false;
this.loggedIn = true;
// Fetch user's profile info
this.Auth0.client.userInfo(this.accessToken, (err, profile) => {
if (err) {
throw err;
}
this.storage.set('profile', profile).then(val =>
this.zone.run(() => this.user = profile)
);
});
});
}
logout() {
this.storage.remove('profile');
this.storage.remove('access_token');
this.storage.remove('expires_at');
this.accessToken = null;
this.user = null;
this.loggedIn = false;
}
isLoggedIn() :boolean{
return this.loggedIn;
}
}
i'm using ionic3 and auth0 authentication, previously i think that was my fault to not use public identifier on my property. but when i change the property to public, or create getter method that returned the property it still not working at all.
This is due to when the chrome console evaluates the object. If you open the object in your console, you'll see a tiny blue info icon. This will say:
Value was evaluated just now
Basically what happens is that the object content changed between the time you logged it, and the time you opened it in your console.
The login action is asynchronous, which means that the loggedIn property on the auth object will be set after the ionViewDidLoad is called. Perhaps a good thing would be to set the auth inside an APP_INITIALIZER provider, or have some Observable on your auth on which you can listen for auth changes
1.you have colling this.loggedIn before assign so that it's undefined.
in your case write console.log(this.auth.loggedIn) after login. please check that scenario.
2.for now, assign some value for loggedIn variable then print it will print a value
loggedIn: boolean; => loggedIn: boolean=false;
then print a value it will work
in some other component
ngOnInit() {
console.log(this.auth.loggedIn)
}

What's the best approach for user auth login?

AuthService
login(user: User){
return this.http.post<any>(`${this.API_URL}`, {
email: user.username,
password: user.password
});
}
isUserLoggedIn(){
return this.http.get<any>(`${this.API_URL}/1`);
}
logoff(){
//this.loggedIn.next(null);
this.router.navigate(['/login']);
}
LoginComponent
onSubmit(){
this.submitted = true;
this.authService.login(this.loginForm.value).subscribe(r => {
if(r.success){
this.router.navigate(['/home']);
}
});
}
Auth Guard
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): any{
return this.authService.isUserLoggedIn()
.pipe(
map(res => {
if (res.isAuth){
return true;
}
this.router.navigate(['/login']);
return false;
})
);
}
LoginComponent: sends the data to an API for login (username and password). The API will return success = true if the user exists on the database;
Guard: (isUserLoggedIn()) check if user is logged in
and if the session is still alive.
Is there a better approach for user authentication in the Angular 4+?
Is this approach used in most projects?
Maybe, you could store user's data in local storage when he logins the first time. Then use a BehaviorSubject from rxjs and convert it to Observable. So every time the user has any changes (language, first name, etc), in any parts of your app you can access to that information.
So a thing like that:
Login Component
this.userService.login(username, password).subscribe(s => {
this.userService.user = s;
this.router.navigate(['/home'])
})
User Service
private userSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
readonly currentUser$: Observable<any> = this.userSubject.asObservable();
set user(user: any) {
// manipulate your user info and put it in localStorage after
this.userSubject.next(user);
}
get user() {
return this.userSubject.value ? this.userSubject.value : window.localStorage.getItem('user');
}
Auth Guard
Now in your guard, you can check the current user from the get method or you can subscribe to user observable, it's your choice
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): any{
return this.userServivce.user ? true : false;
}
You can complicate your logic as you wish.

Error in Logout through MSAL.JS in Javascript / Angular 6 SPA application using B2C

I have one Javascript SPA application using MSAL.JS for authentication against Azure AD B2C and another Angular 6 SPA application using MSAL for Angular against Azure AD B2C.
In both the applications the logout is throwing below error.
Correlation ID: 6de6e068-7b07-4d24-bac4-c1af3131815b
Timestamp: 2018-09-25 16:16:20Z
AADB2C90272: The id_token_hint parameter has not been specified in the request. Please provide token and try again.
For Logout, MSAL has very simple logout api which does not take any parameter, so how can I provide id_token_hint? Am I missing something? Is there any config parameter I need to provide while injecting MsalModule in Angular Application. Or anything similar in Javascript app for Msal.UserAgentApplication.
I m basically using the currently latest "msal": "^0.2.3" , this is my authentication service, there is no configuration needed in the app.module, and the logout works perfectly:
import { Injectable } from '#angular/core';
import { environment } from '../../environments/environment';
import * as Msal from 'msal';
import { User } from "msal/lib-commonjs/User";
import { ApiService } from './api.service';
import { BackendRoutes } from './backend.routes';
#Injectable()
export class AuthenticationService {
private _clientApplication: Msal.UserAgentApplication;
private _authority: string;
constructor(private apiService: ApiService, private backendRoutes: BackendRoutes) {
this._authority = `https://login.microsoftonline.com/tfp/${environment.tenant}/${environment.signUpSignInPolicy}`;
this._clientApplication =
new Msal.UserAgentApplication(
environment.clientID,
this._authority,
this.msalHandler,
{
cacheLocation: 'localStorage',
redirectUri: window.location.origin
});
}
msalHandler(errorDesc: any, token: any, error: any, tokenType: any) {
let userAgent: Msal.UserAgentApplication = <any>(this);
if (errorDesc.indexOf("AADB2C90118") > -1) {
//Forgotten password
userAgent.authority = `https://login.microsoftonline.com/tfp/${environment.tenant}/${environment.passResetPolicy}`;
userAgent.loginRedirect(environment.b2cScopes);
} else if (errorDesc.indexOf("AADB2C90077") > -1) {
//Expired Token, function call from interceptor with proper context
this.logout();
}
}
addUser(): void {
if (this.isOnline()) {
this.apiService.post(this.backendRoutes.addUser).subscribe();
}
}
login(): void {
this._clientApplication.loginRedirect(environment.b2cScopes);
}
logout(): void {
this._clientApplication.logout();
}
getAuthenticationToken(): Promise<string> {
return this._clientApplication.acquireTokenSilent(environment.b2cScopes)
.then(token => token)
.catch(error => {
return Promise.reject(error);
});
}
And the interceptor linked to it:
export class AuthenticationHttpInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return from(this.authenticationService.getAuthenticationToken()
.then(token => {
return req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
})
.catch(err => {
this.authenticationService.msalHandler(err,null,null,null);
return req;
}))
.switchMap(req => {
return next.handle(req);
});
}
}

Categories