Three Ways to Configure Modules in Your Angular App

Alisa - Feb 25 '22 - - Dev Community

Configurations are part of a developer's life. Configuration data is information your app needs to run and may include tokens for third-party systems or settings you pass into libraries. There are different ways to load configuration data as part of application initialization in Angular. Your requirements for configuration data might change based on needs. For example, you may have one unchanging configuration for your app, or you may need a different configuration based on the environment where the app is running. We'll cover a few different ways to load configuration values and identify when you should use each method.

We're covering specific use cases using Angular, so this post assumes you have some experience developing Angular apps. If you're interested in learning more about building your first Angular app, check out the Angular Getting Started docs or the links to tutorials that walk you through integrating Okta into Angular apps.

In this post, we'll cover the following forms of configuration:

  • defining configuration directly in code
  • defining configuration for different environments
  • loading configuration data via an API call

We'll show examples, including how to integrate with Okta, for each method. Also, we'll identify when to use each technique and what to watch out for.

Set up Angular and Okta in a sample project

First we'll set up the base project and Okta resources so you can follow along with the post.

To keep things on an even playing field and avoid any new Angular feature funny-business, I'll use an Angular v9 app in the code sample. All the outlined methods apply from Angular v7 to the current version, Angular v13.

Create the Angular app

You'll need a version of Node and npm that works for the Angular app version you want to create.

I'm using Node v14.18.1 and npm v6.14.15 to create an Angular v9 app, but you can create the app for your favorite Angular v7+ version.

Use your globally installed Angular CLI to create an Angular app with routing and standard CSS for styling by running:

ng new async-load --routing --style=css
Enter fullscreen mode Exit fullscreen mode

Or create the Angular v9 app by running the following command:

npx @angular/cli@9 new async-load --routing --style=css
Enter fullscreen mode Exit fullscreen mode

Create the Okta application

Let's create the Okta resource to have the configuration values we need to integrate.

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Single-Page App and press Enter.

Use http://localhost:4200/login/callback for the Redirect URI and set the Logout Redirect URI to http://localhost:4200.

What does the Okta CLI do?
The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:4200. You will see output like the following when it’s finished:
Okta application configuration:
Issuer:    https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
Enter fullscreen mode Exit fullscreen mode

NOTE: You can also use the Okta Admin Console to create your app. See Create an Angular App for more information.

Make a note of the Issuer and the Client ID. You will need them in the following steps.

We'll need the Okta Angular and Okta Auth JS libraries. Add them to your application by running the following command.

npm install @okta/okta-angular@5.1 @okta/okta-auth-js@6.0
Enter fullscreen mode Exit fullscreen mode

This post won't walk you through setting up sign-in and sign-out; we're interested only in setting up the configuration. If the Angular app runs without errors, the configuration aspect is correct. To see the types of errors we're trying to avoid, try excluding issuer or don't replace the {yourOktaDomain} with the values you got back from the Okta CLI. The sample code repo does have sign-in and sign-out integrated so you can see authentication working all the way through.


Define configuration in code

When your configuration is static the most straightforward way to configure libraries is to define the configuration directly in the code. In this method, you'd define the configuration data in the AppModule or in a feature module in this method. Examples of this method might look something like defining the configuration for routes and passing it into the RouterModule:

const routes: Routes = [
  { path: 'profile', component: ProfileComponent }
];

@NgModule({
  declarations: [ AppComponent, ProfileComponent ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes)
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

You might be surprised to see routing as an example of defining configuration directly in code. And yet, when you pass application-wide configuration into a module's forRoot() static method that's precisely what you're doing.

If you've followed many of our code examples and blog posts to integrate Okta into Angular apps, you've followed a similar pattern where configuration is defined directly in the application.

Your configuration code looks something like this:

import { OktaAuthModule, OKTA_CONFIG } from '@okta/okta-angular';
import { OktaAuth } from '@okta/okta-auth-js';

const oktaAuth = new OktaAuth({
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  clientId: '{yourClientId', 
  redirectUri: window.location.origin + '/login/callback'
});

@NgModule({
  declarations: [ AppComponent, ProfileComponent ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    OktaAuthModule
  ],
  providers: [
    { provide: OKTA_CONFIG, useValue: { oktaAuth } }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Summary of define configuration in code:

The most straightforward way to add configuration to your app is when the configuration does not change based on external factors.

When to use:

  • Use as often as you can since it's the easiest way to configure things.

Best use cases:

  • Static app configurations
  • Configuring third-party libraries
  • Quick tests

Watch out for:

  • Configuration that involves private keys or tokens

Configuration that changes by environment

Angular has a built-in way to support per-environment differences using the environments/environment.*.ts files. When serving locally, Angular CLI uses the values in environments/environment.ts, and when you build for production, Angular CLI substitutes environment.prod.ts instead. You can see this file substitution defined in the angular.json build configuration. And if you have more environments to support, you can customize the build configuration to suit your needs.

The environment files are helpful when you have different configurations you want to support at build time. Some examples include enabling user analytics only on prod environments or defining the API endpoints your QA environment calls.

src/main.ts contains an example of a configuration that changes based on the environment. Here you see the following:

import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}
Enter fullscreen mode Exit fullscreen mode

Angular utilizes the environment files to identify when to call the enableProdMode() method. Notice the file imports from ./environments/environment. That's because the build process handles that file swap.

Now let's look at how we'd use this when integrating with Okta.

In src/environments/environment.ts, add the Okta auth configuration like this.

export const environment = {
  production: false,
  authConfig: {
    issuer: 'https://{yourOktaDomain}/oauth2/default',
    clientId: '{yourClientId}'
  }
};
Enter fullscreen mode Exit fullscreen mode

In src/environments/environment.prod.ts, you'll add the same authConfig properties with values that match your prod environment.

You'll use the environment file to initialize the OktaAuthModule in the AppModule like this.

import { OktaAuthModule, OKTA_CONFIG } from '@okta/okta-angular';
import { OktaAuth } from '@okta/okta-auth-js';
import { environment } from '../environments/environment.ts';

const oktaAuth = new OktaAuth({
  ...environment.authConfig,
  redirectUri: window.location.orgin + '/login/callback'
});

@NgModule({
  declarations: [ AppComponent, ProfileComponent ],
  imports: [ BrowserModule, AppRoutingModule, OktaAuthModule ],
  providers: [
    { provide: OKTA_CONFIG, useValue: { oktaAuth }}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Summary of configuration that changes by environment:

Customizing environment files is the Angular recommended method to inject values during build time.

When to use:

  • You have different configuration values based on build output

Best use cases:

  • Devmode - keep apps served locally from doing things only prod apps should do
  • Multiple staging environment systems

Watch out for:

  • Configuration that involves private keys or tokens
  • Run the build for each environment to test changes you make. You don't want to miss adding a property and potentially get a runtime error.

Loading configurations from external APIs

Sometimes you need to load configuration at runtime. This makes sense if you use release promotion style deployments - creating a build for a staging/pre-production environment and promoting the same build to production after verification. You don't want to create a new build, but what if your staging and production environments require different configurations? Loading configuration from an external API is handy in scenarios like these.

To keep things simple for this external API configuration method, I'll focus only on the Okta example.

In this example, we'll look at src/main.ts where we bootstrap the Angular application. When you need configuration before the application loads, we can take advantage of platformBrowserDynamic() platform injector's extraProviders functionality. The extraProviders allows us to provide platform providers, much in the same way we can provide application-wide providers in the AppModule.

Since we need to make the server call to get the configuration before we have a full Angular application context, we use Web APIs to call the API. Then we can configure the provider for Okta's OKTA_CONFIG injection token.

For a configuration API call response that looks like this:

{
  "issuer": "https://{yourOktaDomain}/oauth2/default",
  "clientId": "{yourClientId}", 
  "redirectUri": "{correctHostForTheEnvironment}/login/callback"
}
Enter fullscreen mode Exit fullscreen mode

...the code in your src/main.ts changes to:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { OKTA_CONFIG } from '@okta/okta-angular';
import { OktaAuth } from '@okta/okta-auth-js';

if (environment.production) {
  enableProdMode();
}

fetch('http://{yourApiUri}/config').then(async res => {
  const authConfig = await res.json();

  platformBrowserDynamic([
    { provide: OKTA_CONFIG, useValue: {oktaAuth: new OktaAuth(authConfig)}}
  ]).bootstrapModule(AppModule)
    .catch(err => console.error(err));
});
Enter fullscreen mode Exit fullscreen mode

Then your AppModule only needs to import OktaAuthModule since you've already provided the OKTA_CONFIG injection token.

If you need to create the callback URI programmatically or if you need to use the configuration in multiple places, you can store the configuration in the app instead. The minimum we need is a class that holds the config, which we will show in the example. You can wrap the config in a service if your needs are more involved than what we'll show here.

You'll add a new file and create an interface that matches the response, as well as a class to hold the config:

export interface AuthConfig {
  issuer: string;
  clientId: string;
}

export class OktaAuthConfig {
  constructor(public config: AuthConfig) { }
}
Enter fullscreen mode Exit fullscreen mode

Edit the src/main.ts to provide the OktaAuthConfig class instead

import { OktaAuthConfig } from './app/auth-config';

fetch('http://{yourApiUri}/config').then(async res => {
  const authConfig = new OktaAuthConfig(await res.json());

  platformBrowserDynamic([
    { provide: OktaAuthConfig, useValue: authConfig }
  ]).bootstrapModule(AppModule)
  .catch(err => console.error(err));
})
Enter fullscreen mode Exit fullscreen mode

In the AppModule you can provide the OKTA_CONFIG needed to integrate with Okta by accessing OktaAuthConfig:

@NgModule({
  declarations: [ AppComponent, ProfileComponent ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    OktaAuthModule
  ],
  providers: [
    {
      provide: OKTA_CONFIG,
      deps: [OktaAuthConfig],
      useFactory: (oktaConfig: OktaAuthConfig) => ({
        oktaAuth: new OktaAuth({
          ...oktaConfig.config,
          redirectUri: window.location.origin + '/login/callback'
        })
      })
    }
  ]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

You can now load a config from an API and use the app's location.

You might be asking yourself, "Isn't there an APP_INITIALIZER token or something we can use instead"? Well, yes, there is an APP_INITIALIZER token for executing initialization functions that complete before application initialization completes. However, in our case, we need the auth configuration in order to initialize. So, we need to finish loading the configuration before initializing the app, which we can do when bootstrapping.

Summary of loading configuration from an external API:

Load configuration from an API and provide the config to the application. Depending on your needs, the configuration loading may occur during bootstrapping or via APP_INITIALIZER.

When to use:

  • You want the configuration to load at runtime instead of being baked into the code
  • Your configuration properties include private information that you don't want to commit to source control

Best use cases:

  • You have different configurations for staging and prod and use release-promotion style deployment processes
  • Your configuration changes frequently or often enough where building and deploying the app is not feasible

Watch out for:

  • Configuration errors or network blips - your app will not run since it's dependent on the external API.
  • Anything that can decrease application load speed such as overly large configuration response, calling too many endpoints, or slow server response.
  • Potential challenges with verification and testing, since configuration may change.

Learn more about Angular

I hope this post is helpful as you consider how to integrate Okta into your Angular app. You can check out the sample code for loading configurations from an external server, along with a minimal Express API to simulate the config loading.

GitHub logo oktadev / okta-angular-async-load-example

Loading Okta configuration from an external API in an Angular app

If you liked this post, check out the following.

Don't forget to follow us on Twitter and subscribe to our YouTube channel for more exciting content. We also want to hear from you about what tutorials you want to see. Leave us a comment below.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .