Angular and Azure AD with a Better UX

Hello, world! In this post I improve the user experience by adding Bootstrap and greeting the user by her name once she is logged in. It assumes you have read Angular6 and Azure AD.

A Better UX

It makes little or no sense to have both Login and Logout buttons visible at the same time. Essential state management: the Login button must be visible when the user is not authenticated and Logout must be when she is. This will be accomplished by modifying AuthService and AppComponent. Let's start with AuthService where the userLoggedIn property is added and set when a successful authentication took place and unset when the user logs out:
import * as Msal from 'msal';

import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private userAgentApplication: Msal.UserAgentApplication;

  private _userLoggedIn: boolean;
  get userLoggedIn(): boolean {
    return this._userLoggedIn;
  }
  set userLoggedIn(value) {
    this._userLoggedIn = value;
  }

  constructor() {
    this.userAgentApplication = new Msal.UserAgentApplication(
      '55f4dfa9-2425-44f0-83a6-d3464b2a7eaa',
      'https://login.microsoftonline.com/illyum.onmicrosoft.com',
      null
    );
  }

  public login(): Observable {
    const graphScopes = ['user.read'];
    const promise = this.userAgentApplication.loginPopup(graphScopes);

    promise
      .then(_ => this.userLoggedIn = true)
      .catch(error => console.log(`loginPopup error = ${error}`));

    return from(promise);
  }

  public logout(): void {
    this.userAgentApplication.logout();
    this.userLoggedIn = false;
  }
}
This property is to be reflected in the AppComponent so the HTML template can use it:
import { Component } from '@angular/core';
import { AuthService } from './core/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Angular and Azure AD';

  get userLoggedIn() {
    return this.authService.userLoggedIn;
  }

  constructor(private authService: AuthService) {
  }

  login(): void {
    this.authService.login();
  }

  logout(): void {
    this.authService.logout();
  }
}
The buttons are shown or hidden depending on whether the user is logged in or not:
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<button *ngIf='!userLoggedIn' (click)='login()'>Login</button>
<button *ngIf='userLoggedIn' (click)='logout()'>Logout</button>
With all the changes above applied, the page appears initially like this:
And when the user is logged in, like this:
Let's invite Bootstrap to the party and the new version of app.component.html...
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Home</a>
  <div class="collapse navbar-collapse">
  </div>
  <ul class="nav navbar-nav navbar-right">
    <li class="nav-item" *ngIf="!userLoggedIn">
        <a class="nav-link" href="#" (click)='login()'>Login</a>
    </li>
    <li class="nav-item" *ngIf="userLoggedIn">
      <a class="nav-link" href="#" (click)='logout()'>Logout</a>
    </li>
  </ul>
</nav>
<div style="text-align:center">
  <h3>
    {{title}}
  </h3>
</div>
...and we get a prettier UI :)...

Note: I am a big fan of Automated Testing and from the initial commit, the Angular project contains a set of unit tests that allows me to validate the application quality as it is being developed. Write unit tests and run them through ng test (or npm test) reveals if the application is going well or wrong. For example, I have a unit test asserting that the Logout menu appears when the user is logged in so I don't need to go visual and verify that.

IdToken and Claims

Up to this point, the application has used the OpenID protocol to authenticate the user through the abstraction provided by MSAL. Now it's time to use the OpenID data encapsulated in what is known as the Identity Token. To get a clearer idea of what the Identity Token conveys, let make the following modification to the AuthService login method:
public login(): Observable {
  const graphScopes = ['user.read'];
  const promise = this.userAgentApplication.loginPopup(graphScopes);

  promise
    .then(rawIdToken => {
      console.log(rawIdToken);
      this.userLoggedIn = true;
    })
    .catch(error => console.log(`loginPopup error = ${error}`));

  return from(promise);
}
loginPopup produces the encoded Identity Token that is printed out to the console for debugging and exploration purposes:


Let's copy and paste the Identity Token to the very useful JWT.io to see what's in it:


Identity Token is in the JSON Web Token (JWT) format and it is made of claims or key-value pairs containing information like the token issuer ("iss") and the user name ("name"). A new version of the AuthService uses MSAL.IdToken to decode the Identity Token, get the "name" claim value, and expose it through the username property:
import * as Msal from 'msal';

import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';
import { IdToken } from 'msal/lib-commonjs/IdToken';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private userAgentApplication: Msal.UserAgentApplication;

  private _userLoggedIn: boolean;
  get userLoggedIn(): boolean {
    return this._userLoggedIn;
  }
  set userLoggedIn(value) {
    this._userLoggedIn = value;
  }

  private _username: string;
  get username() {
    return this._username;
  }
  set username(value) {
    this._username = value;
  }

  constructor() {
    this.userAgentApplication = new Msal.UserAgentApplication(
      '55f4dfa9-2425-44f0-83a6-d3464b2a7eaa',
      'https://login.microsoftonline.com/illyum.onmicrosoft.com',
      null
    );
  }

  public login(): Observable {
    const graphScopes = ['user.read'];
    const promise = this.userAgentApplication.loginPopup(graphScopes);

    promise
      .then(rawIdToken => {
        const idToken = new IdToken(rawIdToken);
        this.username = idToken.name;
        this.userLoggedIn = true;
      })
      .catch(error => console.log(`loginPopup error = ${error}`));

    return from(promise);
  }

  public logout(): void {
    this.userAgentApplication.logout();
    this.userLoggedIn = false;
  }
}
The username is reflected in AppComponent...
import { Component } from '@angular/core';
import { AuthService } from './core/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Angular and Azure AD';

  get userLoggedIn() {
    return this.authService.userLoggedIn;
  }

  get username() {
    return this.authService.username;
  }

  constructor(private authService: AuthService) {
  }

  login(): void {
    this.authService.login();
  }

  logout(): void {
    this.authService.logout();
  }
}
...so it can be used in the template app.component.html like this:
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Home</a>
  <div class="collapse navbar-collapse">
  </div>
  <ul class="nav navbar-nav navbar-right">
    <li class="nav-item" *ngIf="!userLoggedIn">
      <a class="nav-link" href="#" (click)='login()'>Login</a>
    </li>
    <li class="nav-item" *ngIf="userLoggedIn">
      <span class="nav-link">Hola {{username}}!</span>
    </li>
    <li class="nav-item" *ngIf="userLoggedIn">
      <a class="nav-link" href="#" (click)='logout()'>Logout</a>
    </li>
  </ul>
</nav>
<div style="text-align:center">
  <h3>
    {{title}}
  </h3>
</div>
I am sure that the application is working as intended because I created and ran my unit tests but here it is, also, the visual evidence:

That's it for now. Remember that the code can be downloaded from here (committed as "Better UX"). In my next post I am going to give a 101 intro on Microsoft Graph. See you there...

Comments

  1. Looking forward to hear about Microsoft Graph. It looks an interesting topic to explore.

    ReplyDelete

Post a Comment

Popular posts from this blog

Angular, Azure AD, and Microsoft Graph

Unit Testing Solidity Smart Contracts

Generative AI (GenAI) aplicada al código