Mendhak / Code

Setting up an Auth0 secured Angular application with dynamic runtime loaded configuration

How to set up an Angular application. Secured with Auth0 logins and protected API requests. With the configuration loaded dynamically via a web request.

When setting up a new Angular project, one of the first things you should do is set up its security integration and load application configuration dynamically from a web request.

Setting up the login and protecting API calls with OAuth up front is useful because they are non-trivial tasks, which makes it much less painful in the beginning, as opposed to adjusting the application for it later.

Loading the frontend configuration from your backend API is useful as it allows building the frontend once and deploying everywhere by removing environment specific settings from the frontend code; since the backend API runs serverside, it can pick up and expose any environment variables as needed to the frontend.

Concept

This writeup is accompanied by a sample repo, you can jump straight to it and run it to see the above concepts in action.

Generate a new Angular application

Create a new project directory, then generate the frontend Angular application using the ng cli, remember to use npx

mkdir myproject
cd myproject
npx -p @angular/cli ng --style=scss --routing=true --skipGit=true new frontend

Run it, and browse http://localhost:4200/, to make sure it’s working as expected.

npm --prefix frontend start

New Angular Application

Auth0.com Application Setup

If you haven’t already, sign up for a free Auth0.com account and create a tenant. For this example I have created mydemotenant.
In the tenant’s Applications settings, create a new application of type Single Page Application. This application will represent your Angular application.

New Auth0 Application

Auth0 generates a Client ID for you which you will need shortly.

Auth0 Client ID

You’ll also need to tell Auth0 where your application’s requests will be coming from. On the application page, add http://localhost:4200 to the Allowed Callback URLs, Logout URLs and Web Origins, then click Save Changes.

Allowed URLs

Angular integration with Auth0

Now configure the Angular application to interact with Auth0. Auth0 provides a convenience library, auth0-angular which takes care of a lot of integration aspects for you.

Integrating will require installing the library, configuring the library in the Angular module, then calling its login/logout methods. Start by installing the library:

npm --prefix frontend install @auth0/auth0-angular

Next, in app.module.ts, import the library.

import { AuthModule } from '@auth0/auth0-angular';

In the imports: section, add a line for AuthModule, substituting your Domain and ClientId from above. This will be made dynamic later (you should use different tenants for testing and production), but hardcoded for now.

AuthModule.forRoot({
    domain: 'mydemotenant.eu.auth0.com',
    clientId: '89eVpU4Ixox4Llx6j7466L7pnK9lO4A8',
}),

Logging in and out

In app.component.ts, import the AuthService.

import { AuthService } from '@auth0/auth0-angular';

Inject AuthService in the constructor, and set up the login and logout methods.

 constructor(public auth: AuthService) {}

  loginWithRedirect(): void {
    this.auth.loginWithRedirect();
  }

  logout(): void {
    this.auth.logout({ returnTo: window.location.origin });
  }

In app.component.html, delete everything except the <router-outlet></router-outlet>. Then add a bit of code which logs the user in/out, and display some info about the user.


<p>This is the 'home page'</p>

<button *ngIf="(auth.isAuthenticated$ | async) === false"  (click)="loginWithRedirect()">
  Log in
</button>

<button *ngIf="auth.isAuthenticated$ | async" (click)="logout()">
  Log out
</button>

<div *ngIf="auth.user$ | async as user">
Some info about you:
    <ul *ngIf="auth.user$ | async as user" >
    <li>Name: {{ user.name }}</li>
    <li>Email: {{ user.email }}</li>
    </ul>
</div>

Reload the page and click the Login button. If everything is configured correctly, you are redirected to mydemotenant on Auth0 where you can login/signup and come back to the application.

Login page

On return to the application the email you signed up with is displayed on the page.

Moving frontend configuration to the backend

Instead of hardcoding the domain and clientId in the Angular app.module.ts, these values should be supplied at runtime. This is because you should use a different tenant for local development, testing and production. If you leave the values hardcoded you would need to build the application for each environment that you deploy to (a major shortcoming of all SPA frameworks). It is possible to get Angular to load the Auth0 configuration, along with any other settings you’d want, from a backend API server.

Create the Backend API

Start by generating a Node Express API. In a new terminal window,

npx express-generator api

This creates a folder called api with a basic Express project in it. Install its dependencies and start it up.

npm --prefix api install
npm --prefix api start

Once it’s done, browse to http://localhost:3000 to make sure it’s working as expected.

Express API

Create an endpoint for frontend settings

In the Express app’s index.js, add a new /uiconfig endpoint, which will return settings to the frontend.

router.get('/uiconfig', function(req, res, next) {
  res.send({
    domain: 'mydemotenant.eu.auth0.com',
    clientId: '89eVpU4Ixox4Llx6j7466L7pnK9lO4A8',
  });
});

In a real application scenario, you would load the domain, clientId, and various other settings from environment variables.

Restart the Express app, then browse to http://localhost:3000/uiconfig. You should see a JSON response with the Auth0 configuration settings.

UI Config

Loading Angular configuration from a backend API call

Now that the Express API is serving values for the frontend on its /uiconfig endpoint, there’s work to do on the Angular side to read it and load it.

Proxy calls to the Express API

Because the frontend and backend are currently on different domains (localhost:4000 and localhost:3000) you will have to start dealing with CORS issues. It’s actually easier to just get Angular to proxy all calls to the Express APIs (localhost:3000) as a path on the frontend. In other words, we can get all /api calls from the frontend code to request http://localhost:3000 behind the scenes. This does away with cross domain issues.

In the frontend folder, open angular.json and search for the "serve":" section. Add a proxyConfig line under serve > options.

        "serve": {
            ...
          "options": {
            ...
            "proxyConfig": "./proxy.conf.json"
            ...
          },

Create a proxy.conf.json with this content.

{
    "/api": {
      "target": "http://localhost:3000",
      "secure": false,
      "pathRewrite": {
        "^/api": ""
      },
      "logLevel": "debug"
    }
}

Stop and restart the Angular application.

# Ctrl+C
npm --prefix frontend start

Now browse to http://localhost:4200/api/uiconfig and it should show the same contents as http://localhost:3000/uiconfig.

UI Config via Proxy

Angular loading dynamic configuration

Start by removing the hardcoded values from the AuthModule.forRoot() line. It should just be

AuthModule.forRoot()

At the top, import APP_INITIALIZER and the HttpClientModule too

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

In the providers:[] section, add an APP_INITIALIZER, which will call an AppConfigService (we will create this soon):

 providers: [
    AppConfigService,
    { provide: APP_INITIALIZER,useFactory: initializeApp, deps: [AppConfigService], multi: true}
  ],

The initializeApp should be a normal function just outside the @NgModule.

import { AppConfigService } from './app-config.service';


export function initializeApp(appConfigService: AppConfigService) {
  return (): Promise<any> => { 
    return appConfigService.load();
  }
}

Finally create the app-config.service.ts which will do the real work of loading from /api/uiconfig. This AppConfigService has a special purpose. It is meant not just for Auth0 configuration, but for any settings that need to be available to any of our Angular application components. The idea is that just by importing this service, an Angular component can access its properties using AppConfigService.settings.someSettingName. Here are the contents of app-config.service.ts:

import { Injectable }  from '@angular/core';
import { HttpClient, HttpBackend } from '@angular/common/http';
import { AuthClientConfig, AuthConfig, AuthConfigService } from '@auth0/auth0-angular';

@Injectable()
export class AppConfigService {
    static settings: IAppConfig;
    httpClient: HttpClient;
    handler: HttpBackend;
    authClientConfig: AuthClientConfig;

    constructor(private http: HttpClient, handler: HttpBackend, authClientConfig: AuthClientConfig) {
        this.httpClient = http;
        this.handler = handler;
        this.authClientConfig = authClientConfig;
    }

    load() {

        const jsonFile = `/api/uiconfig`;
        return new Promise<void>((resolve, reject) => {
            this.httpClient = new HttpClient(this.handler);
            this.httpClient.get(jsonFile).toPromise().then((response : IAppConfig) => {
               AppConfigService.settings = <IAppConfig>response;

               this.authClientConfig.set({ 
                clientId: AppConfigService.settings.clientId, domain: AppConfigService.settings.domain
                });

               console.log('Config Loaded');
               console.log( AppConfigService.settings);
               resolve();
               
            /*}).catch((response: any) => {
               reject(`Could not load the config file`);*/
            });
        });
    }
}

export interface IAppConfig {
    clientId: string
    domain: string
}

A few things to note about this service

Try it

That was a lot of work but now you can reload the page, and this time watch developer tools. You will see a request being made to /api/uiconfig, and the config is printed out to console. The application’s login and logout functionality should work as normal.

Dynamic configuration

Securing API calls

So far everything done has been to secure the application frontend for a user, with login and logout functionality and some user identity information. Securing API calls requires additional steps - the frontend application must request an Access Token on behalf of the user, and pass that along as an Authorization: Bearer header. Here we will create a secure endpoint in Express, and call it from the frontend.

Auth0.com API setup

Back in Auth0.com in your tenant, go to the API section and create a new API, and give it an audience. The audience can be anything, including a URL, but I prefer normal words like my-api.

Auth0 API

Express secure endpoint

Stop the Express app, and install some additional libraries.

# Ctrl+C
npm --prefix api install --save express-jwt jwks-rsa express-jwt-authz

In index.js, import the libraries and add a middleware that expects and validates the JSON Web Token in the Authorization header. Substitute the tenant domain and audience for your own.

const jwt = require('express-jwt');
const jwtAuthz = require('express-jwt-authz');
const jwksRsa = require('jwks-rsa');

const checkJwt = jwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://mydemotenant.eu.auth0.com/.well-known/jwks.json`
  }),

  audience: 'my-api',
  issuer: `https://mydemotenant.eu.auth0.com/`,
  algorithms: ['RS256']
});

Now create a secure endpoint that uses the above.

router.get('/api/protected', checkJwt, function(req, res) {
  res.json({
    message: 'This is a protected endpoint.'
  });
});

Restart the Express app

npm --prefix api start

Then browse to the protected endpoint at http://localhost:3000/protected, you should get an HTTP 401 Unauthorized error, as you haven’t passed any headers in.

401

Make the frontend a first-party application

The frontend needs to request Access Tokens on behalf of the user, but this needs to be done in a way that isn’t disruptive to the user experience. Auth0 APIs do allow skipping consent, but only for first party applications.

This requires two changes to the frontend application:

Modify Auth0.com Application URLs

In the Auth0.com tenant settings, modify the application’s callback, login and logout URLs to use https://frontend.example:4200.

Auth0 Configuration

Host file

Edit your hosts file and add a mapping.

127.0.0.1  frontend.example

Certificate

Generate a self signed certificate for frontend.example.

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/C=GB/ST=London/L=London/O=Acme/OU=Org/CN=frontend.example"

This generates a certificate and a private key. Modify angular.json to use these. In the same serve > options section where you added a proxy config, add:

"ssl": true,
"sslKey": "../key.pem",
"sslCert": "../cert.pem",
"host": "0.0.0.0",
"disableHostCheck": true,

This allows the Angular application to be served over frontend.example, and uses the generated self signed certificate.

Stop and restar the Angular application.

# Ctrl C
npm --prefix frontend start

Open https://frontend.example:4200/ in the browser. Accept the warning about the self signed certificate. Try out the login and logout functionality, everything should work as before including the dynamic configuration loading.

First Party with Cert

Configure Auth0 library to secure calls to /api

At last the juicy bit. We now need to get Auth0 to intercept our HTTP requests and add the required Authorization header.

In app.module.ts, import the Angular and Auth0 interceptors.

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthHttpInterceptor } from '@auth0/auth0-angular';

Add the HTTP_INTERCEPTORS to the providers:[...] section, so it should now look like this:

 providers: [
    AppConfigService,
    { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true },
    { provide: APP_INITIALIZER,useFactory: initializeApp, deps: [AppConfigService], multi: true}
  ],

Back in the app-config.service.ts, where the Auth0 Configuration is being set, include the httpInterceptor. The configuration is very simple, you just specify a part of the API URL, and which audience and scopes to use.

In our case, the path is /api/* and the audience is my-api.

this.authClientConfig.set({ 
    clientId: AppConfigService.settings.clientId, domain: AppConfigService.settings.domain,
    httpInterceptor: { allowedList: [
        {
            uri: "/api/*",
            tokenOptions: {
                audience: "my-api"
            }
        }
    ] }
    });

Make a call to the API

Modify the constructor in app.component.ts and have it call the API with a normal http.get. Our configuration above will take care of intercepting it.

  public secureMessage;

  constructor(public auth: AuthService, private http: HttpClient) {
    this.getSecureMessage();
  }

  getSecureMessage(){
    this.auth.isAuthenticated$.subscribe(isLoggedIn => {
      if(isLoggedIn){
        this.http.get('/api/protected').subscribe(result => this.secureMessage=result);
      }
    });
  }

Don’t forget to import the HttpClient.

import { HttpClient } from '@angular/common/http';

Edit the app.component.html and display the message returned from the protected backend in the HTML.


<div *ngIf="secureMessage">{{ secureMessage.message }}</div>

Refresh the frontend page and the message “This is a protected endpoint” appears if you’re logged in. Refresh once more and watch the network traffic in developer tools. Note that the Auth0 authorize and token exchanges happen twice.

Secure API call

The first exchange is for your normal authentication check (which is how the username and email are displayed). The response contains a JWT ID Token, but an opaque access token which isn’t of much use to us. The second exchange is when the http.get call is about to be made - the library requests an Access Token with the my-api audience, and a JWT Access Token is in the response. You can then see the Authorization: Bearer header passing that Access Token along to the protected endpoint which allows access.

Finishing notes

There were a lot of steps involved here and these are all needed early in during Angular + OAuth project setups.

I’ve covered:

Once this is done, the project can then be used for ‘normal’ development activity in a secure way.