Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 2a: Authentication

DZone's Guide to

Real-World Angular Series, Part 2a: Authentication

Being able to authenticate users and their data is a key part of building a web app. So, in this post, we'll explore how to building authentication into the front-end.

· Web Dev Zone
Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

Part 1a and Part 1b of this tutorial covered how to set up the cloud-hosted MongoDB database, Node server, and front-end for our real-world Angular application.

The second installment in the series covers authentication, authorization, feature planning, and data modeling.

Angular: Authentication

Let's pick up right where we left off last time. We've built the main layout for our app. Now it's time to add an authentication feature. Our app's basic authentication should include:

  • Login and logout.
  • User profile storage.
  • Access token storage.
  • Session persistence.
  • Factory to authorize HTTP requests with an access token.

Dependencies

First, let's install a few dependencies. We need the auth0-js package to interface with our Auth0 account. The angular2-jwt package provides helpers to authorize HTTP requests. Install both packages with npm from the project root:

$ npm install auth0-js --save
$ npm install angular2-jwt --save

Environment Configuration

Let's create a file to store information about our app's environment. We're currently developing on localhost:4200, but the app will be deployed on the Node server eventually, and in production, it will run on a reverse proxy. We'll need to make sure our development environment doesn't break our production environment and vice versa.

Create a folder, src/core, then add a file there called env.config.ts:

// src/app/core/env.config.ts
const _isDev = window.location.port.indexOf('4200') > -1;
const getHost = () => {
  const protocol = window.location.protocol;
  const host = window.location.host;
  return `${protocol}//${host}`;
};
const apiURI = _isDev ? 'http://localhost:8083/api/' : `/api/`;

export const ENV = {
  BASE_URI: getHost(),
  BASE_API: apiURI
};

This code detects the host environment and sets the app's base URI and base API URI. We'll import this ENV configuration wherever we need to detect and use these URIs.

Authentication Configuration

We'll store our Auth0 authentication configuration in an auth.config.ts file. Create the following blank file: src/app/auth/auth.config.ts.

Open this file and customize the following code with your own Auth0 client and API information:

// src/app/auth/auth.config.ts
import { ENV } from './../core/env.config';

interface AuthConfig {
  CLIENT_ID: string;
  CLIENT_DOMAIN: string;
  AUDIENCE: string;
  REDIRECT: string;
  SCOPE: string;
};

export const AUTH_CONFIG: AuthConfig = {
  CLIENT_ID: '[AUTH0_CLIENT_ID]',
  CLIENT_DOMAIN: '[AUTH0_CLIENT_DOMAIN]',
  AUDIENCE: '[YOUR_AUTH0_API_AUDIENCE]', // likely http://localhost:8083/api/
  REDIRECT: `${ENV.BASE_URI}/callback`,
  SCOPE: 'openid profile'
};

Authentication Service

Authentication logic on the front-end will be handled with an AuthServiceauthentication service. Let's generate the boilerplate for a new service with the CLI:

$ ng g service auth/auth

Now open the generated auth.service.ts file and add the necessary code to our authentication service:

// src/app/auth/auth.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { AUTH_CONFIG } from './auth.config';
import * as auth0 from 'auth0-js';

// Avoid name not found warnings
declare var auth0: any;

@Injectable()
export class AuthService {
  // Create Auth0 web auth instance
  auth0 = new auth0.WebAuth({
    clientID: AUTH_CONFIG.CLIENT_ID,
    domain: AUTH_CONFIG.CLIENT_DOMAIN,
    responseType: 'token id_token',
    redirectUri: AUTH_CONFIG.REDIRECT,
    audience: AUTH_CONFIG.AUDIENCE,
    scope: AUTH_CONFIG.SCOPE
  });
  userProfile: any;
  // Create a stream of logged in status to communicate throughout app
  loggedIn: boolean;
  loggedIn$ = new BehaviorSubject<boolean>(this.loggedIn);

  constructor(private router: Router) {
    // If authenticated, set local profile property
    // and update login status subject.
    // If not authenticated but there are still items
    // in localStorage, log out.
    const lsProfile = localStorage.getItem('profile');

    if (this.tokenValid) {
      this.userProfile = JSON.parse(lsProfile);
      this.setLoggedIn(true);
    } else if (!this.tokenValid && lsProfile) {
      this.logout();
    }
  }

  setLoggedIn(value: boolean) {
    // Update login status subject
    this.loggedIn$.next(value);
    this.loggedIn = value;
  }

  login() {
    // Auth0 authorize request
    this.auth0.authorize();
  }

  handleAuth() {
    // When Auth0 hash parsed, get profile
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        window.location.hash = '';
        this._getProfile(authResult);
      } else if (err) {
        console.error(`Error authenticating: ${err.error}`);
      }
      this.router.navigate(['/']);
    });
  }

  private _getProfile(authResult) {
    // Use access token to retrieve user's profile and set session
    this.auth0.client.userInfo(authResult.accessToken, (err, profile) => {
      if (profile) {
        this._setSession(authResult, profile);
      } else if (err) {
        console.error(`Error authenticating: ${err.error}`);
      }
    });
  }

  private _setSession(authResult, profile) {
    // Save session data and update login status subject
    const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + Date.now());
    // Set tokens and expiration in localStorage and props
    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('id_token', authResult.idToken);
    localStorage.setItem('expires_at', expiresAt);
    localStorage.setItem('profile', JSON.stringify(profile));
    this.userProfile = profile;
    // Update login status in loggedIn$ stream
    this.setLoggedIn(true);
  }

  logout() {
    // Ensure all auth items removed from localStorage
    localStorage.removeItem('access_token');
    localStorage.removeItem('id_token');
    localStorage.removeItem('profile');
    localStorage.removeItem('authRedirect');
    // Reset local properties, update loggedIn$ stream
    this.userProfile = undefined;
    this.setLoggedIn(false);
    // Return to homepage
    this.router.navigate(['/']);
  }

  get tokenValid(): boolean {
    // Check if current time is past access token's expiration
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return Date.now() < expiresAt;
  }

}

This service uses the config variables from auth.config.ts to instantiate an auth0.jsWebAuth instance.

An RxJS BehaviorSubject is used to provide a stream of authentication status events that we can subscribe to anywhere in the app.

The constructor checks the current authentication status upon app initialization. If the user is still logged in from a previous session and their token has not expired, the local properties are set using data from local storage and the loggedIn property and loggedIn$ subject are updated. If the token is expired but the user has not logged themselves out, the logout() method is executed to clear any expired session data.

The login() method authorizes the authentication request with Auth0 using the auth config variables. An Auth0 hosted Lock instance will be shown to the user and they can then log in.

Note: If it's the user's first visit to our app and our callback is on localhost, they'll also be presented with a consent screen where they can grant access to our API. A first party client on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.

We'll receive an id_token, an access_token, and a time until token expiration (expiresIn) from Auth0 when returning to our app. The handleAuth()method uses Auth0's parseHash() method callback to get the user's profile (_getProfile()) and set the session (_setSession()) by saving the tokens, expiration, and profile to local storage and calling setLoggedIn() so that any components in the app are informed that the user is now authenticated.

Finally, we'll define a logout() method that clears data from local storage and updates setLoggedIn(). We also have a tokenValid() accessor to check whether the current DateTime is less than the token expiration DateTime.

Provide Authservice in App Module

In order to use the AuthService methods and properties anywhere in our app, we need to add the service to the providers array in our app.module.ts:

// src/app/app.module.ts
...
import { AuthService } from './auth/auth.service';
...
@NgModule({
  ...
  providers: [
    ...,
    AuthService
  ],
  ...
})
...

Handle Authentication in App Component

The authentication service's handleAuth() method must be called in the app.component.ts constructor so it will run on initialization of our app:

// src/app/app.component.ts
import { AuthService } from './auth/auth.service';
...
  constructor(private auth: AuthService) {
    // Check for authentication and handle if hash present
    auth.handleAuth();
  }
...

Create a Callback Component

Next, we'll create a Callback component. This is where the app is redirected after authentication. This component simply shows a loading message until hash parsing is completed and the Angular app redirects back to the homepage.

Note: Recall that we already added http://localhost:4200/callback and http://localhost:8083/callback to our Auth0 Client Allowed Callback URLs setting.

Let's generate this component with the Angular CLI:

$ ng g component pages/callback

For now, all we need to do in this component is change the text in callback.component.html to Loading..., like so:

<!-- src/app/pages/callback/callback.component.html -->
<div>
  Loading...
</div>

We'll spruce this up later with a nice loading icon. For now, let's add the component to our routing module, app-routing.module.ts:

// src/app/app-routing.module.ts
...
import { CallbackComponent } from './pages/callback/callback.component';

const routes: Routes = [
  ...
  {
    path: 'callback',
    component: CallbackComponent
  }
];
...

Add Login and Logout to Header Component

Now we have the logic necessary to authenticate users, but we still need a way for them to log in and out in the UI. Let's add this in the Header component.

Open up the header.component.ts file:

// src/app/header/header.component.ts
...
import { AuthService } from './../auth/auth.service';
...
export class HeaderComponent implements OnInit {
  ...
  constructor(
    ...,
    public auth: AuthService) { }
  ...
}

We'll import our AuthService and declare it in the constructor function.

Note: We're using public because the authentication service's methods will be used in the template, not just in the class.

Now let's add login, logout, and a user greeting to the header.component.htmltemplate:

<!-- src/app/header/header.component.html -->
<header id="header" class="header">
  <div class="header-page bg-primary">
    ...
    <div class="header-page-authStatus">
      <a *ngIf="!auth.loggedIn" (click)="auth.login()">Log In</a>
      <span *ngIf="auth.loggedIn">
        {{auth.userProfile?.name}} <span class="opacity-half">|</span> <a (click)="auth.logout()">Log Out</a>
      </span>
    </div>
  </div>
  ...

We've added a <div class="header-page-authStatus> element. We'll use the ngIf directive with the loggedIn property from our authentication service to determine if the user is logged in to show or hide the appropriate markup. If the user is not logged in, we'll show a "Log In" link. If they're already authenticated, we'll show their name and a link to log out.

Note: Notice the auth.userProfile?.name binding. The ?. is the safe navigation operator. This operator protects against null and undefined values in property paths. If the object is not yet defined, the safe navigation operator prevents an error from occurring and waits to render until the data is available.

Now let's add a little bit of CSS to style our new authentication status elements. Open the header.component.scss file:

/* src/app/header/header.component.scss */
...
.header-page {
  ...
  &-authStatus {
    color: #fff;
    font-size: 12px;
    line-height: 50px;
    padding: 0 10px;
    position: absolute;
      right: 0; top: 0;

    a:hover {
      text-decoration: underline;
    }
  }
}

We can now log into our app! Try it out in the browser.

Auth0 hosted login screen

Once logged in, you should see your name and a link to log out in the upper right corner of the header.

Auth0 logged into Angular app

You should also be able to close the browser and reopen it to find your session has persisted (unless enough time has passed for the token to expire).

AuthHttp Factory

Now we have authentication working in the front-end, but we also need to use the access_token to make authenticated API requests. In order to facilitate this, we'll create an AuthHttp factory using angular2-jwt.

Add a new class called auth-http.factory.ts with the following command:

$ ng g class auth/auth-http.factory

Open the newly generated auth-http.factory.ts file and add:

// src/app/auth/auth-http.factory.ts
import { Http, RequestOptions } from '@angular/http';
import { AuthHttp, AuthConfig } from 'angular2-jwt';

export function authHttpFactory(http: Http, options: RequestOptions) {
  return new AuthHttp(new AuthConfig({
    tokenName: 'token',
    tokenGetter: (() => localStorage.getItem('access_token'))
  }), http, options);
};

This factory will allow us to use an authHttp method in the place of httpwhen we want to send an authenticated request. The angular2-jwt package will look for an access_token in local storage and use this as a Bearer Authorization header.

In order to use this factory, we need to provide it in our app.module.ts like so:

// src/app/app.module.ts
...
import { HttpModule, Http, RequestOptions } from '@angular/http';
import { AuthHttp } from 'angular2-jwt';
import { authHttpFactory } from './auth/auth-http.factory';

@NgModule({
  ...
  providers: [
    ...,
    {
      provide: AuthHttp,
      useFactory: authHttpFactory,
      deps: [Http, RequestOptions]
    }
  ],
  ...
})
...

Add Http and RequestOptions to the imports from @angular/http. Then import AuthHttp from angular2-jwt and the authHttpFactory we just created. Then we'll provide AuthHttp with its factory and dependencies. Now we can use authHttp as a method to make secure requests.

Tune in for Part 2b where we'll discuss Admin Authorization, Data Modeling, and Planning App Features. 

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
web dev ,angular ,web application development ,typescript

Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}