- Goal
- Authentication Working Principle
- Adding Authentication Section into Project
- Switching between Authentication Modes
- Handling Form Inputs
- Firebase Setup
- Preparing Signup Request
- Sending a Signup Request
- Adding Loading Spinner
- Handling an Error only for Signup Request
- Sending Login Request
- Handling The Error From Sign in as well as Signup Request
- Creating and Storing User Data
- Reflecting Auth State in UI
- Make Fetching Work Again
- Make Save Data Work Again Using Interceptor
- Adding Functionality to Logout
- Add Auto Login
- Add Auto Logout
- Adding Auth Guard
- Create an authentication page with two input fields
- input 1: Email
- input 2: Password
- two buttons [Sign up | Login]
-
In Conventional approach Authentication in multiple page web application is performed using session.
-
In Angular based single page application, where communication between client and server happens using REST API
-
And the authentication in Angular app happens over JSON web token
-
When Angular client sends authentication request to server, server responses with web token if authentication credentials are valid
-
Web token includes ENCODED string with meta data
-
Web token does not include the encrypted string
-
Diagram Showing Web Token Based Authentication
-
Create the component which holds the authentication logic and templet
app |---Auth |---- auth.component.ts [logic] |---- auth.component.html [templet]
-
Basic skeleton of
auth.component.ts
import { Component } from "@angular/core"; @Component({ selector: 'app-auth', templateUrl: './auth.component.html', }) export class AuthComponent{ }
-
An authentication templet
auth.component.html
<div class="row"> <div class="col-xs-12 col-md-6 col-md-offset"> <form> <div class="form-group"> <label for="email">E-mail</label> <input type="text" id="email" class="form-control"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" class="form-control"> </div> <div> <button class="btn btn-primary"> Sign up</button> | <button class="btn btn-primary"> Switch to Login</button> </div> </form> </div> </div>
-
Registering the auth component in
app.module.ts
@NgModule({ declarations: [ AuthComponent ] })
-
Setup routing for Authentication page in
app-routing.module.ts
const appRoutes: Routes = [ {path: 'auth', component: AuthComponent} ]
-
App Authentication tab in header
header.component.html
<div class = "navbar-collapse"> <ul> <li>Recipe</li> <li>Shopping list</li> <li routerLinkActive = "active"> <a routerLink = "/auth">Authentication</a> </li> </ul> </div>
-
Switch between Login / Sign in mode
-
Adding mode property and method which change the property in
auth.component.ts
export class AuthComponent{ isLoginMode = true; // mode property onSwitchMode(){ // method which can change the mode property this.isLoginMode = !this.isLoginMode; } }
-
Accessing the above implemented property from templet
auth.component.html
usage of ternary operation to check the mode and choose the button text
<!--The input fields will remain same for both login and signup--> <div> <button class="btn btn-primary" type="submit"> {{isLoginMode ? 'Login' : 'Signup'}} </button> | <button class="btn btn-primary" (click) = "onSwitchMode()" type="button"> Switch to {{isLoginMode ? 'SignUp' : 'Login'}} </button> </div>
-
Add tags on the input fields
<input type="text" name = "email" ngModel required email > <input type="password" name = "password" ngModel required minlength="6" > <!--name and ngModel are tags to register the control in form--> <!--required, email and minlength are validators-->
-
Declare local reference of the form and use it as a parameter to the submit method
<form (ngSubmit)="onSubmit(authForm)" #authForm = ngForm> .... </form> <!--#authForm is the local reference of the form--> <!--onSubmit() get executed on click of submit button--> <!--authForm a reference is passed as a parameter to the onSubmit method-->
-
Implement
onSubmit()
method inauth.component.ts
onSubmit(form: NgForm){ console.log(form.value); // logging the obtained form values form.reset(); // resetting the form }
-
Create
auth.service.ts
in auth folderThis service will be responsible for user signing up, signing in and managing the web token
-
Basic skeleton of the auth service
@Injectable({ providedIn: 'root' }) export class AuthService{ // following url is signup url and api key is obtained from project setting private signUpURL = "link obtained from firebase auth api signup + API key of project" constructor(private http : HttpClient){ } signUp(email:string, password:string){ }
-
Implement the signup method in
auth.service.ts
// this method returns the observable signUp(email:string, password:string){ return this.http.post(this.signUpURL, {email:email, password:password, returnSecureToken:true }) } // firebase api needs email, password and returnsSecureToken with signup request // email and password will be obtained as the method parameters // subscription to the post request will be done in the method which calls this method
-
Implement Interface
AuthResponseData{ }
// firebase gives following object as the response for signup post request // can be found in the firebase api doc interface AuthResponseData { idToken : string; email : string; refreshToken : string; expiresIn : string; localId : string; } @Injectable({ providedIn: 'root' }) export class AuthService{ }
-
Add specific response to the generic post request as follow
// <AuthResponseData> included in the post method // by default post request is generic // by adding <AuthResponseData> we are specifying the response of the post request signUp(email:string, password:string){ return this.http.post<AuthResponseData>(this.signUpURL, {email:email, password:password, returnSecureToken:true }) }
-
Inject the
AuthService
intoauth.component.ts
export class AuthComponent{ constructor(private authSer:AuthService){} }
-
Obtain the username and password in
onSubmit(form:NgForm)
method ofapp.component.ts
onSubmit(form: NgForm){ const email = form.value.email; const password = form.value.password; }
-
Check if
isLogingMode
property is set?onSubmit(form: NgForm){ const email = form.value.email; const password = form.value.password; if (this.isLoginMode) { // signin logic } else { // sign up logic implemented in next point } }
-
call the
signUp()
method ofauth.service.ts
and subscribe to it// following code will be reside in else section of above point this.authSer.signUp(email, password).subscribe( responseData => { // response of the request obtained as an arrow func console.log(responseData); // logging the obtained responseData }, error => { // error response obtain through arrow function console.log(error); // logging the obtained error response } );
-
Loading spinner will be appear when app is performing request to the firebase
-
Loading spinner source: https://loading.io/css/
-
create loading-spinner component
shared |---- loading-spinner |-------- loading-spinner.css |-------- loading-spinner.ts
-
Content of
loading-spinner.ts
import { Component } from "@angular/core"; @Component({ selector: 'app-loading-spinner', template: '<div class="lds-facebook"><div></div><div></div><div></div></div>', styleUrls: ['./loading-spinner.css'] }) export class LoadingSpinnerComponent{}
-
Register loading spinner component in the
app.module.ts
import { LoadingSpinnerComponent } from './shared/loading-spinner/loading-spinner'; @NgModule({ declarations: [LoadingSpinnerComponent] })
-
Add new property
isLoading : boolean
inauth.component.ts
export class AuthComponent{ isLoading = false; onSubmit(form: NgForm){ const email = form.value.email; const password = form.value.password; this.isLoading = true; // loading started if (this.isLoginMode) { // signin logic } else { // sign up logic this.authSer.signUp(email, password).subscribe( responseData => { console.log(responseData); this.isLoading = false; // loading ends }, error => { console.log(error); this.isLoading = false; // loading ends } ); } form.reset(); } }
-
Add the loading-spinner component in
auth.component.html
<div class="row"> <div class="col-xs-12 col-md-6 col-md-offset"> <div *ngIf = "isLoading" style="text-align: center;"> <app-loading-spinner></app-loading-spinner> </div> <form *ngIf = "!isLoading"> <!--load the form if loading is false--> </form> </div> </div>
-
Declaring property name
error : string
in theauth.component.ts
export class AuthComponent{ error : string = null; }
-
Bind the aforementioned property with
auth.component.html
templet<div class="row"> <div class="col-xs-12 col-md-6 col-md-offset"> <div class="alert alert-danger" *ngIf = "error"> <p> {{ error }}</p> </div> </div> </div>
-
Extract the error message in
auth.service.ts
// pipe rxjs operator is applied to the post request of signup method .pipe(catchError( errorRes => { let errorMessage = 'An unknown error occured'; // default error message if(!errorRes.error || !errorRes.error.error){ return throwError(errorMessage); // if unknown error occures throw this } switch (errorRes.error.error.message){ case 'EMAIL_EXISTS': // if this error occures errorMessage = 'This email exists already' // change the errorMessage } return throwError(errorMessage); // throw error here if known error occured } ));
-
obtain the error message in
auth.component.ts
this.authSer.signUp(email, password).subscribe( responseData => { console.log(responseData); this.isLoading = false; }, errorMessage => { // assign errorMessage to the previously declared error property this.error = errorMessage; this.isLoading = false; } );
-
Obtain URL for Login from firebase API doc
-
create
Login(email:string, password:string)
inauth.service.ts
-
perform post request in login method
// only prepare the post observable but no subscribe here return the observable // post request needs email password and returnSecureToken as data login(email:string, password:string){ return this.http.post<AuthResponseData>(this.loginURL, {email:email, password:password, returnSecureToken: true }); }
-
Add
registered
as an optional property inAuthResponseData
interface// registered property is yield by Signin request but // this property is not yield by Signup request // check firebase API doc for list of properties in response data interface AuthResponseData { idToken : string; email : string; refreshToken : string; expiresIn : string; localId : string; registered? : Boolean // optional property }
-
modify the
AuthResponseData
interface inauth.service.ts
// make it export export interface AuthResponseData {}
-
separate the subscription code from login and sign up mode
// import the Authresponse data from auth.service.ts import { AuthService, AuthResponseData } from "./auth.service"; // create the observable variable as follow let authObservable : Observable<AuthResponseData>; // subscribe to this observable in onSubmit method outside if else block onSubmit(form: NgForm){ const email = form.value.email; const password = form.value.password; // following variable will hold the observable let authObservable : Observable<AuthResponseData>; this.isLoading = true; if (this.isLoginMode) { // signin logic // assigning the login observable to the authObservable variable authObservable = this.authSer.login(email, password); } else { // sign up logic // assigning signup observable to the authObservable variable authObservable = this.authSer.signUp(email, password); } // execute the authObservable here after // appropreate assignment of observable authObservable.subscribe( responseData => { console.log(responseData); this.isLoading = false; }, errorMessage => { this.error = errorMessage; this.isLoading = false; } ); form.reset(); }
-
Centralizing the error handling by implementing
handleError()
method inauth.service.ts
private handleError(errorResponse : HttpErrorResponse){ // errorMessage property will hold the error message obtained from request let errorMessage = 'An unknown error occured'; // default message // check if errorResponse is empty? if(!errorResponse.error || !errorResponse.error.error){ return throwError(errorMessage); // then send the default error message } // if errorResponse is not empty then go to the switch case switch (errorResponse.error.error.message){ // sign up related errors case 'EMAIL_EXISTS': // if errorResponse is this then errorMessage = 'This email exists already'; // put this value in errorMessage break; case 'TOO_MANY_ATTEMPTS_TRY_LATER': errorMessage = 'Too many attempts try again later'; break; // sign in related errors case 'INVALID_PASSWORD': errorMessage = 'Invalid Password'; break; case 'EMAIL_NOT_FOUND': errorMessage = 'This email does not found'; break; } return throwError(errorMessage); // throw error message before leaving this method }
-
Modify the
signUp
andlogin
methods ofauth.service.ts
signUp(email:string, password:string){ return this.http.post<AuthResponseData>(this.signUpURL...) .pipe(catchError(this.handleError)); // calling the handleError method } login(email:string, password:string){ return this.http.post<AuthResponseData>(this.loginURL...) .pipe(catchError(this.handleError)); // calling the handleError method } // this error eventually will end in authObservable subscription block of auth.component.ts
-
auth.component.ts
is untouched
-
Create the user model
// constructor automatically create the instance of property and assign it to it export class User { constructor(public email:string, public id : string, private _token: string, private _tokenExpirationDate : Date){} }
-
Add get token method in the model
get token(){ // if token expired then return null if(!this._tokenExpirationDate || new Date() > this._tokenExpirationDate){ return null; } // or return the token return this._token; }
-
Create the user subject in the
auth.service.ts
userx = new Subject<User>();
-
create the
userAuthentication
methodprivate userAuthentication(email:string, userId:string, token: string, expiresIn: number){ const expirationDate = new Date( new Date().getTime() + expiresIn * 1000); const user = new User(email, userId, token, expirationDate); this.user.next(user); }
-
execute the above implemented method in signup and login under pipe under tap
// we can tap the response data from post request inside pipe // to create the user object userAuthentication method is used signUp(email:string, password:string){ return this.http.post<AuthResponseData>(this.signUpURL...) .pipe( catchError(this.handleError) ,tap( resData => { this.userAuthentication(resData.email, resData.localId, resData.idToken, +resData.expiresIn); } )); } login(email:string, password:string){ // only prepare the observable but no subscribe here return this.http.post<AuthResponseData>(this.loginURL...) .pipe( catchError(this.handleError) ,tap( resData => { this.userAuthentication(resData.email, resData.localId, resData.idToken, +resData.expiresIn); } )); }
-
Inject router in the
auth.component.ts
constructor(private authSer:AuthService, private router:Router){}
-
Redirect to the recipe component upon successful login
// perform this task in authObservable's subscribe method onSubmit(form: NgForm){ ... authObservable.subscribe( responseData => { console.log(responseData); this.isLoading = false; this.router.navigate(['/recipes']); // navigate to the recipe component }, errorMessage => { this.error = errorMessage; console.log(errorMessage); this.isLoading = false; } ); }
-
Inject
auth.service.ts
inheader.component.ts
and declareisAuthenticated
property// this property will be used in html to control the header isAuthenticated = false; constructor(private remote:DataStorageService, private authService : AuthService){ }
-
Subscribe to the
user
subject ofAuthService
and at the end unsubscribeprivate userSub : Subscription; // a property which holds the subscription ngOnInit(){ this.userSub = this.authService.user.subscribe( user => { // is authenticated = true if we receive user object from subscription // is authenticated = false if we dont receive user object[null] from subscription this.isAuthenticated = !user ? false :true; } ); } ngOnDestroy(){ this.userSub.unsubscribe(); }
-
Update the
header.component.html
component<div class="navbar-collapse"> <ul class = "nav navbar-nav"> <li *ngIf = "isAuthenticated"> <a>Recipes</a> </li> <li> <a>Shopping List</a> </li> <li *ngIf = "!isAuthenticated"> <a>Authentication</a> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li *ngIf = "isAuthenticated"> <a>Logout</a> </li> <li class="dropdown" *ngIf = "isAuthenticated"> <a>Manage</a> </li> </ul> </div>
-
Attaching token to the outgoing request [fetch]
-
Inject
authService
indata-storage.service.ts
home ofstoreRecipe()
andfetchRecipe()
methodsconstructor(private http: HttpClient, private authService : AuthService){ }
-
Change the
user
subject inauth.service.ts
// we can now user event after it got emitted [at any time] user = new BehaviorSubject<User>(null);
-
Modify the
fetchRecipe()
ofdata-storage.service.ts
[exhaustMap]fetchRecipes(){ return this.authService.user.pipe(take(1), exhaustMap(user => { // it will subscribe the user get the data and unsubscribe it return this.http .get<Recipe[]>(this.remoteUrl, { params: new HttpParams().set('auth', user.token) } ); }) ,map(recipes =>{ return recipes.map(recipe => { return {...recipe, ingredients: recipe.ingredients ? recipe.ingredients:[] }; }); }) ,tap(recipes => { this.recipeService.setRecipes(recipes); }) ) }
-
Create
auth-interceptor.ts
inauth
folder@Injectable() export class AuthIntercepterService implements HttpInterceptor{ constructor(private authService: AuthService){} // injecting authService dependency // every class which implements HttpInterceptor must have following method in it intercept(){} }
-
Implement
Intercept()
[Intercepter is executed before the request is being made]// param1 : HttpRequest [req] // param2 : Httphandler [next] intercept(req:HttpRequest<any>, next: HttpHandler){ // will return the modified request // this modified request will have user credentials // idea is to authenticate the user before peroforming any post or get req. }
-
Intercept()
codeintercept(req:HttpRequest<any>, next: HttpHandler){ return this.authService.user.pipe(take(1), exhaustMap(user => { // if there is no user then // simply return the req. obtained from paramter of this intercept method1 if(!user){ return next.handle(req); } // make the clone of inputed req. // then add the Http authentication param to that cloned req. const modifiedReq = req.clone({ params: new HttpParams().set('auth',user.token) }); // return the modified request which has actual req. as well as user auth return next.handle(modifiedReq); })); }
-
Provide this interceptor in
app.module.ts
@NgModule({ providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthIntercepterService, multi: true }] })
-
Modify the
data-storage.service.ts
specificallyfetchRecipes()
fetchRecipes(){ return this.http.get<Recipe[]>(this.remoteUrl).pipe( map(recipes =>{ // this is rxjs map operator return recipes.map(recipe => { return {...recipe, ingredients: recipe.ingredients ? recipe.ingredients:[]}; } ); // this map is javascript array method } ) ,tap(recipes => { this.recipeService.setRecipes(recipes); } ) ) } // storeRecipes() will remain same we are just itroducing the itercept so previously modified // fetch method will be remodified
-
Implement
logout()
inauth.service.ts
logout(){ this.user.next(null); // make the user object null } // user object is situated in the same service
-
Call this above implemented logout method in
header.component.ts
onLogout(){ this.authService.logout(); }
-
Add click listener to above implemented method in
header.component.ts
<li *ngIf = "isAuthenticated"> <a style = "cursor:pointer;" (click) = "onLogout()">Logout</a> </li>
-
Perform redirection of the user when logout is clicked
// redirection will be performed in logout() of the auth.service.ts // because logout will be performed from different locations logout(){ this.user.next(null); // make the user object null this.router.navigate(['/auth']); }
-
When page reload happens, application restarts and old user credentials are wiped out
-
Whenever we reload the page we need to login again to access the app
-
In this section an auto login will be implemented
-
So that even after restart user remains logged in
-
User can logout using Logout button or when user token get expired
-
Idea is to store the user data into persistent storage [local storage]
-
Store the logged user in the local storage as follow
// this method is already implemented in previous sections private userAuthentication(){ localStorage.setItem('userData', JSON.stringify(user)); } // convert the user object into plain text using JSON.stringify() method
-
Implement
autoLogin()
inauth.service.ts
autoLogin(){ // obtain the user data from local storage const userData:{ email:string; id:string; _token:string; _tokenExpirationDate:string; } = JSON.parse(localStorage.getItem('userData')); // JSON.parse will converte text into object // userData will hold the user which is stored in the local storage // if there is no user data available in local storage // then dont perform autoLogin if(!userData){ return; } // create the object of user which is already in local storage const LoadedUser = new User(userData.email, userData.id, userData._token, new Date(userData._tokenExpirationDate) ); // check if loaded user has valid token // if yes then publish loaded user over the user subject // user subject is implemented in the same service if(LoadedUser.token){ this.user.next(LoadedUser); } }
-
Add
autoLogin()
into thengOnInit
hook ofapp.component.ts
export class AppComponent implements OnInit { constructor(private authSer: AuthService){} ngOnInit(){ this.authSer.autoLogin(); } }
-
Even after log out click, we can still logged in by refreshing the application
-
Because user data is stored in local storage
-
This user data does not delete even after refreshing the page
-
And we directly do the login even without authentication upon reloading
-
Also when user token expires, we are still logged in
-
Clear the user data upon the execution of
logout()
method ofauth.service.ts
logout(){ // clear the user data locally stored localStorage.removeItem('userData'); // follow section 7 for this if(this.tokenExpirationTimer){ clearTimeout(this.tokenExpirationTimer); } this.tokenExpirationTimer = null; }
-
Implement
autoLogout()
inauth.service.ts
// create the timer expiration property // this property will be used to check whether token timeout happened or not private tokenExpirationTimer: any; // in autoLogout() we will set the above declared timer // and once that timer expires perform logout method autoLogout(expirationDuration : number){ this.tokenExpirationTimer = setTimeout(() =>{ this.logout(); },expirationDuration); }
-
Call the
autoLogout()
method inuserAuthentication()
of theauth.service.ts
private userAuthentication(......expiresIn: number){ this.user.next(user); this.autoLogout(expiresIn*1000); // autologout in [expiresIn*1000] duration localStorage.setItem('userData', JSON.stringify(user)); }
-
Call
autoLogout()
inautoLogin()
method of same serviceautoLogin(){ if(LoadedUser.token){ this.user.next(LoadedUser); const expirationDuration = new Date(userData._tokenExpirationDate) .getTime() - new Date().getTime(); this.autoLogout(expirationDuration); } } // basically when we calls autoLogout method, we are just setting the timer and when this timer overflow logout method is called // the value for timer is passed as a parameter to the autologin method
-
The recipes tab is not visible unless user is logged in
-
Although user can navigate to the recipes component via URL
-
Bug: user can navigate to the recipes component without any authentication via URL
-
To avoid user to navigate to recipes section without authentication a guard need to be implemented
-
Create
auth.guard.ts
under auth folder@Injectable({ providedIn: 'root'}) export class AuthGuard implements CanActivate { // import authService and Router constructor(private authService: AuthService, private router: Router) { } //canActivate method }
-
Implement
canActivate()
canActivate(route: ActivatedRouteSnapshot, router: RouterStateSnapshot): // following things can be returned | boolean | UrlTree | Promise<boolean | UrlTree> | Observable<boolean | UrlTree> { return this.authService.user.pipe(take(1),map(user => { const isAuth = !!user; if (isAuth) { return true; } return this.router.createUrlTree(['/auth']); })); }
-
Apply guard on the recipe route in
app-routing.module.ts
const appRoutes: Routes = [ { path: 'recipes', component: RecipesComponent, canActivate: [AuthGuard], children: [...] } ]
-
Take(1) in
canActivate()
method ofAuth.guard.ts