User Authentication with Angular + AngularFire
Tags: angular, typescript, tutorial, webdev
Published: 2020-05-23 16:43:15

Planning to build a super-cutting-edge web application sometime soon? *Not a bad idea*. Do any of your prototype stories involve user authentication? If you’re not afraid of doing things *The Angular Way™*, this is the guide for you. 

Firebase is a cloud database platform that offers developers a lot of neat functionalities beyond acting as a simple database. Among said functionalities is user authentication. User authentication can be easily implemented in an Angular application with [AngularFire](https://github.com/angular/angularfire), which is the official Angular library for Firebase.

**Reader beware:**
**1.** This guide assumes you have already created a Firebase project with a web application added to it.
**2.** This guide assumes you have already created a new Angular application with routing enabled.
**3.** This guide assumes the working directory of your terminal or command line is set to the containing folder of your Angular project. (I don't think I should have to clarify this, but you never know.)
**4.** This guide demonstrates implementing email and password authentication, however Firebase offers [more authentication methods](https://firebase.google.com/docs/auth#key_capabilities).  

Firstly, add AngularFire to your Angular application. This can be accomplished with the Angular CLI by using the command **ng add @angular/fire**. Alternatively, install the necessary NodeJS packages with npm; use the command **npm install --save @angular/fire @firebase/app firebase**. 

For the sake of not having to refer back to this file later in this guide, your Angular application’s **app.module.ts** file should have the following imports: (Ignore the declared components, we’re getting to that.)

```typescript
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { AngularFireModule } from '@angular/fire';
import { environment } from 'src/environments/environment';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SignInComponent } from './sign-in/sign-in.component';
import { TaskListComponent } from './task-list/task-list.component';
@NgModule({
	declarations: [ AppComponent, SignInComponent, TaskListComponent ],
	imports: [
		FormsModule,
		ReactiveFormsModule,
		BrowserModule,
		AppRoutingModule,
		AngularFireModule.initializeApp(environment.firebaseConfig)
	],
	providers: [],
	bootstrap: [ AppComponent ]
})
export class AppModule {}
```

Notice the last imported module is being initialized with the [Firebase configuration JSON object](https://support.google.com/firebase/answer/7015592?hl=en), which you should add to your Angular application’s **environment.ts** file. (It should be added to the **environment.prod.ts** file as well should you wish to use this app in production mode.)

In addition, the following is what your Angular application's **app-routing.module.ts** file should look like: (Again, we'll be creating these components shortly.)

```typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { TaskListComponent } from './task-list/task-list.component';
import { SignInComponent } from './sign-in/sign-in.component';

const routes: Routes = [
	{ path: 'tasks', component: TaskListComponent },
	{ path: 'signin', component: SignInComponent },
	{ path: '**', redirectTo: 'signin', pathMatch: 'full' }
];

@NgModule({
	imports: [ RouterModule.forRoot(routes) ],
	exports: [ RouterModule ]
})
export class AppRoutingModule {}
```
Firebase offers us a few different cloud-based database solutions, however this guide will only demonstrate implementing the **Firestore** platform. 

Create a new Angular service for integrating the Firestore platform into your application. This can be achieved with the Angular CLI by using the command **ng generate service services/firestore**. 

Note that we are also creating a folder called services within our *src/app* folder. This is merely a file-structuring preference.

Moving forward, your Angular application’s **firebase.service.ts** file should look like this:

```typescript
mport { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { AngularFireAuth } from '@angular/fire/auth';
import { Observable } from 'rxjs';
@Injectable({
	providedIn: 'root'
})
export class FirebaseService {
	public signedIn: Observable<any>;

	constructor(public fs: AngularFirestore, public auth: AngularFireAuth) {
		this.signedIn = new Observable((subscriber) => {
			this.auth.onAuthStateChanged(subscriber);
		});
	}

	async signIn(email: string, password: string) {
		try {
			if (!email || !password) throw new Error('Invalid email and/or password');
			await this.auth.signInWithEmailAndPassword(email, password);
			return true;
		} catch (error) {
			console.log('Sign in failed', error);
			return false;
		}
	}

	async signOut() {
		try {
			await this.auth.signOut();
			return true;
		} catch (error) {
			console.log('Sign out failed', error);
			return false;
		}
	}

	getTasks() {
		return this.fs.collection('tasks').valueChanges({ idField: 'id' });
	}

	async deleteTask(id: string) {
		try {
			if (!id) throw new Error('Invalid ID or data');
			await this.fs.collection('tasks').doc(id).delete();
			return true;
		} catch (error) {
			console.log(error);
			return false;
		}
	}

	async addTask(data: any) {
		try {
			if (!data) throw new Error('Invalid data');
			data.uid = (await this.auth.currentUser).uid;
			await this.fs.collection('tasks').add(data);
			return true;
		} catch (error) {
			console.log(error);
			return true;
		}
	}
}
```

*Some important points to make about what's going on here:*
**1.** Take a look at the constructor method of the **FirebaseService** class. We need to expose both the Firestore API and the Firebase authentication API to our newly-created service. This can be achieved by injecting the **AngularFirestore** service and the **AngularFireAuth** service. 
**2.** Just above the constructor, an **Observable**, called **signedIn**, is being declared. This will pass its observers over to the **AngularFireAuth** service via the **onAuthStateChange** method, which will notify said observers of user authentication changes. (e.g. when a user signs in or signs out)
**3.** Next there are class methods declared for both signing a user in with email and password, and signing the current user out. These methods are called **signIn** and **signOut**, respectively.

Now that our application has a service which provides a means of managing and monitoring the currently-signed-in user, we can begin to integrate it into one of our Angular components. Create a new Angular component and call it **task-list**. (Yes, this is a cleverly disguised task list tutorial. *How exciting.*) This can be achieved with the command **ng generate component task-list**. 

Moving forward, the code in the newly-created component’s **task-list.component.html** file should be changed to look like this:

```typescript
<h1>Task List</h1>
<ul>
    <li *ngFor="let data of fsData">
        <p style="display: inline;">Task: {{data.task}}</p>
        <button (click)="removeTask(data)" >remove task</button>
    </li>
</ul>
<hr>
<h2>Add Task</h2>
    <form [formGroup]="taskForm" (ngSubmit)="addTask(taskForm)">
        <label for="task">Task</label>
        <input type="text" name="task" formControlName="task">
        <button>add</button>
    </form>
    <h4 *ngIf="addFailed" style="color: #f00;">Invalid task. Please try again.</h4>
<hr>
<button (click)="signOut()">sign out</button>
```

The component’s **task-list.component.ts** file should be changed to look like this:

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FirebaseService } from '../services/firebase.service';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';
import { Router } from '@angular/router';
@Component({
	selector: 'app-task-list',
	templateUrl: './task-list.component.html',
	styleUrls: [ './task-list.component.scss' ]
})
export class TaskListComponent implements OnInit, OnDestroy {
	public fsData: Array<any>;
	public taskForm: FormGroup;
	public userAuth: Subscription;
	public taskDataSub: Subscription;
	public addFailed: boolean;
	constructor(public fs: FirebaseService, public fb: FormBuilder, public router: Router) {
		this.addFailed = false;
		this.fsData = new Array();

		this.taskForm = this.fb.group({
			task: new FormControl('', [ Validators.required, Validators.minLength(1) ])
		});
		this.userAuth = this.fs.signedIn.subscribe((user) => {
			if (user) {
				this.getTaskData();
			} else {
				this.router.navigate([ 'signin' ]);
			}
		});
	}

	ngOnInit(): void {}

	ngOnDestroy() {
		if (this.userAuth) this.userAuth.unsubscribe();
		if (this.taskDataSub) this.taskDataSub.unsubscribe();
	}

	async addTask(fg: FormGroup) {
		try {
			console.log(fg.valid, fg.value);
			if (!fg.valid) throw new Error('Invalid form data');
			this.addFailed = false;
			const result = await this.fs.addTask(fg.value);
			if (result) fg.reset();
			else throw new Error('Failed to add task; Something went wrong');
		} catch (error) {
			console.log(error);
			this.addFailed = true;
		}
	}

	async removeTask(task: any) {
		try {
			if (!task) throw new Error('Invalid task');
			const result = await this.fs.deleteTask(task.id);
			if (!result) throw new Error('Failed to remove task');
		} catch (error) {
			console.log(error);
			alert('Failed to remove task; something went wrong.');
		}
	}

	getTaskData() {
		this.taskDataSub = this.fs.getTasks().subscribe((data) => {
			this.fsData = data;
		});
	}

	signOut() {
		this.fs.signOut();
	}
}
```

*Breaking it down:*
**1.** First, the Firestore service needs to be injected into the component. A **FormBuilder** is also injected and used to create a form creating additional tasks.
**2.** In the body of the constructor, a subscription is assigned to the **userAuth** class member. As far as implementing user authorization goes, the callback function passed to this subscription will cause a redirect back to the sign-in page if false-y user data is received from it. (e.g. there is no user currently signed-in.)
**3.** Additionally, there are defined functions for adding/removing tasks from the database that call the appropriate functions previously defined in the **firestore.service.ts** file. These functions are called by corresponding event bindings defined in the component’s template.
**4.** Lastly, there is a function defined for signing the current user out which is also triggered by an event binding defined in the component’s template.

Next, create a new component which will be used to implement a sign-in screen. Achieve this with the command **ng generate component sign-in**. Change the default contents of your sign-in component's template (**sign-in.component.html**) to look like this:

```typescript
<h1>Sign In</h1>
<form [formGroup]="signInForm" (ngSubmit)="signIn(signInForm)">
    <label for="email">Email</label>
    <input type="email" name="email" formControlName="email">
    <label for="password">Password</label>
    <input type="password" name="password" formControlName="password">
    <button>Sign In</button>
</form>
<h4 *ngIf="signInFailed" style="color: #f00;">Sign in failed. Please try again.</h4>
<hr>
```
The newly-created **sign-in.component.ts** file should be edited to contain the following:

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import { FirebaseService } from '../services/firebase.service';
import { Subscription } from 'rxjs';
import { Router } from '@angular/router';

@Component({
	selector: 'app-sign-in',
	templateUrl: './sign-in.component.html',
	styleUrls: [ './sign-in.component.scss' ]
})
export class SignInComponent implements OnInit, OnDestroy {
	public signInForm: FormGroup;
	public signInFailed: boolean;
	public userAuth: Subscription;
	constructor(public fb: FormBuilder, public fs: FirebaseService, public router: Router) {
		this.signInFailed = false;
		this.signInForm = this.fb.group({
			email: new FormControl('', [ Validators.required, Validators.email ]),
			password: new FormControl('', [ Validators.required, Validators.minLength(6) ])
		});
		this.userAuth = this.fs.signedIn.subscribe((user) => {
			if (user) this.router.navigate([ 'tasks' ]);
		});
	}

	ngOnInit(): void {}

	ngOnDestroy(): void {
		if (this.userAuth) this.userAuth.unsubscribe();
	}

	async signIn(fg: FormGroup) {
		try {
			this.signInFailed = false;
			if (!fg.valid) throw new Error('Invalid sign-in credentials');
			const result = await this.fs.signIn(fg.value.email, fg.value.password);
			console.log('that tickles', result);
			if (result) this.router.navigate([ 'tasks' ]);
			else throw new Error('Sign-in failed');
		} catch (error) {
			console.log(error);
			this.signInFailed = true;
		}
	}
}
```

*Things to note about this component:*
**1.** As can be seen in the template, the component has a form which will be used to enter the necessary sign-in credentials. The component class creates the corresponding **FormGroup** declared as a class member, called **signInForm**. 
**2.** The component class has a few services injected into it: the previously-created **Firestore** service, for signing in; a **FormBuilder**, for building the sign in form; and a **Router**, for navigation after a user successfully signs in.
**3.** At the bottom of the component class, there is a function defined for making a sign-in attempt, which is triggered by a form-submission event defined in the component’s template.
**4.** Note the validators used in creating the sign-in form and the validation check on said form in the sign-in method of the component class.

The demo application should be ready to go! Use the command **ng serve** to build the app and serve it locally. The application can now be accessed via the local machine’s web browser. *(http://localhost:4200/)*

Unfortunately the application is entirely useless, because there are no users being managed by the Firebase project. To change this, go to your Firebase console and select the **authentication** tab. On the resulting screen, email and password sign-ins must be enabled. Go to the **sign-in method** tab and enable email/password.

![Enabling email/password sign-ins](https://dev-to-uploads.s3.amazonaws.com/i/ys52u0xxk4tn7lod7013.PNG)

Next, go to the users tab (it's right next to the authentication tab) and create a new user. You may now log into the demo application with these newly registered credentials!

Since you made it this far, here is a little something extra beyond simple user authentication: the specific implementation of the authorization API used in the demo application is the only thing preventing users from seeing or accessing the data. Improved data-security can be achieved using custom security rules. 

Go to the database tab in your Firebase project’s console and select **Firestore**, if it isn’t already selected. Next go to the rules tab on the resulting screen. 

![Security rules tab](https://dev-to-uploads.s3.amazonaws.com/i/h9k81ebgx17kooq8mls4.PNG)

Change the rules to match the following and publish them to your project:

```typescript
rules_version = '2';
service cloud.firestore {
    match /database/{database}/documents {
        match /{document=**} {
            allow read: if true;
            allow write, update, create, delete: if isAuth(request);
        }

        function isAuth(req) {
            return req.auth,uid != null;
        }
    }
}
```

I won't go into too much detail about what's going on here, but basically these rules make it so that all documents in the database require the request to come from an authorized user in order for the request to be allowed to read or write anything within the database.

You can test your fancy new security rules by altering the **task-list** component's **userAuth** subscription callback so that it doesn't redirect to the **sign-in** component when no user is logged in:

```typescript
this.userAuth = this.fs.signedIn.subscribe((user) => {
    // if (user) {
    //     this.getTaskData();
    // } else {
    //     this.router.navigate([ 'signin' ]);
    // }
    this.getTaskData();
});
```

If you go to the task list page and open your web browser's developer console, you will see an *invalid permissions* error waiting for you. 

Now you've got user authentication implemented in your Angular application and some added security in your Firestore database! Pretty neat, eh?