Angular 14 - Facebook Authentication Tutorial with Example
Tutorial built with Angular 14.2.12
Other versions available:
- Angular: Angular 10
- React: React
- Vue: Vue 3, Vue 2
In this tutorial we'll cover how to implement Facebook Authentication in Angular 14.
Tutorial contents
- Example Angular app overview
- Live demo of the Angular app
- Run the Angular Facebook auth app locally
- Connect the Angular app with a .NET API
- Angular 14 project structure
Example Angular 14 App Overview
The example Angular app contains the following three pages to demonstrate logging in with Facebook, then viewing and editing your account details:
- Login (
/login
) - contains a Facebook login button that triggers the authentication flow with Facebook, on success a Facebook access token is returned to the Angular app which is then used to authenticate with the backend API. The first time you login a new account is created for you on the backend API. - Home (
/
) - displays your account details fetched from a secure endpoint on the backend API. - Edit Account (
/edit-account
) - contains a form to update your account details or delete your account from the backend API.
Angular + API + Facebook Login
These are the three pieces involved in making the example work:
- Angular App - the front-end (client) app with pages for Facebook Login, view account and edit account.
- Backend API - the server API that manages and provides secure access to data for the front-end app. It is implemented by a fake backend API in the example.
- Facebook - in this scenario Facebook acts as an authentication server (identity provider) to verify user credentials and return an access token on success. The FB access token is used to authenticate with the Backend API, which validates the access token with Facebook and returns its own JWT token for accessing secure routes on the Backend API.
Communication with Facebook
Communication with Facebook in the example is enabled with a Facebook App I created in the Facebook Developer Portal (https://developers.facebook.com/) named "JasonWatmore.com Login Example". Facebook Apps are used to configure which products you want to leverage from Facebook (e.g. Facebook Login). The App ID is passed to the Facebook SDK during initialization (FB.init(...)
).
To implement Facebook Login in your own Angular (or other) App you will need to create your own Facebook App at https://developers.facebook.com/ and configure it with the Facebook Login product. For more info see the Facebook Login docs at https://developers.facebook.com/docs/facebook-login/web.
Authentication Flow with Angular, API and Facebook Login
These are the steps that happen when a user logs into the Angular App with Facebook:
- User clicks "Login with Facebook" and enters their credentials
- Facebook verifies credentials and returns an FB access token to the Angular App
- Angular App sends the access token to the Backend API to authenticate
- Backend API validates the access token with the Facebook API
- On first login, the Backend API creates a new account for the user (with their name fetched from the Facebook API)
- Backend API returns a new short-lived (15 minute) JWT token to the Angular App
- Angular App stores the JWT and uses it to make secure requests to the Backend API (Logged In!)
- Angular App starts a timer to repeat steps 3-8 before the JWT expires to auto refresh the JWT and keep the user logged in
Fake backend API
The example Angular app runs with a fake-backend api by default to enable it to run completely in the browser without a real api, the fake api contains routes for authentication and account CRUD operations and it uses browser local storage to save data. To disable the fake backend you just have to remove a couple of lines of code from the app module, you can build your own api or hook it up with the .NET API available.
Styled with Bootstrap 5
The example login app is styled with the CSS from Bootstrap 5.2, for more info about Bootstrap see https://getbootstrap.com/docs/5.2/getting-started/introduction/.
Code on GitHub
The example project is available on GitHub at https://github.com/cornflourblue/angular-14-facebook-login-example.
Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-14-facebook-login-example)
Run the Angular Facebook Login App Locally
- Install Node.js and npm from https://nodejs.org
- Download or clone the project source code from https://github.com/cornflourblue/angular-14-facebook-login-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Start the application in SSL (https) mode by running
npm run start:ssl
from the command line in the project root folder, SSL is required for the Facebook SDK to run properly, this will launch a browser with the URLhttps://localhost:4200/
. - You should see the message Your connection is not private (or something similar in non Chrome browsers), don't worry it's just because the Angular development server runs with a self signed SSL certificate. To open the app click the Advanced button and the link Proceed to localhost.
Connect the Angular Facebook Auth App with a .NET API
For full details about the .NET Facebook Auth API see the post .NET 7.0 - Facebook Authentication API Tutorial with Example. But to get up and running quickly just follow the below steps.
- Install the .NET SDK from https://dotnet.microsoft.com/download.
- Download or clone the project source code from https://github.com/cornflourblue/dotnet-7-facebook-authentication-api
- Start the api by running
dotnet run
from the command line in the project root folder (where the WebApi.csproj file is located), you should see the messageNow listening on: http://localhost:4000
. - Back in the Angular app, remove or comment out the line below the comment
// provider used to create fake backend
located in the/src/app/app.module.ts
file, then start the Angular app and it should now be hooked up with the .NET API.
Angular Project Structure
The Angular CLI was used to generate the base project structure with the ng new <project name>
command, the CLI is also used to build and serve the application. For more info about the Angular CLI see https://angular.io/cli.
The app and code structure of the tutorial mostly follow the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.
Folder Structure
Each feature has it's own folder (home & login), other shared/common code such as services, models, helpers etc are placed in folders prefixed with an underscore _
to easily differentiate them from features and group them together at the top of the folder structure.
Barrel Files
The index.ts
files in each folder are barrel files that group the exported modules from that folder together so they can be imported using only the folder path instead of the full module path, and to enable importing multiple modules in a single import (e.g. import { HomeComponent, EditAccountComponent } from './home'
).
TypeScript Path Aliases
Path aliases @app
and @environments
have been configured in tsconfig.json that map to the /src/app
and /src/environments
directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '../../../MyComponent'
).
Here are the main project files that contain the application logic, I left out some files that were generated by Angular CLI ng new
command that I didn't change.
- src
- app
- _helpers
- _models
- account.ts
- index.ts
- _services
- account.service.ts
- index.ts
- home
- login
- app-routing.module.ts
- app.component.html
- app.component.ts
- app.module.ts
- environments
- index.html
- main.ts
- polyfills.ts
- styles.less
- app
- package.json
- tsconfig.json
App Initializer
The app initializer runs before the Angular app starts up to load and initialize the Facebook SDK, and get the user's login status from Facebook. If the user is already logged in with Facebook they are automatically logged into the backend API (and Angular App) using the Facebook access token, the user is then redirected to the home page by the login component.
The app initializer is added to angular app in the providers
section of the app module using the APP_INITIALIZER
injection token. For more info see Angular - Execute an init function before app startup.
import { finalize } from 'rxjs';
import { AccountService } from '@app/_services';
import { environment } from '@environments/environment';
export function appInitializer(accountService: AccountService) {
return () => new Promise(resolve => {
// wait for facebook sdk to initialize before starting the angular app
window.fbAsyncInit = function () {
FB.init({
appId: environment.facebookAppId,
cookie: true,
xfbml: true,
version: 'v8.0'
});
// auto login to the api if already logged in with facebook
FB.getLoginStatus(({ authResponse }) => {
if (authResponse) {
accountService.loginApi(authResponse.accessToken)
.pipe(finalize(() => resolve(null)))
.subscribe();
} else {
resolve(null);
}
});
};
// load facebook sdk script
(function (d, s, id) {
var js: any;
var fjs: any = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) { return; }
js = d.createElement(s);
js.id = id;
js.src = "https://connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
});
}
Auth Guard
The auth guard is an angular route guard that's used to prevent unauthorized users from accessing restricted routes, it does this by implementing the CanActivate
interface which allows the guard to decide if a route can be activated with the canActivate()
method. If the method returns true
the route is activated (allowed to proceed), otherwise if the method returns false
the route is blocked.
The auth guard uses the account service to check if the user is logged in, if the user isn't logged in they're redirected to the /login
page with the returnUrl
in the query parameters.
Angular route guards are attached to routes in the router config, this auth guard is used in app-routing.module.ts to protect the home and edit account routes.
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AccountService } from '@app/_services';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private accountService: AccountService
) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const account = this.accountService.accountValue;
if (account) {
// logged in so return true
return true;
}
// not logged in so redirect to login page with the return url
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}
Error Interceptor
The Error Interceptor intercepts http responses from the api to check if there were any errors. If there is a 401 Unauthorized
or 403 Forbidden
response the account is automatically logged out of the application, all other errors are re-thrown up to the calling service or component to be handled.
It's implemented using the Angular HttpInterceptor
interface included in the HttpClientModule
, by implementing the HttpInterceptor interface you can create a custom interceptor to catch all error responses from the api in a single location.
Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AccountService } from '@app/_services';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private accountService: AccountService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
if ([401, 403].includes(err.status)) {
// auto logout if 401 or 403 response returned from api
this.accountService.logout();
}
const error = err.error?.message || err.statusText;
console.error(err);
return throwError(() => error);
}))
}
}
Fake Backend API
In order to run and test the Angular app without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the Angular app and sends back "fake" responses. This is done by a class that implements the Angular HttpInterceptor
interface, for more information on Angular HTTP Interceptors see https://angular.io/api/common/http/HttpInterceptor or this article.
The fake backend is organised into a top level handleRoute()
function that checks the request url and method to determine how the request should be handled. For intercepted routes one of the below // route functions
is called, for all other routes the request is passed through to the real backend by calling next.handle(request)
. Below the route functions there are // helper functions
for returning different response types and performing small tasks.
For more info see Angular 14 - Fake Backend API to Intercept HTTP Requests in Development.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError, from } from 'rxjs';
import { delay, materialize, dematerialize, concatMap } from 'rxjs/operators';
// array in local storage for accounts
const accountsKey = 'angular-14-facebook-login-accounts';
let accounts: any[] = JSON.parse(localStorage.getItem(accountsKey)!) || [];
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
// wrap in delayed observable to simulate server api call
return handleRoute();
function handleRoute() {
switch (true) {
case url.endsWith('/accounts/authenticate') && method === 'POST':
return authenticate();
case url.endsWith('/accounts/current') && method === 'GET':
return getAccount();
case url.endsWith('/accounts/current') && method === 'PUT':
return updateAccount();
case url.endsWith('/accounts/current') && method === 'DELETE':
return deleteAccount();
default:
// pass through any requests not handled above
return next.handle(request);
}
}
// route functions
function authenticate() {
const { accessToken } = body;
return from(new Promise(resolve => {
fetch(`https://graph.facebook.com/v8.0/me?access_token=${accessToken}`)
.then(response => resolve(response.json()));
})).pipe(concatMap((data: any) => {
if (data.error) return unauthorized(data.error.message);
let account = accounts.find(x => x.facebookId === data.id);
if (!account) {
// create new account if first time logging in
account = {
id: newAccountId(),
facebookId: data.id,
name: data.name,
extraInfo: `This is some extra info about ${data.name} that is saved in the API`
}
accounts.push(account);
localStorage.setItem(accountsKey, JSON.stringify(accounts));
}
return ok({
...account,
token: generateJwtToken(account)
});
}));
}
function getAccount() {
if (!isLoggedIn()) return unauthorized();
return ok(currentAccount());
}
function updateAccount() {
if (!isLoggedIn()) return unauthorized();
let params = body;
let account = currentAccount();
// update and save account
Object.assign(account, params);
localStorage.setItem(accountsKey, JSON.stringify(accounts));
return ok(account);
}
function deleteAccount() {
if (!isLoggedIn()) return unauthorized();
// delete account then save
accounts = accounts.filter(x => x.id !== currentAccount().id);
localStorage.setItem(accountsKey, JSON.stringify(accounts));
return ok();
}
// helper functions
function ok(body?: any) {
return of(new HttpResponse({ status: 200, body }))
.pipe(delay(500));
}
function unauthorized(message = 'Unauthorized') {
return throwError(() => ({ status: 401, error: { message } }))
.pipe(materialize(), delay(500), dematerialize());
}
function isLoggedIn() {
return headers.get('Authorization')?.startsWith('Bearer fake-jwt-token');
}
function newAccountId() {
return accounts.length ? Math.max(...accounts.map(x => x.id)) + 1 : 1;
}
function currentAccount() {
// check if jwt token is in auth header
const authHeader = headers.get('Authorization');
if (!authHeader?.startsWith('Bearer fake-jwt-token')) return;
// check if token is expired
const jwtToken = JSON.parse(atob(authHeader.split('.')[1]));
const tokenExpired = Date.now() > (jwtToken.exp * 1000);
if (tokenExpired) return;
const account = accounts.find(x => x.id === jwtToken.id);
return account;
}
function generateJwtToken(account: any) {
// create token that expires in 15 minutes
const tokenPayload = {
exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000),
id: account.id
}
return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;
}
}
}
export let fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true
};
JWT Interceptor
The JWT Interceptor intercepts http requests from the application to add a JWT auth token to the Authorization header if the user is logged in and the request is to the Angular app's api url (environment.apiUrl
).
It's implemented using the HttpInterceptor
interface included in the HttpClientModule
, by implementing the HttpInterceptor interface you can create a custom interceptor to modify http requests before they get sent to the server.
Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '@environments/environment';
import { AccountService } from '@app/_services';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private accountService: AccountService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add auth header with jwt if account is logged in and request is to the api url
const account = this.accountService.accountValue;
const isLoggedIn = account?.token;
const isApiUrl = request.url.startsWith(environment.apiUrl);
if (isLoggedIn && isApiUrl) {
request = request.clone({
setHeaders: { Authorization: `Bearer ${account.token}` }
});
}
return next.handle(request);
}
}
Account Model
The account model is a small interface that defines the properties of an account.
export interface Account {
id: string;
facebookId: string;
name: string;
extraInfo: string;
token?: string;
}
Account Service
The account service handles communication between the Angular app and the backend API for everything related to accounts. It contains methods for logging in and out, as well as standard CRUD methods for retrieving and modifying account data.
The login()
method first calls the loginFacebook()
method to login to Facebook, then pipes the Facebook access token into the loginApi()
method to login to the backend API.
On successful login the API returns the account details and a JWT token, the loginApi()
method then publishes the data to all subscriber components with a call to this.accountSubject.next(account)
. A timer is then started to automatically re-authenticate in the background to keep the user logged in (this.startAuthenticateTimer()
).
The logout()
method logs out of Facebook by calling the SDK method FB.logout()
, it cancels the re-authenticate timer running in the background by calling this.stopAuthenticateTimer()
, logs the user out of the Angular app by publishing null
to all subscribers (this.accountSubject.next(null)
), and redirects to the login page.
The account
property exposes an RxJS observable (Observable<Account>
) so any component can subscribe to be notified when a user logs in, logs out or updates their account. The notification is triggered by the call to this.accountSubject.next()
from each of the corresponding methods in the service. For more info on component communication with RxJS see Angular 14 - Communicating Between Components with RxJS Observable & Subject.
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, from, of, EMPTY } from 'rxjs';
import { map, concatMap, finalize } from 'rxjs/operators';
import { environment } from '@environments/environment';
import { Account } from '@app/_models';
const baseUrl = `${environment.apiUrl}/accounts`;
@Injectable({ providedIn: 'root' })
export class AccountService {
private accountSubject: BehaviorSubject<Account | null>;
public account: Observable<Account | null>;
constructor(
private router: Router,
private http: HttpClient
) {
this.accountSubject = new BehaviorSubject<Account | null>(null);
this.account = this.accountSubject.asObservable();
}
public get accountValue() {
return this.accountSubject.value;
}
login() {
// login with facebook then the API to get a JWT auth token
return this.loginFacebook().pipe(
concatMap(accessToken => this.loginApi(accessToken))
);
}
loginFacebook() {
// login with facebook and return observable with fb access token on success
const fbLoginPromise = new Promise<fb.StatusResponse>(resolve => FB.login(resolve));
return from(fbLoginPromise).pipe(
concatMap(({ authResponse }) => authResponse ? of(authResponse.accessToken) : EMPTY)
);
}
loginApi(accessToken: string) {
// authenticate with the api using a facebook access token,
// on success the api returns an account object with a JWT auth token
return this.http.post<any>(`${baseUrl}/authenticate`, { accessToken })
.pipe(map(account => {
this.accountSubject.next(account);
this.startAuthenticateTimer();
return account;
}));
}
logout() {
FB.logout();
this.stopAuthenticateTimer();
this.accountSubject.next(null);
this.router.navigate(['/login']);
}
getAccount() {
return this.http.get<Account>(`${baseUrl}/current`);
}
updateAccount(params: any) {
return this.http.put(`${baseUrl}/current`, params)
.pipe(map((account: any) => {
// publish updated account to subscribers
account = { ...this.accountValue, ...account };
this.accountSubject.next(account);
return account;
}));
}
deleteAccount() {
return this.http.delete(`${baseUrl}/current`)
.pipe(finalize(() => {
// auto logout after account is deleted
this.logout();
}));
}
// helper methods
private authenticateTimeout?: NodeJS.Timeout;
private startAuthenticateTimer() {
// parse json object from base64 encoded jwt token
const jwtBase64 = this.accountValue!.token!.split('.')[1];
const jwtToken = JSON.parse(atob(jwtBase64));
// set a timeout to re-authenticate with the api one minute before the token expires
const expires = new Date(jwtToken.exp * 1000);
const timeout = expires.getTime() - Date.now() - (60 * 1000);
const accessToken = FB.getAuthResponse()?.accessToken;
if (accessToken) {
this.authenticateTimeout = setTimeout(() => {
this.loginApi(accessToken).subscribe();
}, timeout);
}
}
private stopAuthenticateTimer() {
// cancel timer for re-authenticating with the api
clearTimeout(this.authenticateTimeout);
}
}
Edit Account Component Template
The edit account component template contains a form for updating the details of your account or deleting your account.
<h2>Edit Account</h2>
<p>Updating your info here only changes it in the Angular example app, it does not (and cannot) change anything on Facebook itself.</p>
<form *ngIf="account" [formGroup]="form" (ngSubmit)="saveAccount()">
<div class="mb-3">
<label class="form-label">Facebook Id</label>
<div>{{account.facebookId}}</div>
</div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" formControlName="name" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.name.errors }" />
<div *ngIf="submitted && f.name.errors" class="invalid-feedback">
<div *ngIf="f.name.errors.required">Name is required</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Extra Info</label>
<input type="text" formControlName="extraInfo" class="form-control" />
</div>
<div class="mb-3">
<button type="submit" [disabled]="isSaving" class="btn btn-primary me-2">
<span *ngIf="isSaving" class="spinner-border spinner-border-sm me-1"></span>
Save
</button>
<a routerLink="/" class="btn btn-secondary me-2">Cancel</a>
<button (click)="deleteAccount()" class="btn btn-danger" [disabled]="isDeleting">
<span *ngIf="isDeleting" class="spinner-border spinner-border-sm me-1"></span>
Delete
</button>
<div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
</div>
</form>
<div *ngIf="!account" class="text-center p-3">
<span class="spinner-border spinner-border-lg align-center"></span>
</div>
Edit Account Component
The edit account component enables updating the details of your account or deleting your account. The form is prepopulated with the current account details which are accessed via the accountValue
property of the account service.
On save the account is updated with the account service and the user is redirected back to the home page with their account details.
On delete the account is deleted from the backend API and the user is logged out.
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AccountService } from '@app/_services';
import { Account } from '@app/_models';
@Component({ templateUrl: 'edit-account.component.html' })
export class EditAccountComponent implements OnInit {
form!: FormGroup;
account!: Account;
isSaving = false;
isDeleting = false;
submitted = false;
error = '';
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService
) { }
ngOnInit() {
this.form = this.formBuilder.group({
name: ['', Validators.required],
extraInfo: ['']
});
// populate form with current account details
this.account = this.accountService.accountValue!;
this.form.patchValue(this.account);
}
// convenience getter for easy access to form fields
get f() { return this.form.controls; }
saveAccount() {
this.submitted = true;
// stop here if form is invalid
if (this.form.invalid) {
return;
}
this.isSaving = true;
this.error = '';
this.accountService.updateAccount(this.form.value)
.pipe(first())
.subscribe({
next: () => {
this.router.navigate(['/']);
},
error: error => {
this.error = error;
this.isSaving = false;
}
});
}
deleteAccount() {
this.isDeleting = true;
this.accountService.deleteAccount()
.pipe(first())
.subscribe();
}
}
Home Component Template
The home component template contains a simple welcome message and your account details with a button to edit.
<h2>You're logged in with Angular 14 & Facebook!!</h2>
<p>Below are your account details fetched from a secure API endpoint:</p>
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Facebook Id</th>
<th>Name</th>
<th>Extra Info</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngIf="account">
<td>{{account.id}}</td>
<td>{{account.facebookId}}</td>
<td>{{account.name}}</td>
<td>{{account.extraInfo}}</td>
<td style="width: 50px;">
<a routerLink="edit-account" class="btn btn-sm btn-primary">Edit</a>
</td>
</tr>
<tr *ngIf="!account">
<td colspan="5" class="text-center">
<span class="spinner-border spinner-border-lg align-center"></span>
</td>
</tr>
</tbody>
</table>
Home Component
The home component fetches the current account from the backend API with the account service in the ngOnInit()
method and makes it available to the home template via the account
property.
The component fetches the account to demonstrate accessing a secure endpoint on the backend API with the JWT token returned after logging in. The account is also available via the accountValue
property.
import { Component } from '@angular/core';
import { first } from 'rxjs/operators';
import { AccountService } from '@app/_services';
import { Account } from '@app/_models';
@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
account!: Account;
constructor(private accountService: AccountService) { }
ngOnInit() {
this.accountService.getAccount()
.pipe(first())
.subscribe(x => this.account = x);
}
}
Login Component Template
The login component template contains a single Facebook login button that is bound to the login()
method of the login component on click.
<div class="col-md-6 offset-md-3 mt-5 text-center">
<div class="card">
<h4 class="card-header">Angular 14 Facebook Authentication</h4>
<div class="card-body">
<button class="btn btn-facebook" (click)="login()">
<i class="fa fa-facebook me-1"></i>
Login with Facebook
</button>
</div>
</div>
</div>
Login Component
The login component uses the account service to login to the application using Facebook. If the user is already logged in they are automatically redirected to the home page.
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AccountService } from '@app/_services';
@Component({ templateUrl: 'login.component.html' })
export class LoginComponent {
constructor(
private router: Router,
private route: ActivatedRoute,
private accountService: AccountService
) {
// redirect to home if already logged in
if (this.accountService.accountValue) {
this.router.navigate(['/']);
}
}
login() {
this.accountService.login()
.subscribe(() => {
// get return url from query parameters or default to home page
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigateByUrl(returnUrl);
});
}
}
App Routing Module
The app routing module defines the routes for the angular application and generates a root routing module by passing the array of routes
to the RouterModule.forRoot()
method. The module is imported into the main app module below.
The home route maps the root path (/
) of the app to the home component, the edit account route /edit-account
maps to the edit account component, and the /login
route maps to the login component.
The home and edit account routes are secured by passing the auth guard to the canActivate
property of each route.
For more information on angular routing see https://angular.io/guide/router.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent, EditAccountComponent } from './home';
import { LoginComponent } from './login';
import { AuthGuard } from './_helpers';
const routes: Routes = [
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'edit-account', component: EditAccountComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
// otherwise redirect to home
{ path: '**', redirectTo: '' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
App Component Template
The app component template is the root component template of the angular application, it contains the main nav bar which is only displayed when you're logged in, and a router-outlet
directive that renders the contents of each view based on the current route / path.
<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="accountService.accountValue">
<div class="navbar-nav">
<a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
<button class="btn btn-link nav-item nav-link" (click)="accountService.logout()">Logout</button>
</div>
</nav>
<!-- main app container -->
<div class="container pt-4">
<router-outlet></router-outlet>
</div>
App Component
The app component is the root component of the angular app, it defines the root tag of the app as <app-root></app-root>
with the selector property of the @Component()
decorator.
An account service instance is made accessible to the app component template by including it as a dependency in the constructor params with the protected modifier (protected accountService: AccountService
).
import { Component } from '@angular/core';
import { AccountService } from './_services';
@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
constructor(protected accountService: AccountService) {}
}
App Module
The app module defines the root module of the angular application along with metadata about the module. For more info about angular 14 modules see https://angular.io/docs/ts/latest/guide/ngmodule.html.
This is where the fake backend provider is added to the application, to switch to a real backend simply remove the fakeBackendProvider
located below the comment // provider used to create fake backend
.
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
// used to create fake backend
import { fakeBackendProvider } from './_helpers';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { JwtInterceptor, ErrorInterceptor, appInitializer } from './_helpers';
import { AccountService } from './_services';
import { HomeComponent, EditAccountComponent } from './home';
import { LoginComponent } from './login';
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule,
HttpClientModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
EditAccountComponent,
LoginComponent
],
providers: [
{ provide: APP_INITIALIZER, useFactory: appInitializer, multi: true, deps: [AccountService] },
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
// provider used to create fake backend
fakeBackendProvider
],
bootstrap: [AppComponent]
})
export class AppModule { }
Production Environment Config
The production environment config contains variables required to run the application in production. This enables you to build the application with a different configuration for each different environment (e.g. production & development) without updating the app code.
When you build the application for production with the command ng build --configuration production
, the output environment.ts
is replaced with environment.prod.ts
.
export const environment = {
production: true,
apiUrl: 'http://localhost:4000',
facebookAppId: '314930319788683'
};
Development Environment Config
The development environment config contains variables required to run the app in development.
Environment config is accessed by importing the environment object into any Angular service of component with the line import { environment } from '@environments/environment'
and accessing properties on the environment
object, see the account service for an example.
export const environment = {
production: false,
apiUrl: 'http://localhost:4000',
facebookAppId: '314930319788683'
};
Main Index Html File
The main index.html file is the initial page loaded by the browser that kicks everything off. The Angular CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<title>Angular 14 - Facebook Authentication Tutorial with Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- bootstrap & font-awesome css -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
Main (Bootstrap) File
The main file is the entry point used by angular to launch and bootstrap the application.
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
Polyfills
Some features used by Angular 14 are not yet supported natively by all major browsers, polyfills are used to add support for features where necessary so your Angular application works across all major browsers.
This file is generated by the Angular CLI when creating a new project with the ng new
command, I've excluded the comments in the file for brevity.
import 'zone.js'; // Included with Angular CLI.
Global LESS/CSS Styles
The global styles file contains LESS/CSS styles that are applied globally throughout the angular 14 application.
The example contains just a few styles for the Facebook login button.
/* You can add global styles to this file, and also import other style files */
.btn-facebook,
.btn-facebook:hover,
.btn-facebook:first-child:active {
background: #3B5998;
border-color: #3B5998;
color: #fff;
}
.btn-facebook:hover {
opacity: 0.9;
}
.btn-facebook:first-child:active {
opacity: 0.8;
}
Package.json
The package.json file contains project configuration information including package dependencies that get installed when you run npm install
and scripts that are executed when you run npm start
or npm run build
etc. Full documentation is available at https://docs.npmjs.com/files/package.json.
{
"name": "angular-14-example",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --open",
"start:ssl": "ng serve --ssl --open",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.2.0",
"@angular/common": "^14.2.0",
"@angular/compiler": "^14.2.0",
"@angular/core": "^14.2.0",
"@angular/forms": "^14.2.0",
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"@types/facebook-js-sdk": "^3.3.6",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^14.2.8",
"@angular/cli": "~14.2.8",
"@angular/compiler-cli": "^14.2.0",
"@types/jasmine": "~4.0.0",
"jasmine-core": "~4.3.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.7.2"
}
}
TypeScript tsconfig.json
The tsconfig.json file contains the base TypeScript compiler configuration for all projects in the Angular workspace, it configures how the TypeScript code will be compiled / transpiled into JavaScript that is understood by the browser. For more info see https://angular.io/config/tsconfig.
Most of the file is unchanged from when it was generated by the Angular CLI, only the paths
property has been added to map the @app
and @environments
aliases to the /src/app
and /src/environments
directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '@app/MyComponent'
instead of import MyComponent from '../../../MyComponent'
).
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2020",
"module": "es2020",
"lib": [
"es2020",
"dom"
],
"paths": {
"@app/*": ["src/app/*"],
"@environments/*": ["src/environments/*"]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
Need Some Angular 14 Help?
Search fiverr for freelance Angular 14 developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!