User Authentication with Angular + AngularFire
Scott VanderWeide
Posted on May 23, 2020
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, 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.
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.)
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, 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.)
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:
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:
<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:
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:
<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:
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.
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.
Change the rules to match the following and publish them to your project:
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:
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?
Posted on May 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.