Authentication Service

The Authentication service supplies our app with the methods it needs to log in, log out, set up user sessions, and automatically renew tokens. Open auth.service.ts and add the following code:

// src/app/auth/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, bindNodeCallback, timer, of, Subscription } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import * as auth0 from 'auth0-js';
import { environment } from './../../environments/environment';
import { Location } from '@angular/common';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  // Create Auth0 web auth instance
  // @TODO: Update environment variables and remove .sample
  // extension in src/environments/environment.ts.sample
  // and src/environments/environment.prod.ts.sample
  private Auth0 = new auth0.WebAuth({
    clientID: environment.auth.clientId,
    domain: environment.auth.domain,
    responseType: 'token id_token',
    audience: 'https://secure-dino-api',
    redirectUri: environment.auth.redirect,
    scope: 'openid profile email read:dino-details write:dino-fav read:admin'
  });
  // localStorage property names
  private authFlag = 'isLoggedIn';
  private redirect = 'redirect';
  // Store access token and create stream
  accessToken: string = null;
  accessToken$ = new BehaviorSubject<string>(this.accessToken);
  // Create stream of user profile data
  userProfile: any = null;
  userProfile$ = new BehaviorSubject<any>(this.userProfile);
  // Auth-related URL paths
  logoutPath = '/';
  defaultSuccessPath = '/';
  // Create observable of Auth0 parseHash method; gather auth results
  parseHash$ = bindNodeCallback(this.Auth0.parseHash.bind(this.Auth0));
  // Create observable of Auth0 checkSession method to
  // verify authorization server session and renew tokens
  checkSession$ = bindNodeCallback(this.Auth0.checkSession.bind(this.Auth0));
  // Token expiration management
  accessTokenExp: number;
  refreshSub: Subscription;
  // Hide auth header while performing local login
  // (e.g., on the callback page)
  hideAuthHeader: boolean;

  constructor(
    private router: Router,
    private location: Location
  ) { }

  login(autoLogin?: boolean) {
    // Was this triggered by unauthorized access attempt?
    if (!autoLogin) {
      // If user clicked login button, store path
      // to redirect to after successful login
      this.storeAuthRedirect(this.router.url);
      // If login was triggered by an access attempt
      // instead, the route guard will set redirect
    }
    this.Auth0.authorize();
  }

  handleLoginCallback() {
    if (window.location.hash && !this.isAuthenticated) {
      // Hide header while parsing hash
      this.hideAuthHeader = true;
      // Subscribe to parseHash$ bound callback observable
      this.parseHash$({}).subscribe(
        authResult => {
          // Don't keep callback route + hash in browser history
          this.location.replaceState('/');
          // Log in locally and navigate
          this.localLogin(authResult);
          this.navigateAfterParseHash();
        },
        err => this.handleError(err)
      );
    } else {
      // If visiting the callback page with no hash
      // return to default logged out route
      this.goToLogoutUrl();
    }
  }

  renewAuth() {
    if (this.isAuthenticated) {
      // Check Auth0 authorization server session
      this.checkSession$({}).subscribe(
        authResult => this.localLogin(authResult),
        err => this.handleError(err)
      );
    }
  }

  private localLogin(authResult) {
    if (authResult && authResult.accessToken && authResult.idToken && authResult.idTokenPayload) {
      // Set token expiration
      const now = new Date().getTime();
      this.accessTokenExp = now + (authResult.expiresIn * 1000);
      // Set token in local property and emit in stream
      this.setToken(authResult.accessToken);
      // Emit value for user profile stream
      this.userProfile = authResult.idTokenPayload;
      this.userProfile$.next(this.userProfile);
      // Set flag in local storage stating app is logged in
      localStorage.setItem(this.authFlag, JSON.stringify(true));
      // Set up silent token renewal for this browser session
      this.scheduleRenewal();
    } else {
      // Something was missing from expected authResult
      this.localLogout(true);
    }
  }

  private localLogout(redirect?: boolean) {
    this.userProfile$.next(null);
    this.setToken(null);
    this.unscheduleRenewal();
    this.clearRedirect();
    localStorage.setItem(this.authFlag, JSON.stringify(false));
    // Redirect back to logout URL (if param set)
    if (redirect) {
      this.goToLogoutUrl();
    }
  }

  logout() {
    this.localLogout();
    // Auth0 server logout does a full page redirect:
    // make sure you have full logout URL in your Auth0
    // Dashboard Application settings in Allowed Logout URLs
    this.Auth0.logout({
      returnTo: environment.auth.logoutUrl,
      clientID: environment.auth.clientId
    });
  }

  scheduleRenewal() {
    if (!this.isAuthenticated) { return; }
    // Clean up any previous token renewal
    this.unscheduleRenewal();
    // Create and subscribe to expiration timer observable
    const expiresIn$ = of(this.accessTokenExp).pipe(
      mergeMap(exp => timer(Math.max(1, exp - Date.now())))
    );
    this.refreshSub = expiresIn$.subscribe(
      () => this.renewAuth()
    );
  }

  unscheduleRenewal() {
    if (this.refreshSub) {
      this.refreshSub.unsubscribe();
    }
  }

  private handleError(err) {
    this.hideAuthHeader = false;
    console.error(err);
    // Log out locally and redirect to default auth failure route
    this.localLogout(true);
  }

  get isAuthenticated(): boolean {
    // Check if the Angular app thinks this user is authenticated
    return JSON.parse(localStorage.getItem(this.authFlag));
  }

  setToken(token: string) {
    this.accessToken = token;
    this.accessToken$.next(token);
  }

  navigateAfterParseHash() {
    const rd = localStorage.getItem(this.redirect);
    if (rd) {
      this.router.navigateByUrl(rd).then(
        navigated => {
          if (navigated) {
            this.hideAuthHeader = false;
          }
          this.clearRedirect();
        }
      );
    } else {
      this.clearRedirect();
      this.router.navigateByUrl(this.defaultSuccessPath);
    }
  }

  storeAuthRedirect(url: string) {
    localStorage.setItem(this.redirect, url);
  }

  clearRedirect() {
    localStorage.removeItem(this.redirect);
  }

  goToLogoutUrl() {
    this.router.navigate([this.logoutPath]);
  }

  userHasRole(reqRole: string): boolean {
    return this.userProfile[environment.auth.roles_namespace].indexOf(reqRole) > -1;
  }

}

We will use Auth0's /authorize endpoint (called with the auth0.js method authorize()) to open the Auth0 login page and send users to a centralized authorization server for authentication. Upon successful authentication, the authorization server will then redirect the user back to our application with a URL hash, which can be parsed to extract auth results. The user's access token, token expiration, profile, and desired redirect URL (if they were trying to access a guarded route) are saved in memory. If the user's access token expires while they are still using the app, their session will be automatically renewed.

Note: If the user logged in with a social identity provider and Auth0 dev keys are set up for the connection, any attempts to renew the session silently will return a login_required error. To avoid this error, set up client accounts with all social IdPs.

results matching ""

    No results matching ""