Angular OIDC OAuth2 client with Google Identity Platform

This article shows how an Angular client could implement a login for a SPA application using Google Identity Platform OpenID. The Angular application uses the npm package angular-auth-oidc-client to implement the OpenID Connect Implicit Flow to connect with the google identity platform.

Code: https://github.com/damienbod/angular-auth-oidc-sample-google-openid

History

2020-05-03 Updated to OIDC lib version 11.0.0, Angular 9.1.4, ASP.NET Core 3.1

2017-07-09 Updated to version 1.1.4, new configuration

Setting up Google Identity Platform

The Google Identity Platform provides good documentation on how to set up its OpenID Connect implementation.

You need to login into google using a gmail account.
https://accounts.google.com

Now open the OpenID Connect google documentation page

https://developers.google.com/identity/protocols/OpenIDConnect

Open the credentials page provided as a link.

https://console.developers.google.com/apis/credentials

Create new credentials for your application, select OAuth Client ID in the drop down:

Select a web application and configure the parameters to match your client application URLs.

Implementing the Angular OpenID Connect client

The client application is implemtented using ASP.NET Core and Angular.

The npm package angular-auth-oidc-client is used to connect to the OpenID server. The package can be added to the package.json file in the dependencies.

"dependencies": {
    ...
    "angular-auth-oidc-client": "11.0.0"
},

Now the AuthModule, OidcSecurityService, AuthConfiguration can be imported. The AuthModule.forRoot() is used and added to the root module imports, the OidcSecurityService is added to the providers and the AuthConfiguration is the configuration class which is used to set up the OpenID Connect Implicit Flow.

import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AuthModule, LogLevel, OidcConfigService, OidcSecurityService } from 'angular-auth-oidc-client';
import { AppComponent } from './app.component';
import { routing } from './app.routes';
import { AutoLoginComponent } from './auto-login/auto-login.component';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';

export function configureAuth(oidcConfigService: OidcConfigService) {
    return () =>
        oidcConfigService.withConfig({
            stsServer: 'https://accounts.google.com',
            redirectUrl: window.location.origin,
            clientId: '188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
            responseType: 'id_token token',
            scope: 'openid email profile',
            triggerAuthorizationResultEvent: true,
            postLogoutRedirectUri: window.location.origin + '/unauthorized',
            startCheckSession: false,
            silentRenew: false,
            silentRenewUrl: window.location.origin + '/silent-renew.html',
            postLoginRoute: '/home',
            forbiddenRoute: '/forbidden',
            unauthorizedRoute: '/unauthorized',
            logLevel: LogLevel.Debug,
            historyCleanupOff: true,
            // iss_validation_off: false
            // disable_iat_offset_validation: true
        });
}

@NgModule({
    imports: [BrowserModule, FormsModule, routing, HttpClientModule, AuthModule.forRoot()],
    declarations: [AppComponent, ForbiddenComponent, HomeComponent, AutoLoginComponent, NavigationComponent, UnauthorizedComponent],
    providers: [
        OidcSecurityService,
        OidcConfigService,
        {
            provide: APP_INITIALIZER,
            useFactory: configureAuth,
            deps: [OidcConfigService],
            multi: true,
        },
    ],
    bootstrap: [AppComponent],
})
export class AppModule {}

The AuthConfiguration class is used to configure the module.

stsServer
This is the URL where the STS server is located. We use https://accounts.google.com in this example.

redirectUrl
This is the redirect_url which was configured on the google client ID on the server.

clientId
The client_id must match the Client ID for Web application which was configured on the google server.

responseType
This must be ‘id_token token’ or ‘id_token’. If you want to use the user service, or access data using using APIs, you must use the ‘id_token token’ configuration. This is the OpenID Connect Implicit Flow. The possible values are defined in the well known configuration URL from the OpenID Connect server.

scope
Scope which are used by the client. The openid must be defined: ‘openid email profile’

postLogoutRedirectUri
Url after a server logout if using the end session API. This is not supported by google OpenID.

startChecksession
Checks the session using OpenID session management. Not supported by google OpenID

silentRenew
Renews the client tokens, once the token_id expires.

startupRoute
Angular route after a successful login.

forbiddenRoute
HTTP 403

unauthorizedRoute
HTTP 401

logLevel
Logs all module warnings to the console.

export function configureAuth(oidcConfigService: OidcConfigService) {
    return () =>
        oidcConfigService.withConfig({
            stsServer: 'https://accounts.google.com',
            redirectUrl: window.location.origin,
            clientId: '188968487735-b1hh7k87nkkh6vv84548sinju2kpr7gn.apps.googleusercontent.com',
            responseType: 'id_token token',
            scope: 'openid email profile',
            triggerAuthorizationResultEvent: true,
            postLogoutRedirectUri: window.location.origin + '/unauthorized',
            startCheckSession: false,
            silentRenew: false,
            silentRenewUrl: window.location.origin + '/silent-renew.html',
            postLoginRoute: '/home',
            forbiddenRoute: '/forbidden',
            unauthorizedRoute: '/unauthorized',
            logLevel: LogLevel.Debug,
            historyCleanupOff: true,
            // iss_validation_off: false
            // disable_iat_offset_validation: true
        });
}

The following json is the actual configuration for the google well known configuration. What’s really interesting is that the end session endpoint is not supported, which is strange I think. It’s also interesting to see that the response_types_supported supports a type which is not supported “token id_token”, this should be “id_token token”.

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

{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://www.googleapis.com/oauth2/v4/token",
  "userinfo_endpoint": "https://www.googleapis.com/oauth2/v3/userinfo",
  "revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "response_types_supported": [
    "code",
    "token",
    "id_token",
    "code token",
    "code id_token",
    "token id_token",
    "code token id_token",
    "none"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "profile"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "client_secret_basic"
  ],
  "claims_supported": [
    "aud",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "iss",
    "locale",
    "name",
    "picture",
    "sub"
  ],
  "code_challenge_methods_supported": [
    "plain",
    "S256"
  ]
}

The AppComponent implements the authorize and the authorizedCallback functions from the OidcSecurityService provider.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';

@Component({
    selector: 'app-root',
    templateUrl: 'app.component.html',
})
export class AppComponent implements OnInit, OnDestroy {
    constructor(public oidcSecurityService: OidcSecurityService, private router: Router) {}

    ngOnInit() {
        this.oidcSecurityService
            .checkAuth()

            .subscribe((isAuthenticated) => {
                if (!isAuthenticated) {
                    if ('/autologin' !== window.location.pathname) {
                        this.write('redirect', window.location.pathname);
                        this.router.navigate(['/autologin']);
                    }
                }
                if (isAuthenticated) {
                    this.navigateToStoredEndpoint();
                }
            });
    }

    ngOnDestroy(): void {}

    login() {
        console.log('start login');
        this.oidcSecurityService.authorize();
    }

    refreshSession() {
        console.log('start refreshSession');
        this.oidcSecurityService.authorize();
    }

    logout() {
        console.log('start logoff');
        this.oidcSecurityService.logoff();
    }

    private navigateToStoredEndpoint() {
        const path = this.read('redirect');

        if (this.router.url === path) {
            return;
        }

        if (path.toString().includes('/unauthorized')) {
            this.router.navigate(['/']);
        } else {
            this.router.navigate([path]);
        }
    }

    private read(key: string): any {
        const data = localStorage.getItem(key);
        if (data != null) {
            return JSON.parse(data);
        }

        return;
    }

    private write(key: string, value: any): void {
        localStorage.setItem(key, JSON.stringify(value));
    }
}

Running the application

Start the application using IIS Express in Visual Studio 2017. This starts with https://localhost:44386 which is configured in the launch settings file. If you use a differnt URL, you need to change this in the client application and also the servers client credentials configuration.

Login then with your gmail.

And you are redirected back to the SPA.

Links:

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

https://developers.google.com/identity/protocols/OpenIDConnect

23 comments

  1. Damien, I noticed that you say Google block the downloading of the configuration of the OIDC configuration from the .well-known/openid-configuration endpoint, and therefore the downloading of it, and saving of it to a local file.

    For providers that does support this, will your library automatically download the configuration from the .well-known/openid-configuration endpoint – ie. we do not have to download it and save it locally first?

    1. Hi Jerrie, at present, I do this manually and add it to the using project. It is not part of the npm lib. The lib allows you to configure the src if required.

      Google blocks it for a browser application; CORS. You can download it from a desktop application or direct in the browser.
      Greetings Damien

  2. […] Angular OIDC OAuth2 client with Google Identity Platform  […]

  3. Hi Damien, I noticed that your example says version 0.0.8, and I am getting 1.1.4. Could that be the reason that all configuration items in module constructor are throwing an error “Cannot assign to ” because it is a constant or a read-only property.”.
    Of course, it could be something else as well… Should I contact package author 😉

    1. Hi Felix, sorry yes I will update the blog. Version 1.1.4 is configured differently now. check the github repo with this blog, it has been updated already.

      1. yes, thank you, I noticed (after I posted). Looking through repo explanation, it goes to a generic Identity Provider (like, I suppose, IdentityServer 4) – not specifically, Google. Nothing wrong – but more complicated for a first attempt 🙂

      2. FWIW, as soon as I inject OidcSecurityService the module code from repo gives me “Unhandled Promise rejection: No provider for AuthConfiguration! ; Zone: ; Task: Promise.then ; Value: Error: No provider for AuthConfiguration!”, although there are no references in AuthConfiguration in *my*code…

  4. Hi Felix

    I checked the example and it works. Have you configured the app.module correctly:

    https://github.com/damienbod/angular-auth-oidc-sample-google-openid/blob/master/src/AngularClient/angularApp/app/app.module.ts

    Greetings Damien

    1. Looks like the problem was that I imported AuthModule, rather than AuthModule.forRoot(). And I see that you updated the post 🙂 🙂
      Thank you!

      1. cool, great that it’s working for you 🙂

  5. […] Angular OIDC OAuth2 client with Google Identity Platform … – This article shows how an Angular client could implement a login for a SPA application using Google Identity Platform OpenID. The Angular application uses the npm … […]

  6. Do you have an example for Azure AD??

      1. Using the example attached, i receive the error:

        AADSTS50001: Resource identifier is not provided.

        I set de Resource property, but it is not added to the request url, when I add manually”&resource=myresource” to the url request, it works perfectly. But it seems that is not added by the library.

  7. Enrique Ramirez · · Reply

    Hi Damien, great library and great sample code.

    I’ve got the sample code working (using 3.0.3 version) but after authenticating the user the access token and id token are still present in the url (i.e. anyone can copy and paste that url and get in). Is that supposed to be the normal behavior? Am I doing something wrong?

    Thanks

  8. from 7/11/2017 stopped working example. Began to issue error 400. “That’s an error. Error: redirect_uri_mismatch”.
    It’s a pity, but I do not have enough skill to understand why. Tried to substitute their registration data, still an error.

  9. I’m sorry, it’s already working. Half a day did not work. Today launched with its registration data, and everything turned out.

    1. Wendy · · Reply

      I only modified with client_id which I obtained on google developers page. But I always meet the error 400. “That’s an error. Error: redirect_uri_mismatch”, and still unsolved. Need some help.

      1. Hi Wendy

        You have to configure you google application to match the configuration sent in the Angular app. The redirect uri from the Angular app does not match what is configured in the google Application configuration. Update the google account application to accpet the redirect and it will work.

        Greetings Damien

  10. BiLKiNiS · · Reply

    Hi Damien,
    I have a problem with all your angular project example, when loading the project in visual studio 2017 and launching the debug, I always stumble on a blank page with “This is server routing, not angular2 routing”
    I think something somewhere is missing but i don’t know what
    I have an angular application that is using the template available with the .net core 2.0 and it’s working fine

  11. Wendy · · Reply

    Hi, Damien
    I have update the google account application and still have the 400 error. So I have some question about redirect url. Can redirect uri be direct visited locally?Does the configuration of uri have special requirements? The redirect uri of my google application is same with you when I configure.

    1. cool thanks

  12. Wendy · · Reply

    Hi, Damien
    I have solved the bug. Thank you for job and share!

Leave a comment

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