In this article, we’ll add authentication to our Angular application and learn how we can protect sections from our application from unauthorized access.
This article is part 5 of the SitePoint Angular 2+ Tutorial on how to create a CRUD App with the Angular CLI.
- Part 0 — The Ultimate Angular CLI Reference Guide
- Part 1 — Getting our first version of the Todo application up and running
- Part 2 — Creating separate components to display a list of todos and a single todo
- Part 3 — Update the Todo service to communicate with a REST API
- Part 4 — Use Angular router to resolve data
- Part 5 — Add authentication to protect private content
- Part 6 — How to Update Angular Projects to the latest version.
In part 1 we learned how to get our Todo application up and running and deploy it to GitHub pages. This worked just fine but, unfortunately, the whole app was crammed into a single component.
In part 2 we examined a more modular component architecture and learned how to break this single component into a structured tree of smaller components that are easier to understand, reuse and maintain.
In part 3 we updated our application to communicate with a REST API backend using RxJS and Angular’s HTTP service.
In part 4, we introduced Angular Router and learned how the router updates our application when the browser URL changes and how we can use the router to resolve data from our backend API.
Don’t worry! You don’t need to have followed part 1, 2, 3 or 4 of this tutorial, for five to make sense. You can simply grab a copy of our repo, check out the code from part 4, and use that as a starting point. This is explained in more detail below.
Up and Running
Make sure you have the latest version of the Angular CLI installed. If you don’t, you can install it with the following command:
npm install -g @angular/cli@latest
If you need to remove a previous version of the Angular CLI, you can run this:
npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/cli@latest
After that, you’ll need a copy of the code from part 4. This is available at https://github.com/sitepoint-editors/angular-todo-app. Each article in this series has a corresponding tag in the repository so you can switch back and forth between the different states of the application.
The code that we ended with in part 4 and that we start with in this article is tagged as part-4. The code that we end this article with is tagged as part-5.
You can think of tags like an alias to a specific commit id. You can switch between them using git checkout
. You can read more on that here.
So, to get up and running (with the latest version of the Angular CLI installed) we would do this:
git clone git@github.com:sitepoint-editors/angular-todo-app.git
cd angular-todo-app
git checkout part-4
npm install
ng serve
Then visit http://localhost:4200/. If all’s well, you should see the working Todo app.
Plan of Attack
In this article, we will:
- set up a backend to authenticate against
- add a sign-in method to our existing
ApiService
- set up an authentication service to handle authentication logic
- set up a session service to store session data
- create a
SignInComponent
to display a sign-in form - set up a route guard to protect parts of our application from unauthorized access.
By the end of this article, you’ll understand:
- the difference between cookies and tokens
- how to create an
AuthService
to implement authentication logic - how to create a
SessionService
to store session data - how to create a sign-in form using an Angular reactive form
- how to create a route guard to prevent unauthorized access to parts of your application
- how to send a user’s token as an Authorization Header in an HTTP request to your API
- why you should never send your user’s token to a third party.
Our application will look like this:
So, let’s get started!
Authentication Strategy
Server-side web applications typically handle user sessions on the server. They store session details on the server and send the session ID to the browser via a cookie. The browser stores the cookie and automatically sends it to the server with every request. The server then grabs the session ID from the cookie and looks up the corresponding session details from its internal storage (memory, database, etc). The session details remain on the server and are not available in the client.
In contrast, client-side web applications, such as Angular applications, typically manage user sessions in the client. The session data is stored in the client and sent to server when needed. A standardized way to store sessions in the client are JSON Web Tokens, also called JWT tokens. If you’re unfamiliar with how tokens work, check out this simple metaphor to easily understand and remember how token-based authentication works and you’ll never forget again.
If you want to get a deeper understanding of cookies and tokens, make sure to check out Philippe De Ryck’s talk on Cookies versus tokens: a paradoxial choice.
Due to the popularity of JSON Web Tokens in today’s ecosystem, we’ll use a JWT-based authentication strategy.
Setting Up the Backend
Before we can add authentication to our Angular application, we need a back end to authenticate against.
In the previous parts of this series, we use json-server to serve back end data based on the db.json
file in the root of our project.
Luckily, json-server can also be loaded as a node module, allowing us to add custom request handlers.
Let’s start by installing the body-parser npm module, which we’ll need to parse the JSON in our HTTP requests:
$ npm install --save body-parser
Next, we create a new file json-server.js
in the root of our project:
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('db.json');
const middlewares = jsonServer.defaults();
const bodyParser = require('body-parser');
// Sample JWT token for demo purposes
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' +
'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg';
// Use default middlewares (CORS, static, etc)
server.use(middlewares);
// Make sure JSON bodies are parsed correctly
server.use(bodyParser.json());
// Handle sign-in requests
server.post('/sign-in', (req, res) => {
const username = req.body.username;
const password = req.body.password;
if(username === 'demo' && password === 'demo') {
res.json({
name: 'SitePoint Reader',
token: jwtToken
});
}
res.send(422, 'Invalid username and password');
});
// Protect other routes
server.use((req, res, next) => {
if (isAuthorized(req)) {
console.log('Access granted');
next();
} else {
console.log('Access denied, invalid JWT');
res.sendStatus(401);
}
});
// API routes
server.use(router);
// Start server
server.listen(3000, () => {
console.log('JSON Server is running');
});
// Check whether request is allowed
function isAuthorized(req) {
let bearer = req.get('Authorization');
if (bearer === 'Bearer ' + jwtToken) {
return true;
}
return false;
}
This article is not meant to be a tutorial on json-server, but let’s quickly have a look at what’s happening.
First we import all json-server machinery:
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('db.json');
const middlewares = jsonServer.defaults();
const bodyParser = require('body-parser');
In a real-world application, we would dynamically generate a JWT token when a user authenticates, but for the purpose of this demo, we define a JWT token statically:
// Sample JWT token for demo purposes
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' +
'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg';
Next, we configure json-server to run its own default middlewares:
// Use default middlewares (CORS, static, etc)
server.use(middlewares);
and to parse incoming JSON requests properly:
// Make sure JSON bodies are parsed correctly
server.use(bodyParser.json());
Json-server’s default middlewares are request handler functions that deal with static files, CORS, etc. For more detailed information, check out the documentation.
We then define a request handler for sign-in requests:
// Handle sign-in requests
server.post('/sign-in', (req, res) => {
const username = req.body.username;
const password = req.body.password;
if(username === 'demo' && password === 'demo') {
res.json({
name: 'SitePoint Reader',
token: jwtToken
});
}
res.send(422, 'Invalid username and password');
});
We tell json-server to listen for HTTP POST requests on /sign-in
. If the request contains a username field with a value of demo
and password field with a value of demo
, we return an object with the JWT token. If not, we send an HTTP 422 response to indicate that the username and password are invalid.
In addition, we also tell json-server to authorize all other requests:
// Protect other routes
server.use((req, res, next) => {
if (isAuthorized(req)) {
console.log('Access granted');
next();
} else {
console.log('Access denied, invalid JWT');
res.sendStatus(401);
}
});
// Check whether request is allowed
function isAuthorized(req) {
let bearer = req.get('Authorization');
if (bearer === 'Bearer ' + jwtToken) {
return true;
}
return false;
}
If the client’s HTTP request contains an Authorization header with the JWT token, we grant access. If not, we deny access and send an HTTP 401 response.
Finally, we tell json-server to load the API routes from db.json
and start the server:
// API routes
server.use(router);
// Start server
server.listen(3000, () => {
console.log('JSON Server is running');
});
To start our new back end, we run:
$ node json-server.js
For our convenience, let’s update the json-server
script in package.json
:
"json-server": "node json-server.js"
Now we can run:
$ npm run json-server
> todo-app@0.0.0 json-server /Users/jvandemo/Projects/sitepoint-editors/angular-todo-app
> node json-server.js
JSON Server is running
And voila, we have our own API server with authentication running.
Time to dig into the Angular side.
Adding Authentication Logic to our API Service
Now that we have an API endpoint to authenticate against, let’s add a new method to our ApiService
to perform an authentication request:
@Injectable()
export class ApiService {
constructor(
private http: Http
) {
}
public signIn(username: string, password: string) {
return this.http
.post(API_URL + '/sign-in', {
username,
password
})
.map(response => response.json())
.catch(this.handleError);
}
// ...
}
When called, the signIn()
method performs an HTTP POST request to our new /sign-in
API endpoint, including the username and password in the request body.
If you’re not familiar with Angular’s built-in HTTP service, make sure to read Part 3 — Update the Todo service to communicate with a REST API.
Creating a Session Service
Now that we have an API method to authenticate against our back end, we need a mechanism to store the session data we receive from the API, namely the name
and token
.
Because the data will be unique across our entire application, we’ll store it in a service called SessionService
.
So let’s generate our new SessionService:
$ ng generate service session --module app.module.ts
create src/app/session.service.spec.ts
create src/app/session.service.ts
update src/app/app.module.ts
The --module app.module.ts
part tells Angular CLI to automatically register our new service as a provider in AppModule
so that we don’t have to register it manually. Registering a service as a provider is needed so that the Angular dependency injector can instantiate it when needed. If you’re not familiar with the Angular dependency injection system, make sure the check out the official documentation.
Open up src/app/session.service.ts
and add the following code:
import { Injectable } from '@angular/core';
@Injectable()
export class SessionService {
public accessToken: string;
public name: string;
constructor() {
}
public destroy(): void {
this.accessToken = null;
this.name = null;
}
}
We keep things very simple. We define a property to store the user’s API access token and a property to store the user’s name.
We also add a method destroy()
to reset all data in case we wish to sign out the current user.
Notice how SessionService
is not aware of any authentication logic. It’s only responsible for storing session data.
We’ll create a separate AuthService
to implement the actual authentication logic.
Creating an Authentication Service
Putting the authentication logic in a separate service promotes a nice separation of concern between the authentication process and the storage of session data.
This ensures that we don’t have to change the SessionService
if the authentication flow changes and allows us to easily mock session data in unit tests.
So let’s create a service called AuthService
:
$ ng generate service auth --module app.module.ts
create src/app/auth.service.spec.ts
create src/app/auth.service.ts
update src/app/app.module.ts
Open src/app/auth.service.ts
and add the following code:
import { Injectable } from '@angular/core';
import { SessionService } from './session.service';
@Injectable()
export class AuthService {
constructor(
private session: SessionService,
) {
}
public isSignedIn() {
return !!this.session.accessToken;
}
public doSignOut() {
this.session.destroy();
}
public doSignIn(accessToken: string, name: string) {
if ((!accessToken) || (!name)) {
return;
}
this.session.accessToken = accessToken;
this.session.name = name;
}
}
We inject the SessionService
and add a few methods:
isSignedIn()
: returns whether or not the user is signed indoSignOut()
: signs out the user by clearing the session datadoSignIn()
: signs in the user by storing the session data.
Again, notice how the authentication logic is defined in AuthService
, while SessionService
is used to store the actual session data.
Now that we have our authentication service in place, let’s create a sign-in page with an authentication form.
Creating a Sign-in Page
Let’s create a SignInComponent
using Angular CLI:
$ ng generate component sign-in
create src/app/sign-in/sign-in.component.css
create src/app/sign-in/sign-in.component.html
create src/app/sign-in/sign-in.component.spec.ts
create src/app/sign-in/sign-in.component.ts
update src/app/app.module.ts
Our sign-in form is going to be an Angular reactive form, so we must import ReactiveFormsModule
in our application module in src/app/app.module.ts
:
// ...
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
// ...
],
imports: [
// ...
ReactiveFormsModule
],
providers: [
// ...
],
bootstrap: [AppComponent]
})
export class AppModule {
}
Next, we add our TypeScript code to src/app/sign-in/sign-in.component.ts
:
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../api.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-sign-in',
templateUrl: './sign-in.component.html',
styleUrls: ['./sign-in.component.css']
})
export class SignInComponent implements OnInit {
public frm: FormGroup;
public isBusy = false;
public hasFailed = false;
public showInputErrors = false;
constructor(
private api: ApiService,
private auth: AuthService,
private fb: FormBuilder,
private router: Router
) {
this.frm = fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
ngOnInit() {
}
public doSignIn() {
// Make sure form values are valid
if (this.frm.invalid) {
this.showInputErrors = true;
return;
}
// Reset status
this.isBusy = true;
this.hasFailed = false;
// Grab values from form
const username = this.frm.get('username').value;
const password = this.frm.get('password').value;
// Submit request to API
this.api
.signIn(username, password)
.subscribe(
(response) => {
this.auth.doSignIn(
response.token,
response.name
);
this.router.navigate(['todos']);
},
(error) => {
this.isBusy = false;
this.hasFailed = true;
}
);
}
}
First, we instantiate a reactive form in the constructor:
this.frm = fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
We define a reactive form as a form group that contains two form controls — one for the username and one for the password. Both controls have a default value of an empty string ''
, and both controls require a value.
If you’re not familiar with reactive forms, make sure to check out the official documentation on the Angular website.
Next, we define a doSignIn()
method:
public doSignIn() {
// Make sure form values are valid
if (this.frm.invalid) {
this.showInputErrors = true;
return;
}
// Reset status
this.isBusy = true;
this.hasFailed = false;
// Grab values from form
const username = this.frm.get('username').value;
const password = this.frm.get('password').value;
// Submit request to API
this.api
.signIn(username, password)
.subscribe(
(response) => {
this.auth.doSignIn(
response.token,
response.name
);
this.router.navigate(['todos']);
},
(error) => {
this.isBusy = false;
this.hasFailed = true;
}
);
}
First, we check if the form is in a valid state. In the constructor, we configured the username
and password
form controls with Angular’s built-in Validators.required
validator. This marks both controls as required and causes the form to be in an invalid state as soon as one of the form controls has an empty value.
If the form is in an invalid state, we enable showInputErrors
and return without calling the API.
If the form is in a valid state (both username
and password
have a value), we set isBusy
to true and call the the signIn()
method of our ApiService
. We’ll use the isBusy
variable to disable the sign-in button in the view while the API call is being made.
If the API call succeeds, we call the doSignIn()
method of the AuthService
with the token
and name
from the API’s response and navigate the user to the todos
route.
If the API call fails, we mark isBusy
as false
and hasFailed
as true
so we can re-enable the sign-in button and show an error message in the view.
Now that we have our component’s controller in place, let’s add its corresponding view template to src/app/sign-in/sign-in.component.ts
:
<div class="sign-in-wrapper">
<form [formGroup]="frm">
<h1>Todos</h1>
<!-- Username input -->
<input type="text" formControlName="username" placeholder="Your username">
<!-- Username validation message -->
<div
class="input-errors"
*ngIf="(frm.get('username').invalid && frm.get('username').touched) || showInputErrors"
>
<div *ngIf="frm.get('username').hasError('required')">
Please enter your username
</div>
</div>
<!-- Password input -->
<input type="password" formControlName="password" placeholder="Your password">
<!-- Password validation message -->
<div
class="input-errors"
*ngIf="(frm.get('password').invalid && frm.get('password').touched) || showInputErrors"
>
<div *ngIf="frm.get('password').hasError('required')">
Please enter your password
</div>
</div>
<!-- Sign-in error message -->
<div class="sign-in-error" *ngIf="hasFailed">
Invalid username and password.
</div>
<!-- Sing-in button -->
<button (click)="doSignIn()" [disabled]="isBusy">
<ng-template [ngIf]="!isBusy">Sign in</ng-template>
<ng-template [ngIf]="isBusy">Signing in, please wait...</ng-template>
</button>
<!-- Tip -->
<p class="tip">You can sign in with username "demo" and password "demo".</p>
</form>
</div>
First of all, we define a form element and bind it to our reactive form in the controller using [formGroup]="frm"
.
Inside the form, we add an input element for the username and we bind it to its corresponding form control using formControlName="username"
.
Next, we add a validation error to display if the username is invalid. Notice how we can use convenient properties (provided by Angular) such as valid
, invalid
, pristine
, dirty
, untouched
and touched
to narrow down the conditions in which we want to show the validation message. Here, we want to display the validation error when the username is invalid and the user touched the input. In addition, we also want to display the validation error when the user clicks the “Sign in” button and the input has no value.
We repeat the same pattern for the password input and add a general error message to display in case the username and password are not valid credentials.
Finally, we add the submit button:
<button (click)="doSignIn()" [disabled]="isBusy">
<ng-template [ngIf]="!isBusy">Sign in</ng-template>
<ng-template [ngIf]="isBusy">Signing in, please wait...</ng-template>
</button>
When the user clicks the button and an API call is made, we disable the button using [disabled]="isBusy"
and change its text so that the user has a visual indication that the sign-in process is busy.
Now that we have our sign-in page in place, let’s reconfigure our routes in `src/app/app-routing.module.ts
:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SignInComponent } from './sign-in/sign-in.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { TodosComponent } from './todos/todos.component';
import { TodosResolver } from './todos.resolver';
const routes: Routes = [
{
path: '',
redirectTo: 'sign-in',
pathMatch: 'full'
},
{
path: 'sign-in',
component: SignInComponent
},
{
path: 'todos',
component: TodosComponent,
resolve: {
todos: TodosResolver
}
},
{
path: '**',
component: PageNotFoundComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
TodosResolver
]
})
export class AppRoutingModule { }
We define a new sign-in
route:
{
path: 'sign-in',
component: SignInComponent
}
and redirect the default URL to our new sign-in route:
{
path: '',
redirectTo: 'sign-in',
pathMatch: 'full'
}
so that the user is automatically redirected to the sign-in page when loading our application.
If you run:
$ ng serve
and navigate your browser to http://localhost:4200
, you should see:
So far, we’ve already covered a lot:
- set up our back end
- added a method to our ApiService to sign in
- created an AuthService for our authentication logic
- created a SessionService to store our session data
- created a SignInComponent to sign in users.
However, if we sign in with username demo and password demo, the API returns an error 401 when we request the todo items:
In addition, Angular still allows us to navigate our browser directly to http://localhost:4200/todos
, even if we’re not signed in.
To fix both issues, we will now:
- protect the private area of our application from unauthorized access by users who aren’t signed in
- send the user’s token with API requests that require authentication.
Let’s start by securing our application’s private area.
Protecting Our Application’s Private Area From Unauthorized Access
In part 4, we already learned how to use Angular Router to resolve data. In this section, we explore route guards, a feature by Angular Router that allows us to control route navigation.
In essence, a route guard is a function that returns either true
to indicate that routing is permitted or false
to indicate that routing is not permitted. A guard can also return a Promise or an Observable that evaluates to a truthy or falsy value. In that case, the router will wait until the Promise or Observable completes.
There are 4 types of route guards:
CanLoad
: determines whether or not a lazy-loaded module can be loadedCanActivate
: determines whether a route can be activated when the user navigates to the routeCanActivateChild
: determines whether a route can be activated when the user navigates to one of its childrenCanDeactivate
: determines whether a route can be deactivated.
In our application, we wish to make sure the user is signed in when they navigate to the todos
route. Therefore, a CanActivate
guard is a good fit.
Let’s create our guard in a new file called src/app/can-activate-todos.guard.ts
:
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class CanActivateTodosGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {
}
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
if (!this.auth.isSignedIn()) {
this.router.navigate(['/sign-in']);
return false;
}
return true;
}
}
Because our guard is a CanActivate
guard, it needs to implement the CanActivate
interface, provided by @angular/router
.
The CanActivate
interface requires that our guard implements a canActivate()
method:
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
if (!this.auth.isSignedIn()) {
this.router.navigate(['/sign-in']);
return false;
}
return true;
}
The canActivate()
method receives the activated route snapshot and the router state snapshot as arguments, in case we need them to make a smart decision whether or not we wish to permit navigation.
In our example, the logic is very simple. If the user isn’t signed in, we instruct Angular router to navigate the user to the sign-in page and stop further navigation.
In contrast, if the user is signed in, we return true
allowing the user to navigate to the requested route.
Now that we created the route guard, we must tell Angular router to actually use it.
So let’s add it our routing configuration in src/app/app-routing.module.ts
:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SignInComponent } from './sign-in/sign-in.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { TodosComponent } from './todos/todos.component';
import { CanActivateTodosGuard } from './can-activate-todos.guard';
import { TodosResolver } from './todos.resolver';
const routes: Routes = [
{
path: '',
redirectTo: 'sign-in',
pathMatch: 'full'
},
{
path: 'sign-in',
component: SignInComponent
},
{
path: 'todos',
component: TodosComponent,
canActivate: [
CanActivateTodosGuard
],
resolve: {
todos: TodosResolver
}
},
{
path: '**',
component: PageNotFoundComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
CanActivateTodosGuard,
TodosResolver
]
})
export class AppRoutingModule { }
We tell Angular router to use our guard for the todos
route, by adding a canActivate
property to the route:
{
path: 'todos',
component: TodosComponent,
canActivate: [
CanActivateTodosGuard
],
resolve: {
todos: TodosResolver
}
}
The canActivate
property accepts an array of CanActivate
guards so you can easily register multiple guards if your application requires it.
Finally, we need to add CanActivateTodosGuard
as a provider so Angular’s dependency injector can instantiate it when the router asks for it:
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
CanActivateTodosGuard,
TodosResolver
]
})
export class AppRoutingModule { }
With our route guard in place, our application now redirects the user to the sign-in page when they aren’t signed in and try to navigate directly to the todos
route.
In contrast, when the user is signed in, navigation to the todos
route is permitted.
How sweet is that!
Sending The User’s Token With API Requests
So far, our signed-in user can access the todos
route, but the API still refuses to return any todo data because we aren’t sending the user’s token to the API.
So let’s open up src/app/api.service.ts
and tell Angular to send our user’s token in the headers of our HTTP request when neeeded:
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response } from '@angular/http';
import { environment } from 'environments/environment';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { SessionService } from 'app/session.service';
const API_URL = environment.apiUrl;
@Injectable()
export class ApiService {
constructor(
private http: Http,
private session: SessionService
) {
}
public signIn(username: string, password: string) {
return this.http
.post(API_URL + '/sign-in', {
username,
password
})
.map(response => response.json())
.catch(this.handleError);
}
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.map(response => {
const todos = response.json();
return todos.map((todo) => new Todo(todo));
})
.catch(this.handleError);
}
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public deleteTodoById(todoId: number): Observable<null> {
const options = this.getRequestOptions();
return this.http
.delete(API_URL + '/todos/' + todoId, options)
.map(response => null)
.catch(this.handleError);
}
private handleError(error: Response | any) {
console.error('ApiService::handleError', error);
return Observable.throw(error);
}
private getRequestOptions() {
const headers = new Headers({
'Authorization': 'Bearer ' + this.session.accessToken
});
return new RequestOptions({ headers });
}
}
First, we define a convenience method to create our request options:
private getRequestOptions() {
const headers = new Headers({
'Authorization': 'Bearer ' + this.session.accessToken
});
return new RequestOptions({ headers });
}
Next, we update all methods that communicate with an API endpoint that requires authentication:
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.map(response => {
const todos = response.json();
return todos.map((todo) => new Todo(todo));
})
.catch(this.handleError);
}
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public deleteTodoById(todoId: number): Observable<null> {
const options = this.getRequestOptions();
return this.http
.delete(API_URL + '/todos/' + todoId, options)
.map(response => null)
.catch(this.handleError);
}
We instantiate the request options using our convenience helper and pass the options as the second argument in our http call.
WARNING: Be Very Careful!
Always make sure you only send the token to your trusted API. Don’t just blindly send the token with every outgoing HTTP request.
For example: if your application communicates with a third-party API and you accidentally send your user’s token to that third-party API, the third party can use the token to sign in to query your API on behalf of your user. So be very careful and only send the token to trusted parties and only with the requests that require it.
To learn more about the security aspects of token-based authentication, make sure to check out Philippe De Ryck’s talk on Cookies versus tokens: a paradoxial choice.
If you navigate your browser to http://localhost:4200
, you should now be able to sign in with username demo and password demo.
Adding a Sign-out Button to Our TodosComponent
For the sake of completeness, let’s also add a sign-out button under our list of todos.
Let’s open up src/app/todos/todos.component.ts
and add a doSignOut()
method:
import { Component, OnInit } from '@angular/core';
import { TodoDataService } from '../todo-data.service';
import { Todo } from '../todo';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-todos',
templateUrl: './todos.component.html',
styleUrls: ['./todos.component.css']
})
export class TodosComponent implements OnInit {
todos: Todo[] = [];
constructor(
private todoDataService: TodoDataService,
private route: ActivatedRoute,
private auth: AuthService,
private router: Router
) {
}
// ...
doSignOut() {
this.auth.doSignOut();
this.router.navigate(['/sign-in']);
}
}
First, we import the AuthService
and Router
.
Next, we define a doSignOut()
method that signs out the user and navigates the user back to the sign-in page.
Now that we have the logic in place, let’s add the button to our view in src/app/todos/todos.component.html
:
<!-- Todos -->
<section class="todoapp">
<app-todo-list-header
(add)="onAddTodo($event)"
></app-todo-list-header>
<app-todo-list
[todos]="todos"
(toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"
></app-todo-list>
<app-todo-list-footer
[todos]="todos"
></app-todo-list-footer>
</section>
<!-- Sign out button -->
<button (click)="doSignOut()">Sign out</button>
If you refresh your browser and sign in again, you should see:
Clicking the sign-out button triggers the doSignOut()
method in the component controller, sending you back to the sign-in page.
Also, if you sign out and you try to navigate your browser directly to http://localhost:4200/todos
, the route guard detects that you are not signed in and sends you to the sign-in page.
How sweet is that!
We covered a lot in this Angular series, so let’s recap what we’ve learned.
Summary
In the first article, we learned how to:
- initialize our Todo application using Angular CLI
- create a
Todo
class to represent individual todos - create a
TodoDataService
service to create, update and remove todos - use the
AppComponent
component to display the user interface - deploy our application to GitHub pages.
In the second article, we refactored AppComponent
to delegate most of its work to:
- a
TodoListComponent
to display a list of todos - a
TodoListItemComponent
to display a single todo - a
TodoListHeaderComponent
to create a new todo - a
TodoListFooterComponent
to show how many todos are left.
In the third article, we learned how to:
- create a mock REST API back end
- store the API URL as an environment variable
- create an
ApiService
to communicate with the REST API - update the
TodoDataService
to use the newApiService
- update the
AppComponent
to handle asynchronous API calls - create an
ApiMockService
to avoid real HTTP calls when running unit tests.
In the fourth article, we learned:
- why an application may need routing
- what a JavaScript router is
- what Angular Router is, how it works and what it can do for you
- how to set up Angular router and configure routes for our application
- how to tell Angular router where to place components in the DOM
- how to gracefully handle unknown URLs
- how to use a resolver to let Angular router resolve data.
In this fifth article, we learned:
- the difference between cookies and tokens
- how to create an
AuthService
to implement authentication logic - how to create a
SessionService
to store session data - how to create a sign-in form using an Angular reactive form
- how to create a route guard to prevent unauthorized access to parts of your application
- how to send a user’s token as an Authorization Header in an HTTP request to your API
- why you should never send your user’s token to a third party.
Feel free to let us know in the comments below if you were able to make it work or if you have any questions.
All code from this article is available at https://github.com/sitepoint-editors/angular-todo-app/tree/part-5.
Have a great one!
Challenge
In its current state, the session data is lost when the browser refreshes the page.
Can you figure out what’s needed to persist the session data in the browser’s sessionStorage or localStorage?
Let us know what you come up with in the comments below.
Good luck!!