Skip to content
This repository has been archived by the owner on Apr 27, 2021. It is now read-only.

Register a Browser Based Application with Fabric.Identity

michael.vidal edited this page Jun 1, 2018 · 27 revisions

Getting Started with a Browser Based Application

When integrating a browser based application (aka a JavaScript SPA) with Fabric.Identity, the recommended approach is to use the OpenID Connect Implicit flow.

The steps to set up your application to use Implicit Flow are:

  1. Register your application with Fabric.Identity
  2. Add code to your application to invoke the Implicit authentication flow

For a more detailed sequence diagram of the Implicit Flow authentication sequence see the Implicit Flow sequence diagram.

NOTE: This guide assumes you already have Fabric.Identity installed and have the fabric-installer secret. For proper installation documentation see Installing Fabric.Identity.

Register your app with Fabric.Identity

To register your client application with Fabric.Identity, you will need to POST the JSON request body to the following URL:

POST /api/client

with the following headers:

Authorization: Bearer {access_token}
Accept: application/json
Content-Type: application/json

where {access_token} is retrieved per the instructions in Retrieving an Access Token from Fabric.Identity.

The request below assumes your application is accessible at http://localhost:4200.

JSON Request Body

{
    "clientId": "sample-application",
    "clientName": "Sample Application",
    "allowedScopes": [
        "openid",
        "profile",
        "fabric.profile"
    ],
    "allowedGrantTypes": [
        "implicit"
    ],
    "allowedCorsOrigins": [
        "http://localhost:4200"
    ],
    "redirectUris": [
        "http://localhost:4200/oidc-callback.html",
        "http://localhost:4200/silent.html"
    ],
    "postLogoutRedirectUris": [
        "http://localhost:4200"
    ],
    "allowOfflineAccess": false,
    "requireConsent": false,
    "allowAccessTokensViaBrowser": true,
    "enableLocalLogin": false,
    "accessTokenLifetime": 1200
}

A few things to note about the above JSON Request sample:

  • allowedScopes - this array defines the scopes that your client will have access to in Fabric.Identity. For a comprehensive list of Scopes and more details about what a Scope is refer to our Scopes reference.
  • allowedGrantTypes - this array defines the flows (aka Grants) that your client application is allowed to use. In this case, for an implicit client, the implicit grant type is specified.
  • allowedCorsOrigins - this array defines the CORS origins that Fabric.Identity will allow when cross origin requests are made.
  • redirectUris - this array defines the list of allowed redirect URIs for a client application. When your application invokes the implicit flow, it will pass a redirect URI that it wants Fabric.Identity to redirect to after the user has authenticated. The code that executes on this URL should grab and store the tokens that are returned from Fabric.Identity. The redirect URI that your app passes during the implicit authentication flow has to be in this list of allowed URIs.
  • postLogoutRedirectUris - this array defines the list of allowed URIs that Fabric.Identity will redirect to after logout.
  • allowOfflineAccess - defines whether or not the registered client can request refresh tokens.
  • requireConsent - defines whether the consent screen is displayed to the user during authentication.
  • accessTokenLifetime - this defines how long an access token is valid for in seconds. For an implicit application, a setting of 20 minutes is recommended.

Integration

This walk through will show you step by step how to get a new angular 6 app running and authenticating a user via Fabric.Identity. The code for this walk through can be found in our samples repo. The walk through makes use of the Angular CLI to create the application, and the oidc-client npm package to handle the interaction with Fabric.Identity. It uses the Fabric.Identity client configuration that was registered in the above section. In the end, the sample will have a home page that shows the logged in user's details once the user is authenticated. After authentication the application will have an id_token that represents the authenticated user, and an access_token that allows the application to access other resources on behalf of the authenticated user.

Finally, the sample also adds the ability to automatically renew the user's access token before it expires using the automatic silent renew functionality of the oidc-client npm package.

Create a new angular application

From a command prompt, execute the following angular cli commands to create a new application:

ng new sample-application
cd sample-application

Add the oidc-client npm pacakage

Install the oidc-client npm package using npm:

npm install oidc-client --save

Add an Authentication Service

Execute the angular cli command:

ng g service authentication

Add the login(), logout(), isUserAuthenticated() and getUser() functions to the service. The login() method will be used to kick off the authentication flow:

import { Injectable } from '@angular/core';
import { UserManager, User, Log } from 'oidc-client';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {
  userManager: UserManager;

  constructor() { 
    var self = this;
    
    // The configuration settings below set the redirect and silent renew URI's,
    // enable automatic silent renew, ensure that the extended user information is 
    // loaded from the Fabric.Identity UserInfo endpoint (loadUserInfo),
    // specify the type of tokens we want back (response_type),
    // and specify the requested scopes
    const clientSettings: any = {
      authority: 'https://{fabric-identity-url}',
      client_id: 'sample-application',
      redirect_uri: 'http://localhost:4200/oidc-callback.html',
      post_logout_redirect_uri: 'http://localhost:4200',
      response_type: 'id_token token',
      scope: 'openid profile fabric.profile',  
      silent_redirect_uri: 'http://localhost:4200/silent.html',
      automaticSilentRenew: true,    
      filterProtocolClaims: true,
      loadUserInfo: true
    };

    this.userManager = new UserManager(clientSettings);    

    // events that you can hook into to do perform operations
    // when they fire.
    this.userManager.events.addAccessTokenExpiring(function(){      
      console.log("access token expiring");
    });

    this.userManager.events.addSilentRenewError(function(e){
      console.log("silent renew error: " + e.message);
    });

    this.userManager.events.addAccessTokenExpired(function () {
      console.log("access token expired");    
      //when access token expires logout the user
      self.logout();
    });  
  }

  login() {
    var self = this;
    this.userManager.signinRedirect().then(() => {
      console.log("signin redirect done");
    }).catch(err => {
      console.error(err);
    });
  }

  logout() {
    this.userManager.signoutRedirect();
  }

  isUserAuthenticated() {
    var self = this;
    return this.userManager.getUser().then(function (user) {
      if (user) {
        console.log("signin redirect done. ");
        console.log(user.profile);
        return true;
      } else {
        console.log("User is not logged in");
        return false;
      }
    });
  }

  getUser(): Promise<User> {
    return this.userManager.getUser();
  }
}

Handle the OIDC Callback

After the user authenticates via Fabric.Identity, their browser will be redirected back to the specified redirect uri, the access_token and id_token will be present in the URL fragment (the portion of the URL after the hash mark #). The code that runs when this URL is requested needs to extract the access_token and id_token, and store it. Fortunately, the oidc-client package does all this for us, we simply add a new oidc-callback.html file with the below content to the src directory:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
</head>
<body>
  <h1 id="waiting">Waiting...</h1>
  <div id="error"></div>
  <script src="assets/oidc-client.min.js"></script>
<script>
  Oidc.Log.logger = console;
  new Oidc.UserManager().signinRedirectCallback().then(function (user) {
      if (user == null) {
        document.getElementById("waiting").style.display = "none";
        document.getElementById("error").innerText = "No sign-in request pending.";
        console.log("at oidc-callback, but user is null");
      }
      else {
        console.log("user is not null, redirecting to index");
        window.location = "/";
      }
    })
    .catch(function (er) {
      document.getElementById("waiting").style.display = "none";
      document.getElementById("error").innerText = er.message;
      console.error(er);
    });
</script>
</body>
</html>

You will need to make sure that the oidc-client.min.js and oidc-callback.html are served up as static content by angular. You can do that by modifying the angular.json assets array:

 "assets": [
    ...
    {
    "glob": "oidc-callback.html",
    "input": "src/",
    "output": "/"
    },
    {
    "glob": "oidc-client.min.js",
    "input": "./node_modules/oidc-client/dist",
    "output": "/assets/"
    },
    ...
 ]

Add a Home component

We will add a home component so that once a user is authenticated, they can be redirected here. The home component will display the details of the authenticated user.

ng g component home

Update the home.component.ts to include the following:

import { Component, OnInit } from '@angular/core';
import { AuthenticationService } from 'src/app/authentication.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
  profile: any = {};
  name: string;

  constructor(private authenticationService: AuthenticationService) { }

  ngOnInit() {
    this.authenticationService.getUser().then(result => {
      if (result) {
          this.profile = result.profile;      
          this.name = this.profile.name;    
      }
    });
  }

}

and update the home.component.html to inlcude:

<div *ngIf="profile">
  <h3>User claims</h3>
  <p>These are the claims of the user who authenticated.</p>
  <pre>{{ profile | json }}</pre>
</div>

Add a Guard

Now that we have a service that can invoke the authentication flow, and a handler that can get the access_token and id_token, we want to wire up a Guard that will call the login() function of the authentication service to kick off the authentication flow:

ng g guard authentication

and add the following code:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthenticationService } from 'src/app/authentication.service';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationGuard implements CanActivate {
  constructor(private authenticationService: AuthenticationService){}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authenticationService.isUserAuthenticated().then((result) => {
            if (result) {
                return true;
            } else {               
                this.authenticationService.login();
                return false;
            }
        });
  }
}

The above code uses our authentication service to see if the user is already authenticated, and if not, it invokes the login() method which will kick off the authentication flow.

We will update the routing of the angular app in app.module.ts so that a user cannot get to the home component without first authenticating:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { AuthenticationGuard } from 'src/app/authentication.guard';
import { AuthenticationService } from 'src/app/authentication.service';
import { HomeComponent } from './home/home.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: '', canActivate: [AuthenticationGuard], children:[
        {path: '', redirectTo: 'home', pathMatch: 'full'},
        {path: 'home', component: HomeComponent }
      ]}
    ])
  ],
  providers: [
    AuthenticationService,
    AuthenticationGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The important part of this code is the RouterModule.forRoot(...) statement. We are saying that in order to activate a route, the canActivate of the AuthenticationGuard has to return true. As shown above, the AuthenticationGuard will only return true if the user is already logged in, if not, it will invoke the login() function kicking off the authentication flow.

Add a RouterOutlet

Since we have added a RouterModule for our app, we now have to specify a RouterOutlet so we have a place to emit the content of the activated component. Update your app.component.html so that is has the following code:

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<router-outlet></router-outlet>

The important part is the <router-outlet></router-outlet> statement.

Add a handler for silent renewal of tokens

Access tokens only live for a limited amount, in this case the client was registered and specified with a token lifetime of 20 minutes. This means after 20 minutes the access_token will be invalid. In order to get a new token, while the user is logged in, you can perform a silent renewal of the token. Once again, the oidc-client package takes care of the difficult parts for us, we simply have to specify in the config that we want auto renewal, and also specify the silent renewal redirect uri, which we did above when we created the authentication service. The last step is to add code to handle the callback. In the src directory add an html page silent.html, and add the following code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
    <script src="assets/oidc-client.min.js"></script>
    <script>       
        var mgr = new Oidc.UserManager();
        mgr.signinSilentCallback();
    </script>
</body>
</html>

Don't forget to add this page to the assets array in angular.json:

 "assets": [
    ...
    {
    "glob": "silent.html",
    "input": "src/",
    "output": "/"
    },
    ...
 ]

Run the sample

Once you have all that wired up, start your angular dev server:

ng serve

Then in your browser navigate to http://localhost:4200. You should be redirected to authenticate via Fabric.Identity, and once complete, redirected back to the home page of your application with the details of the user you authenticated as.

Sequence Diagram

The below sequence diagram details the interaction between four participants:

  1. The users browser (UserAgent)
  2. The client application (App)
  3. Fabric.Identity (FabricIdentity)
  4. A Protected API that the client application calls (ProtectedAPI)

NOTE: For a better view of the above diagram, right click and open it in a new tab.

Reference

http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth

Clone this wiki locally