Securing Angular applications using the OpenID Connect Code Flow with PKCE

In this post, I show how an Angular application could be secured using the OpenID Connect Code Flow with Proof Key for Code Exchange (PKCE).

The Angular application uses the OIDC lib angular-auth-oidc-client. In this example, the src code is used directly, but you could also use the npm package. Here’s an example which uses the npm package.

Code https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow

lib src: https://github.com/damienbod/angular-auth-oidc-client

npm package: https://www.npmjs.com/package/angular-auth-oidc-client

Configuring the Angular client

The Angular application loads the configurations from a configuration json file. The response_type is set to “code”. This defines the OpenID Connect (OIDC) flow. PKCE is always used, as this is a public client which cannot keep a secret.

The other configurations must match the OpenID Connect client configurations on the server.

"ClientAppSettings": {
    "stsServer": "https://localhost:44318",
    "redirect_url": "https://localhost:44352",
    "client_id": "angular_code_client",
    "response_type": "code",
    "scope": "dataEventRecords securedFiles openid profile",
    "post_logout_redirect_uri": "https://localhost:44352",
    "start_checksession": true,
    "silent_renew": true,
    "startup_route": "/dataeventrecords",
    "forbidden_route": "/forbidden",
    "unauthorized_route": "/unauthorized",
    "log_console_warning_active": true,
    "log_console_debug_active": true,
    "max_id_token_iat_offset_allowed_in_seconds": 10,
}

The Angular application reads the configuration in the app.module and initializes the security lib.

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

import { AuthModule } from './auth/modules/auth.module';
import { OidcSecurityService } from './auth/services/oidc.security.service';
import { OpenIDImplicitFlowConfiguration } from './auth/modules/auth.configuration';
import { OidcConfigService } from './auth/services/oidc.security.config.service';
import { AuthWellKnownEndpoints } from './auth/models/auth.well-known-endpoints';

// Add then other imports, config you need

export function loadConfig(oidcConfigService: OidcConfigService) {
    console.log('APP_INITIALIZER STARTING');
    return () => oidcConfigService.load(`${window.location.origin}/api/ClientAppSettings`);
}

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        routing,
        HttpClientModule,
        AuthModule.forRoot(),
    ],
    declarations: [
        AppComponent,
    ],
    providers: [
        OidcConfigService,
        OidcSecurityService,
        {
            provide: APP_INITIALIZER,
            useFactory: loadConfig,
            deps: [OidcConfigService],
            multi: true
        },
        Configuration
    ],
    bootstrap: [AppComponent],
})

export class AppModule {

    constructor(
        private oidcSecurityService: OidcSecurityService,
        private oidcConfigService: OidcConfigService,
        configuration: Configuration
    ) {
        this.oidcConfigService.onConfigurationLoaded.subscribe(() => {

            const openIDImplicitFlowConfiguration = new OpenIDImplicitFlowConfiguration();
            openIDImplicitFlowConfiguration.stsServer = this.oidcConfigService.clientConfiguration.stsServer;
            openIDImplicitFlowConfiguration.redirect_url = this.oidcConfigService.clientConfiguration.redirect_url;
            // The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer
            // identified by the iss (issuer) Claim as an audience.
            // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
            // or if it contains additional audiences not trusted by the Client.
            openIDImplicitFlowConfiguration.client_id = this.oidcConfigService.clientConfiguration.client_id;
            openIDImplicitFlowConfiguration.response_type = this.oidcConfigService.clientConfiguration.response_type;
            openIDImplicitFlowConfiguration.scope = this.oidcConfigService.clientConfiguration.scope;
            openIDImplicitFlowConfiguration.post_logout_redirect_uri = this.oidcConfigService.clientConfiguration.post_logout_redirect_uri;
            openIDImplicitFlowConfiguration.start_checksession = this.oidcConfigService.clientConfiguration.start_checksession;

            openIDImplicitFlowConfiguration.silent_renew = this.oidcConfigService.clientConfiguration.silent_renew;
            openIDImplicitFlowConfiguration.silent_renew_url = this.oidcConfigService.clientConfiguration.redirect_url + '/silent-renew.html';

            openIDImplicitFlowConfiguration.post_login_route = this.oidcConfigService.clientConfiguration.startup_route;
            // HTTP 403
            openIDImplicitFlowConfiguration.forbidden_route = this.oidcConfigService.clientConfiguration.forbidden_route;
            // HTTP 401
            openIDImplicitFlowConfiguration.unauthorized_route = this.oidcConfigService.clientConfiguration.unauthorized_route;
            openIDImplicitFlowConfiguration.log_console_warning_active = this.oidcConfigService.clientConfiguration.log_console_warning_active;
            openIDImplicitFlowConfiguration.log_console_debug_active = this.oidcConfigService.clientConfiguration.log_console_debug_active;
            // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time,
            // limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific.
            openIDImplicitFlowConfiguration.max_id_token_iat_offset_allowed_in_seconds =
                this.oidcConfigService.clientConfiguration.max_id_token_iat_offset_allowed_in_seconds;

            // openIDImplicitFlowConfiguration.iss_validation_off = false;
            configuration.FileServer = this.oidcConfigService.clientConfiguration.apiFileServer;
            configuration.Server = this.oidcConfigService.clientConfiguration.apiServer;

            const authWellKnownEndpoints = new AuthWellKnownEndpoints();
            authWellKnownEndpoints.setWellKnownEndpoints(this.oidcConfigService.wellKnownEndpoints);

            this.oidcSecurityService.setupModule(openIDImplicitFlowConfiguration, authWellKnownEndpoints);

        });

        console.log('APP STARTING');
    }
}

The redirect request with the code from the secure token server (STS) needs to be handled inside the Angular application. This is done in the app.component.

If the redirected URL from the server has the code and state parameters, and the state is valid, the tokens are requested from the STS server. The tokens in the response are validated as defined in the OIDC specification.

private doCallbackLogicIfRequired() {
   console.log(window.location);
   // Will do a callback, if the url has a code and state parameter.
   this.oidcSecurityService.authorizedCallbackWithCode(window.location.toString());
}

or if you want more control, or have specific logic:

private doCallbackLogicIfRequired() {
        console.log(window.location);
   
        const urlParts = window.location.toString().split('?');
        const params = new HttpParams({
            fromString: urlParts[1]
        });
        const code = params.get('code');
        const state = params.get('state');
        const session_state = params.get('session_state');

        if (code && state && session_state) {
            this.oidcSecurityService.requestTokensWithCode(code, state, session_state);
        }
}

IdentityServer4 is used to configure and implement the secure token server. The client is configured to use PKCE and no secret. The client ID must match the Angular application configuration.

new Client
{
	ClientName = "angular_code_client",
	ClientId = "angular_code_client",
	AccessTokenType = AccessTokenType.Reference,
	// RequireConsent = false,
	AccessTokenLifetime = 330,// 330 seconds, default 60 minutes
	IdentityTokenLifetime = 30,

	RequireClientSecret = false,
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,

	AllowAccessTokensViaBrowser = true,
	RedirectUris = new List<string>
	{
		"https://localhost:44352",
		"https://localhost:44352/silent-renew.html"

	},
	PostLogoutRedirectUris = new List<string>
	{
		"https://localhost:44352/unauthorized",
		"https://localhost:44352"
	},
	AllowedCorsOrigins = new List<string>
	{
		"https://localhost:44352"
	},
	AllowedScopes = new List<string>
	{
		"openid",
		"dataEventRecords",
		"dataeventrecordsscope",
		"securedFiles",
		"securedfilesscope",
		"role",
		"profile",
		"email"
	}
},

Silent Renew

The tokens are refreshed using an iframe like the OpenID Connect Implicit Flow. The HTML has one small difference. The detail value of the event returned to the Angular application returns the URL and not just the hash.

<!doctype html>
<html>
<head>
    <base href="./">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>silent-renew</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

    <script>
        window.onload = function () {
            /* The parent window hosts the Angular application */
            var parent = window.parent;
            /* Send the id_token information to the oidc message handler */
            var event = new CustomEvent('oidc-silent-renew-message', { detail: window.location });
            parent.dispatchEvent(event);
        };
    </script>
</body>
</html>

Now the OIDC Flow can be used in the Angular client application. When the application is started, the configurations are loaded.

The authorize request is sent to the STS with the code_challenge and the code_challenge_method.

https://localhost:44318/connect/authorize?
client_id=angular_code_client
&redirect_uri=https%3A%2F%2Flocalhost%3A44352
&response_type=code
&scope=dataEventRecords%20securedFiles%20openid%20profile
&nonce=N0.55639781033142241546880026878
&state=15468798618260.8857500931703779
&code_challenge=vBcZBGqBEcQAA3HYf_nSWy6jViRjtGQyiqrrZYUdHHU
&code_challenge_method=S256
&ui_locales=de-CH

The STS redirects back to the Angular application with the code and state.

https://localhost:44352/?
code=2ee056b556db7dcd5c936686c4b30056e7efd78046eb4e8d4f57c3f6cc638449
&scope=openid%20profile%20dataEventRecords%20securedFiles
&state=15468798618260.8857500931703779
&session_state=xQzQduNOGHP7Qh8l5Pjs02piChWuSawPBpDhb2vCmqo.8cccc6873860ec345ea65ead4233c4ee

The client application then requests the tokens using the code:

HTTP POST
https://localhost:44318/connect/token

BODY
grant_type=authorization_code
&client_id=angular_code_client
&code_verifier=C0.8490756539574429154688002688015468800268800.23727863075955402
&code=2ee056b556db7dcd5c936686c4b30056e7efd78046eb4e8d4f57c3f6cc638449
&redirect_uri=https://localhost:44352

The tokens are then returned and validated. The silent renew works in the same way.

Notes

The Angular application works now using OIDC Code Flow with PKCE to authenticate and authorize, but requires other security protections such as CSP, HSTS XSS protection, and so on. This is a good solution for Angular applications which uses APIs from any domain.

Links

https://tools.ietf.org/html/rfc7636

https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest

https://www.npmjs.com/package/angular-auth-oidc-client

8 comments

  1. […] Securing Angular applications using the OpenID Connect Code Flow with PKCE (Damien Bowden) […]

  2. […] Securing Angular applications using the OpenID Connect Code Flow with PKCE Damien Bowden (@damien_bod) […]

  3. Jawahar Kaliraj Mariappan · · Reply

    Hi, Can this approach be used against Azure AD (and with implicit flow disabled) please?

    Thanks
    Jawahar

    1. Hi Jawahar

      As far as I know, the answer is no. Azure AD does not support this flow yet.

      Greetings Damien

      1. Jawahar Kaliraj Mariappan · ·

        Thanks Damien.

      2. jawahar75 · ·

        Hi Damien,

        It appears that latest Azure AD (i.e. Microsoft Identity platform – https://docs.microsoft.com/en-us/azure/active-directory/develop/about-microsoft-identity-platform) supports “authorization code flow” now. (https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)

        Am I right or missing something here please?

        Thanks
        Jawahar

  4. jawahar75 · · Reply

    Hi Damien,

    It appears that latest Azure AD (i.e. Microsoft Identity platform – https://docs.microsoft.com/en-us/azure/active-directory/develop/about-microsoft-identity-platform) supports “authorization code flow” now. (https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)

    Am I right or missing something here please?

    Thanks
    Jawahar

    1. jawahar75 · · Reply

      Hi Damien, Please ignore this comment as I have replied to relevant comment thread.

      Thanks
      Jawahar

Leave a Reply to Dew Drop - January 10, 2018 (#2876) - Morning Dew Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: