Angular OpenID Connect Implicit Flow with IdentityServer4

This article shows how to implement an OpenID Connect Implicit Flow client in Angular. The Angular client is implemented in Typescript and uses IdentityServer4 and an ASP.NET core 2.0 resource server. The OpenID Connect specification for Implicit Flow can be found here.

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

History:

2019-09-20: Updated ASP.NET Core 3.0, Angular 8.2.6
2018-12-05: Updated to ASP.NET Core 2.2 Angular 7.1.1
2018-06-22: Updated ASP.NET Core 2.1, Angular 6.0.6, ASP.NET Core Identity 2.1

Full history:
https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow#history

Other posts in this series:

IdentityServer4 Configuration

The client configuration in IdentityServer4 is set up to use the enum Flow.Implicit and the required Angular client URLs. The RedirectUris must match the redirect_uri URL used for the client authorization request.

new Client
{
	ClientName = "angularclient",
	ClientId = "angularclient",
	AccessTokenType = AccessTokenType.Reference,
	//AccessTokenLifetime = 600, // 10 minutes, default 60 minutes
	AllowedGrantTypes = GrantTypes.Implicit,
	AllowAccessTokensViaBrowser = true,
	RedirectUris = new List<string>
	{
		"https://localhost:44311"

	},
	PostLogoutRedirectUris = new List<string>
	{
		"https://localhost:44311/Unauthorized"
	},
	AllowedCorsOrigins = new List<string>
	{
		"https://localhost:44311",
		"http://localhost:44311"
	},
	AllowedScopes = new List<string>
	{
		"openid",
		"dataEventRecords",
		"dataeventrecordsscope",
		"securedFiles",
		"securedfilesscope",
		"role"
	}
}

Angular client using ASP.NET Core

Angular is downloaded from npm. The npm dependencies are defined in the package.json file. This file is hidden per default in Visual Studio. This can be made visible by adding DnxInvisibleContent Include=”package.json” to the project file. The required angular2 dependencies can be copied from the quickstart Angular guide on the Angular 2 web page. This changes regularly.

{
    "name": "angular-webpack-visualstudio",
    "version": "5.0.22",
    "description": "An Angular VS template",
    "main": "wwwroot/index.html",
    "author": "",
    "license": "ISC",
    "repository": {
        "type": "git",
        "url": "https://github.com/damienbod/Angular2WebpackVisualStudio.git"
    },
    "scripts": {
        "start": "concurrently \"webpack-dev-server --env=dev --open --hot --inline --port 8080\" \"dotnet run\" ",
        "webpack-dev": "webpack --env=dev",
        "webpack-production": "webpack --env=prod",
        "build": "npm run webpack-dev",
        "build-dev": "npm run webpack-dev",
        "build-production": "npm run webpack-production",
        "watch-webpack-dev": "webpack --env=dev --watch --color",
        "watch-webpack-production": "npm run build-production --watch --color",
        "publish-for-iis": "npm run build-production && dotnet publish -c Release",
        "test": "karma start",
        "test-ci": "karma start --single-run --browsers ChromeHeadless",
        "lint": "tslint -p tsconfig.json angularApp/**/*.ts"
    },
    "dependencies": {
        "@angular/animations": "~8.2.6",
        "@angular/common": "~8.2.6",
        "@angular/compiler": "~8.2.6",
        "@angular/core": "~8.2.6",
        "@angular/forms": "~8.2.6",
        "@angular/http": "~7.2.15",
        "angular-l10n": "^8.1.2",
        "@angular/platform-browser": "~8.2.6",
        "@angular/platform-browser-dynamic": "~8.2.6",
        "@angular/router": "~8.2.6",
        "bootstrap": "4.3.1",
        "core-js": "^2.6.5",
        "ie-shim": "0.1.0",
        "jsrsasign": "8.0.12",
        "popper.js": "^1.15.0",
        "rxjs": "~6.5.3",
        "rxjs-compat": "^6.5.3",
        "zone.js": "~0.10.2"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "~0.803.4",
        "@angular/cli": "~8.3.4",
        "@angular/compiler-cli": "~8.2.6",
        "@angular/language-service": "~8.2.6",
        "@types/node": "~12.7.5",
        "@types/jasmine": "~3.4.0",
        "@types/jasminewd2": "~2.0.6",
        "codelyzer": "~5.1.0",
        "jasmine-core": "~3.4.0",
        "jasmine-spec-reporter": "~4.2.1",
        "karma": "~4.3.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage-istanbul-reporter": "~2.1.0",
        "karma-jasmine": "~2.0.1",
        "@ngtools/webpack": "^6.2.5",
        "typescript": "~3.4.5",
        "karma-jasmine-html-reporter": "^1.4.2",
        "protractor": "~6.0.0",
        "ts-node": "~8.3.0",
        "tslint": "~5.20.0",
        "angular-router-loader": "0.8.5",
        "angular2-template-loader": "^0.6.2",
        "awesome-typescript-loader": "^5.2.1",
        "clean-webpack-plugin": "^2.0.2",
        "concurrently": "^4.1.0",
        "copy-webpack-plugin": "^5.0.3",
        "css-loader": "^2.1.1",
        "file-loader": "^3.0.1",
        "html-webpack-plugin": "^3.2.0",
        "jquery": "^3.4.1",
        "json-loader": "^0.5.7",
        "karma-sourcemap-loader": "^0.3.7",
        "karma-spec-reporter": "^0.0.32",
        "karma-webpack": "3.0.5",
        "raw-loader": "^1.0.0",
        "node-sass": "^4.12.0",
        "rimraf": "^2.6.3",
        "sass-loader": "^7.1.0",
        "source-map-loader": "^0.2.4",
        "style-loader": "^0.23.1",
        "tslint-loader": "^3.5.4",
        "toposort": "2.0.2",
        "uglifyjs-webpack-plugin": "^2.1.3",
        "url-loader": "^1.1.2",
        "webpack": "^4.40.2",
        "webpack-bundle-analyzer": "^3.5.0",
        "webpack-cli": "3.3.8",
        "webpack-dev-server": "^3.8.0",
        "webpack-filter-warnings-plugin": "^1.2.1"
    },
    "-vs-binding": {
        "ProjectOpened": [
            "watch-webpack-dev"
        ]
    }
}

The Typescript configuration for the project is defined in the tsconfig.json file in the root of the project. This is required to produce the js files.The tsconfig.json is configuration as follows for the development build.

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": true,
    "noImplicitAny": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "lib": [
      "es2015",
      "dom"
    ],
    "typeRoots": [
      "./node_modules/@types/"
    ]
  },
  "exclude": [
    "node_modules",
    "./angularApp/main-aot.ts"
  ],
  "awesomeTypescriptLoaderOptions": {
    "useWebpackText": true
  },
  "compileOnSave": false,
  "buildOnSave": false
}

webpack is used to add the dependencies to the Angular2 app.

/// <binding ProjectOpened='Run - Development' />

var environment = (process.env.NODE_ENV || "development").trim();

if (environment === "development") {
    module.exports = require('./webpack.dev.js');
} else {
    module.exports = require('./webpack.prod.js');
}

The Angular application is then initialized in the index.html file in the angularApp folder. The required dependencies for Angular are added using Webpack dist bundles.

<!doctype html>
<html>
<head>
    <base href="./">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ASP.NET Core 3.0 Angular Implicit Flow IdentityServer4 Client</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
    <app-component>Loading...</app-component>
</body>
</html>

The vendor.ts in the app folder file is used to add the vendor npm packages to the application. The angular packages are not required here.

import 'jquery/src/jquery';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';

// Other vendors for example jQuery, Lodash or Bootstrap
// You can import js, ts, css, sass, ...

To make the angular application work with a F5 refresh in the browser, middleware needs to be added to the Startup class.

public void Configure(IApplicationBuilder app)
{
	app.UseCors("AllowAllOrigins");

	var angularRoutes = new[] {
		"/home",
		"/forbidden",
		"/authorized",
		"/authorize",
		"/unauthorized",
		"/dataeventrecords",
		"/dataeventrecords/list",
		"/dataeventrecords/create",
		"/dataeventrecords/edit",
		"/logoff",
		"/securefiles",
	};

	app.Use(async (context, next) =>
	{
		if (context.Request.Path.HasValue && null != angularRoutes.FirstOrDefault(
			(ar) => context.Request.Path.Value.StartsWith(ar, StringComparison.OrdinalIgnoreCase)))
		{
			context.Request.Path = new PathString("/");
		}

		await next();
	});

	app.UseDefaultFiles();
	app.UseStaticFiles();

	app.UseRouting();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapControllerRoute(
			name: "default",
			pattern: "{controller=Home}/{action=Index}/{id?}");
	});
}

Angular Authorize

The Angular application is initialized in the main.ts file. This starts the AppModule defined in the app.module.ts file. The module bootstraps the AppComponent.

import './styles.scss';

import 'zone.js/dist/zone';

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

import { AppModule } from './app/app.module';

// Styles.
// Enables Hot Module Replacement.
declare var module: any;

if (module.hot) {
    module.hot.accept();
}

platformBrowserDynamic().bootstrapModule(AppModule);

The AppModule is the starting point for the application and loads all the required, child modules, components and services.

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { Configuration } from './app.constants';
import { routing } from './app.routes';

import { HttpClientModule } from '@angular/common/http';
import { SecureFileService } from './securefile/SecureFileService';

import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { SecureFilesComponent } from './securefile/securefiles.component';
import { DataEventRecordsModule } from './dataeventrecords/dataeventrecords.module';

import { AuthModule } from './auth/modules/auth.module';
import { OidcSecurityService } from './auth/services/oidc.security.service';
import { OidcConfigService, ConfigResult } from './auth/services/oidc.security.config.service';
import { OpenIdConfiguration } from './auth/models/auth.configuration';

import { L10nConfig, L10nLoader, TranslationModule, StorageStrategy, ProviderType } from 'angular-l10n';
import { AuthorizationGuard } from './authorization.guard';
import { AuthorizationCanGuard } from './authorization.can.guard';

const l10nConfig: L10nConfig = {
    locale: {
        languages: [
            { code: 'en', dir: 'ltr' },
            { code: 'it', dir: 'ltr' },
            { code: 'fr', dir: 'ltr' },
            { code: 'de', dir: 'ltr' }
        ],
        language: 'en',
        storage: StorageStrategy.Cookie
    },
    translation: {
        providers: [
            { type: ProviderType.Static, prefix: './i18n/locale-' }
        ],
        caching: true,
        missingValue: 'No key'
    }
};

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

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        routing,
        HttpClientModule,
        TranslationModule.forRoot(l10nConfig),
        DataEventRecordsModule,
        AuthModule.forRoot(),
    ],
    declarations: [
        AppComponent,
        ForbiddenComponent,
        HomeComponent,
        UnauthorizedComponent,
        SecureFilesComponent
    ],
    providers: [
        OidcConfigService,
        OidcSecurityService,
        {
            provide: APP_INITIALIZER,
            useFactory: loadConfig,
            deps: [OidcConfigService],
            multi: true
        },
        AuthorizationGuard,
        AuthorizationCanGuard,
        SecureFileService,
        Configuration
    ],
    bootstrap: [AppComponent],
})

export class AppModule {

    constructor(
        private oidcSecurityService: OidcSecurityService,
        private oidcConfigService: OidcConfigService,
        configuration: Configuration,
        public l10nLoader: L10nLoader
    ) {
        this.l10nLoader.load();

        this.oidcConfigService.onConfigurationLoaded.subscribe((configResult: ConfigResult) => {

            const config: OpenIdConfiguration = {
                stsServer: configResult.customConfig.stsServer,
                redirect_url: configResult.customConfig.redirect_url,
                client_id: configResult.customConfig.client_id,
                response_type: configResult.customConfig.response_type,
                scope: configResult.customConfig.scope,
                post_logout_redirect_uri: configResult.customConfig.post_logout_redirect_uri,
                start_checksession: configResult.customConfig.start_checksession,
                silent_renew: configResult.customConfig.silent_renew,
                silent_renew_url: configResult.customConfig.redirect_url + '/silent-renew.html',
                post_login_route: configResult.customConfig.startup_route,
                forbidden_route: configResult.customConfig.forbidden_route,
                unauthorized_route: configResult.customConfig.unauthorized_route,
                log_console_warning_active: configResult.customConfig.log_console_warning_active,
                log_console_debug_active: configResult.customConfig.log_console_debug_active,
                max_id_token_iat_offset_allowed_in_seconds: configResult.customConfig.max_id_token_iat_offset_allowed_in_seconds,
                history_cleanup_off: true
                // iss_validation_off: false
                // disable_iat_offset_validation: true
            };

            configuration.FileServer = configResult.customConfig.apiFileServer;
            configuration.Server = configResult.customConfig.apiServer;

            this.oidcSecurityService.setupModule(config, configResult.authWellknownEndpoints);
        });

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

The authorization process is initialized in the AppComponent. The html defines the Login and the Logout buttons. The buttons are displayed using the securityService.IsAuthorized() which is set using the OidcSecurityService authorize process. the (click) definition is used to define the click event in Angular.

<div class="container">
    <!-- Static navbar -->
    <nav class="bg-dark mb-4 navbar navbar-dark navbar-expand-md">
        <div class="container-fluid">
            <div class="navbar-header">
                <a [routerLink]="['/dataeventrecords']" class="navbar-brand"><img src="assets/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <div class="navbar-collapse collapse" id="navbar">
                <ul class="mr-auto navbar-nav">
                    <li class="nav-item"><a class="nav-link" *ngIf="isAuthorized" [routerLink]="['/dataeventrecords']">DataEventRecords</a></li>
                    <li class="nav-item"><a class="nav-link" *ngIf="isAuthorized" [routerLink]="['/dataeventrecords/create']">Create DataEventRecord</a></li>
                    <li class="nav-item"><a class="nav-link" *ngIf="isAuthorized" [routerLink]="['/securefiles']">Secured Files Download</a></li>

                    <li class="nav-item"><a class="nav-link" *ngIf="!isAuthorized" (click)="login()">{{ 'LOGIN' | translate:lang }}</a></li>
                    <li class="nav-item"><a class="nav-link" *ngIf="isAuthorized" (click)="logout()">{{ 'LOGOUT' | translate:lang }}</a></li>

                    <li class="nav-item"><a class="nav-link" (click)="changeCulture('de','CH')">DE</a></li>
                    <li class="nav-item"><a class="nav-link" (click)="changeCulture('fr','CH')">FR</a></li>
                    <li class="nav-item"><a class="nav-link" (click)="changeCulture('it','CH')">IT</a></li>
                    <li class="nav-item"><a class="nav-link" (click)="changeCulture('en','US')">EN</a></li>

                    <li class="nav-item"><a class="nav-link" *ngIf="checksession" (click)="refreshSession()">Refresh Session</a></li>

                </ul>
            </div><!--/.nav-collapse -->
        </div><!--/.container-fluid -->
    </nav>

    <router-outlet></router-outlet>

</div>

The app.component.ts defines the routes and the Login, Logout click events. The component uses the @Injectable() SecurityService where the authorization is implemented. It is important that the authorization callback is used outside of the angular routing as this removes the hash which is used to return the id_token. The ngOnInit method just checks if a hash exists, and if it does, executes the AuthorizedCallback method in the SecurityService.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Router } from '@angular/router';
import { OidcSecurityService } from './auth/services/oidc.security.service';
import { LocaleService, TranslationService, Language } from 'angular-l10n';
import './app.component.css';
import { AuthorizationResult } from './auth/models/authorization-result';
import { AuthorizationState } from './auth/models/authorization-state.enum';
// import { ValidationResult } from './auth/models/validation-result.enum';

@Component({
    selector: 'app-component',
    templateUrl: 'app.component.html',
})

export class AppComponent implements OnInit, OnDestroy {

    @Language() lang = '';

    title = '';

    isAuthorizedSubscription: Subscription | undefined;
    isAuthorized = false;

    onChecksessionChanged: Subscription | undefined;
    checksession = false;

    constructor(
        public oidcSecurityService: OidcSecurityService,
        public locale: LocaleService,
        private router: Router,
        public translation: TranslationService
    ) {
        console.log('AppComponent STARTING');

        if (this.oidcSecurityService.moduleSetup) {
            this.doCallbackLogicIfRequired();
        } else {
            this.oidcSecurityService.onModuleSetup.subscribe(() => {
                this.doCallbackLogicIfRequired();
            });
        }

        this.oidcSecurityService.onCheckSessionChanged.subscribe(
            (checksession: boolean) => {
                console.log('...recieved a check session event');
                this.checksession = checksession;
            });

        this.oidcSecurityService.onAuthorizationResult.subscribe(
            (authorizationResult: AuthorizationResult) => {
                this.onAuthorizationResultComplete(authorizationResult);
            });
    }

    ngOnInit() {
        this.isAuthorizedSubscription = this.oidcSecurityService.getIsAuthorized().subscribe(
            (isAuthorized: boolean) => {
                this.isAuthorized = isAuthorized;
            });
    }

    changeCulture(language: string, country: string) {
        this.locale.setDefaultLocale(language, country);
        console.log('set language: ' + language);
    }

    ngOnDestroy(): void {
        if (this.isAuthorizedSubscription) {
            this.isAuthorizedSubscription.unsubscribe();
        }
    }

    login() {
        console.log('start login');

        let culture = 'de-CH';
        if (this.locale.getCurrentCountry()) {
            culture = this.locale.getCurrentLanguage() + '-' + this.locale.getCurrentCountry();
        }

        this.oidcSecurityService.setCustomRequestParameters({ 'ui_locales': culture});

        this.oidcSecurityService.authorize();
    }

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

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

    private doCallbackLogicIfRequired() {
        if (window.location.hash) {
            this.oidcSecurityService.authorizedImplicitFlowCallback();
        }
    }

    private onAuthorizationResultComplete(authorizationResult: AuthorizationResult) {

        console.log('Auth result received AuthorizationState:'
            + authorizationResult.authorizationState
            + ' validationResult:' + authorizationResult.validationResult);

        this.oidcSecurityService.getUserData().subscribe(
            (data: any) => {
                console.log(data);
            });

        if (authorizationResult.authorizationState === AuthorizationState.unauthorized) {
            if (window.parent) {
                // sent from the child iframe, for example the silent renew
                this.router.navigate(['/unauthorized']);
            } else {
                window.location.href = '/unauthorized';
            }
        }
    }
}

The new Angular routing is defined in the app.routes.ts file. The routing const is used in the AppModule imports. .

import { Routes, RouterModule } from '@angular/router';

import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { SecureFilesComponent } from './securefile/securefiles.component';
import { AuthorizationGuard } from './authorization.guard';
import { AuthorizationCanGuard } from './authorization.can.guard';

const appRoutes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'home', component: HomeComponent },
    { path: 'forbidden', component: ForbiddenComponent },
    { path: 'unauthorized', component: UnauthorizedComponent },
    {
        path: 'securefiles',
        component: SecureFilesComponent,
        canActivate: [AuthorizationGuard],
        canLoad: [AuthorizationCanGuard]
    }
];

export const routing = RouterModule.forRoot(appRoutes);

The The Authorization module in the angular app is configured in the auth.configuration.ts file using the AuthConfiguration class. The Oidc Implicit flow client is configured here, and must match the server configuration.

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { OpenIdConfiguration, OpenIdInternalConfiguration } from '../models/auth.configuration';
import { AuthWellKnownEndpoints } from '../models/auth.well-known-endpoints';
import { PlatformProvider } from './platform.provider';

@Injectable({ providedIn: 'root' })
export class ConfigurationProvider {
    private DEFAULT_CONFIG: OpenIdInternalConfiguration = {
        stsServer: 'https://please_set',
        redirect_url: 'https://please_set',
        client_id: 'please_set',
        response_type: 'code',
        scope: 'openid email profile',
        hd_param: '',
        post_logout_redirect_uri: 'https://please_set',
        start_checksession: false,
        silent_renew: false,
        silent_renew_url: 'https://please_set',
        silent_renew_offset_in_seconds: 0,
        use_refresh_token: false,
        post_login_route: '/',
        forbidden_route: '/forbidden',
        unauthorized_route: '/unauthorized',
        auto_userinfo: true,
        auto_clean_state_after_authentication: true,
        trigger_authorization_result_event: false,
        log_console_warning_active: true,
        log_console_debug_active: false,
        iss_validation_off: false,
        history_cleanup_off: false,
        max_id_token_iat_offset_allowed_in_seconds: 3,
        isauthorizedrace_timeout_in_seconds: 5,
        disable_iat_offset_validation: false,
        storage: typeof Storage !== 'undefined' ? sessionStorage : null,
    };

    private INITIAL_AUTHWELLKNOWN: AuthWellKnownEndpoints = {
        issuer: '',
        jwks_uri: '',
        authorization_endpoint: '',
        token_endpoint: '',
        userinfo_endpoint: '',
        end_session_endpoint: '',
        check_session_iframe: '',
        revocation_endpoint: '',
        introspection_endpoint: '',
    };

    private mergedOpenIdConfiguration: OpenIdInternalConfiguration = this.DEFAULT_CONFIG;
    private authWellKnownEndpoints: AuthWellKnownEndpoints = this.INITIAL_AUTHWELLKNOWN;

    private onConfigurationChangeInternal = new Subject<OpenIdConfiguration>();

    get openIDConfiguration(): OpenIdInternalConfiguration {
        return this.mergedOpenIdConfiguration;
    }

    get wellKnownEndpoints(): AuthWellKnownEndpoints {
        return this.authWellKnownEndpoints;
    }

    get onConfigurationChange() {
        return this.onConfigurationChangeInternal.asObservable();
    }

    constructor(private platformProvider: PlatformProvider) {}

    setup(passedOpenIfConfiguration: OpenIdConfiguration, passedAuthWellKnownEndpoints: AuthWellKnownEndpoints) {
        this.mergedOpenIdConfiguration = { ...this.mergedOpenIdConfiguration, ...passedOpenIfConfiguration };
        this.setSpecialCases(this.mergedOpenIdConfiguration);
        this.authWellKnownEndpoints = { ...passedAuthWellKnownEndpoints };
        this.onConfigurationChangeInternal.next({ ...this.mergedOpenIdConfiguration });
    }

    private setSpecialCases(currentConfig: OpenIdConfiguration) {
        if (!this.platformProvider.isBrowser) {
            currentConfig.start_checksession = false;
            currentConfig.silent_renew = false;
            currentConfig.use_refresh_token = false;
        }
    }
}

The Authorize method calls the IdentityServer4 connect/authorize using a response type “id_token token”. This is one of the OpenID Connect Implicit flow which is described in the OpenID specification. The required parameters are also defined in this specification. Important is that the used parameters match the IdentityServer4 client definition.

// Code Flow with PCKE or Implicit Flow
authorize(urlHandler?: (url: string) => any) {
	if (this.configurationProvider.wellKnownEndpoints) {
		this.authWellKnownEndpointsLoaded = true;
	}

	if (!this.authWellKnownEndpointsLoaded) {
		this.loggerService.logError('Well known endpoints must be loaded before user can login!');
		return;
	}

	if (!this.oidcSecurityValidation.config_validate_response_type(this.configurationProvider.openIDConfiguration.response_type)) {
		// invalid response_type
		return;
	}

	this.resetAuthorizationData(false);

	this.loggerService.logDebug('BEGIN Authorize Code Flow, no auth data');

	let state = this.oidcSecurityCommon.authStateControl;
	if (!state) {
		state = Date.now() + '' + Math.random() + Math.random();
		this.oidcSecurityCommon.authStateControl = state;
	}

	const nonce = 'N' + Math.random() + '' + Date.now();
	this.oidcSecurityCommon.authNonce = nonce;
	this.loggerService.logDebug('AuthorizedController created. local state: ' + this.oidcSecurityCommon.authStateControl);

	let url = '';
	// Code Flow
	if (this.configurationProvider.openIDConfiguration.response_type === 'code') {
		// code_challenge with "S256"
		const code_verifier = 'C' + Math.random() + '' + Date.now() + '' + Date.now() + Math.random();
		const code_challenge = this.oidcSecurityValidation.generate_code_verifier(code_verifier);

		this.oidcSecurityCommon.code_verifier = code_verifier;

		if (this.configurationProvider.wellKnownEndpoints) {
			url = this.createAuthorizeUrl(
				true,
				code_challenge,
				this.configurationProvider.openIDConfiguration.redirect_url,
				nonce,
				state,
				this.configurationProvider.wellKnownEndpoints.authorization_endpoint || ''
			);
		} else {
			this.loggerService.logError('authWellKnownEndpoints is undefined');
		}
	} else {
		// Implicit Flow

		if (this.configurationProvider.wellKnownEndpoints) {
			url = this.createAuthorizeUrl(
				false,
				'',
				this.configurationProvider.openIDConfiguration.redirect_url,
				nonce,
				state,
				this.configurationProvider.wellKnownEndpoints.authorization_endpoint || ''
			);
		} else {
			this.loggerService.logError('authWellKnownEndpoints is undefined');
		}
	}

	if (urlHandler) {
		urlHandler(url);
	} else {
		this.redirectTo(url);
	}
}

The user is redirected to the default IdentityServer4 login html view:

loginidentityserverwithaspnetidentity_01

And then to the permissions page, which shows what the client is requesting.

loginidentityserverwithaspnetidentity_02

Angular2 Authorize Callback

The AuthorizedCallback uses the returned hash to extract the token and the id_token and save these to the local storage of the browser. The method also checks the state and the nonce to prevent cross-site request forgery attacks.

// Implicit Flow
authorizedImplicitFlowCallback(hash?: string) {
	this._isModuleSetup
		.pipe(
			filter((isModuleSetup: boolean) => isModuleSetup),
			take(1)
		)
		.subscribe(() => {
			this.authorizedImplicitFlowCallbackProcedure(hash);
		});
}

// Implicit Flow
private authorizedCallbackProcedure(result: any, isRenewProcess: boolean) {
	this.oidcSecurityCommon.authResult = result;

	if (!this.configurationProvider.openIDConfiguration.history_cleanup_off && !isRenewProcess) {
		// reset the history to remove the tokens
		window.history.replaceState({}, window.document.title, window.location.origin + window.location.pathname);
	} else {
		this.loggerService.logDebug('history clean up inactive');
	}

	if (result.error) {
		if (isRenewProcess) {
			this.loggerService.logDebug(result);
		} else {
			this.loggerService.logWarning(result);
		}

		if ((result.error as string) === 'login_required') {
			this._onAuthorizationResult.next(new AuthorizationResult(AuthorizationState.unauthorized, ValidationResult.LoginRequired));
		} else {
			this._onAuthorizationResult.next(new AuthorizationResult(AuthorizationState.unauthorized, ValidationResult.SecureTokenServerError));
		}

		this.resetAuthorizationData(false);
		this.oidcSecurityCommon.authNonce = '';

		if (!this.configurationProvider.openIDConfiguration.trigger_authorization_result_event && !isRenewProcess) {
			this.router.navigate([this.configurationProvider.openIDConfiguration.unauthorized_route]);
		}
	} else {
		this.loggerService.logDebug(result);

		this.loggerService.logDebug('authorizedCallback created, begin token validation');

		this.getSigningKeys().subscribe(
			jwtKeys => {
				const validationResult = this.getValidatedStateResult(result, jwtKeys);

				if (validationResult.authResponseIsValid) {
					this.setAuthorizationData(validationResult.access_token, validationResult.id_token);
					this.oidcSecurityCommon.silentRenewRunning = '';

					if (this.configurationProvider.openIDConfiguration.auto_userinfo) {
						this.getUserinfo(isRenewProcess, result, validationResult.id_token, validationResult.decoded_id_token).subscribe(
							response => {
								if (response) {
									this._onAuthorizationResult.next(
										new AuthorizationResult(AuthorizationState.authorized, validationResult.state)
									);
									if (!this.configurationProvider.openIDConfiguration.trigger_authorization_result_event && !isRenewProcess) {
										this.router.navigate([this.configurationProvider.openIDConfiguration.post_login_route]);
									}
								} else {
									this._onAuthorizationResult.next(
										new AuthorizationResult(AuthorizationState.unauthorized, validationResult.state)
									);
									if (!this.configurationProvider.openIDConfiguration.trigger_authorization_result_event && !isRenewProcess) {
										this.router.navigate([this.configurationProvider.openIDConfiguration.unauthorized_route]);
									}
								}
							},
							err => {
								/* Something went wrong while getting signing key */
								this.loggerService.logWarning('Failed to retreive user info with error: ' + JSON.stringify(err));
							}
						);
					} else {
						if (!isRenewProcess) {
							// userData is set to the id_token decoded, auto get user data set to false
							this.oidcSecurityUserService.setUserData(validationResult.decoded_id_token);
							this.setUserData(this.oidcSecurityUserService.getUserData());
						}

						this.runTokenValidation();

						this._onAuthorizationResult.next(new AuthorizationResult(AuthorizationState.authorized, validationResult.state));
						if (!this.configurationProvider.openIDConfiguration.trigger_authorization_result_event && !isRenewProcess) {
							this.router.navigate([this.configurationProvider.openIDConfiguration.post_login_route]);
						}
					}
				} else {
					// something went wrong
					this.loggerService.logWarning('authorizedCallback, token(s) validation failed, resetting');
					this.loggerService.logWarning(window.location.hash);
					this.resetAuthorizationData(false);
					this.oidcSecurityCommon.silentRenewRunning = '';

					this._onAuthorizationResult.next(new AuthorizationResult(AuthorizationState.unauthorized, validationResult.state));
					if (!this.configurationProvider.openIDConfiguration.trigger_authorization_result_event && !isRenewProcess) {
						this.router.navigate([this.configurationProvider.openIDConfiguration.unauthorized_route]);
					}
				}
			},
			err => {
				/* Something went wrong while getting signing key */
				this.loggerService.logWarning('Failed to retreive siging key with error: ' + JSON.stringify(err));
				this.oidcSecurityCommon.silentRenewRunning = '';
			}
		);
	}
}

The oidc.security.validation.ts is implemented as per spec from OpenID using the jsrsasign library.

import { Injectable } from '@angular/core';
import { OidcSecurityCommon } from './oidc.security.common';

// from jsrasiign
declare var KJUR: any;
declare var KEYUTIL: any;
declare var hextob64u: any;

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

// id_token
//// id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.
//// id_token C2: 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.
// id_token C3: If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
// id_token C4: If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
//// id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the alg Header Parameter of the JOSE Header. The Client MUST use the keys provided by the Issuer.
//// id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the OpenID Connect Core 1.0 [OpenID.Core] specification.
//// id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account for clock skew).
// 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.
//// id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one that was sent in the Authentication Request.The Client SHOULD check the nonce value for replay attacks.The precise method for detecting replay attacks is Client specific.
// id_token C10: If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.The meaning and processing of acr Claim Values is out of scope for this document.
// id_token C11: When a max_age request is made, the Client SHOULD check the auth_time Claim value and request re- authentication if it determines too much time has elapsed since the last End- User authentication.

//// Access Token Validation
//// access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA] for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.
//// access_token C2: Take the left- most half of the hash and base64url- encode it.
//// access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash is present in the ID Token.

@Injectable()
export class OidcSecurityValidation {

    constructor(private oidcSecurityCommon: OidcSecurityCommon) {
    }

    // id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account for clock skew).
    isTokenExpired(token: string, offsetSeconds?: number): boolean {

        let decoded: any;
        decoded = this.getPayloadFromToken(token, false);

        let tokenExpirationDate = this.getTokenExpirationDate(decoded);
        offsetSeconds = offsetSeconds || 0;

        if (tokenExpirationDate == null) {
            return false;
        }

        // Token expired?
        return !(tokenExpirationDate.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000)));
    }

    // id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one that was sent in the Authentication Request.The Client SHOULD check the nonce value for replay attacks.The precise method for detecting replay attacks is Client specific.
    validate_id_token_nonce(dataIdToken: any, local_nonce: any): boolean {
        if (dataIdToken.nonce !== local_nonce) {
            this.oidcSecurityCommon.logDebug('Validate_id_token_nonce failed, dataIdToken.nonce: ' + dataIdToken.nonce + ' local_nonce:' + local_nonce);
            return false;
        }

        return true;
    }

    // id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.
    validate_id_token_iss(dataIdToken: any, client_id: any): boolean {
        if (dataIdToken.iss !== client_id) {
            this.oidcSecurityCommon.logDebug('Validate_id_token_iss failed, dataIdToken.iss: ' + dataIdToken.iss + ' client_id:' + client_id);
            return false;
        }

        return true;
    }

    // id_token C2: 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.
    validate_id_token_aud(dataIdToken: any, aud: any): boolean {
        if (dataIdToken.aud !== aud) {
            this.oidcSecurityCommon.logDebug('Validate_id_token_aud failed, dataIdToken.aud: ' + dataIdToken.aud + ' client_id:' + aud);
            return false;
        }

        return true;
    }

    validateStateFromHashCallback(state: any, local_state: any): boolean {
        if (state !== local_state) {
            this.oidcSecurityCommon.logDebug('ValidateStateFromHashCallback failed, state: ' + state + ' local_state:' + local_state);
            return false;
        }

        return true;
    }

    getPayloadFromToken(token: any, encode: boolean) {
        let data = {};
        if (typeof token !== 'undefined') {
            let encoded = token.split('.')[1];
            if (encode) {
                return encoded;
            }
            data = JSON.parse(this.urlBase64Decode(encoded));
        }

        return data;
    }

    getHeaderFromToken(token: any, encode: boolean) {
        let data = {};
        if (typeof token !== 'undefined') {
            let encoded = token.split('.')[0];
            if (encode) {
                return encoded;
            }
            data = JSON.parse(this.urlBase64Decode(encoded));
        }

        return data;
    }

    getSignatureFromToken(token: any, encode: boolean) {
        let data = {};
        if (typeof token !== 'undefined') {
            let encoded = token.split('.')[2];
            if (encode) {
                return encoded;
            }
            data = JSON.parse(this.urlBase64Decode(encoded));
        }

        return data;
    }

    // id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the alg Header Parameter of the JOSE Header. The Client MUST use the keys provided by the Issuer.
    // id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the OpenID Connect Core 1.0 [OpenID.Core] specification.
    validate_signature_id_token(id_token: any, jwtkeys: any): boolean {

        if (!jwtkeys || !jwtkeys.keys) {
            return false;
        }

        let header_data = this.getHeaderFromToken(id_token, false);
        let kid = header_data.kid;
        let alg = header_data.alg;

        if ('RS256' != alg) {
            this.oidcSecurityCommon.logWarning('Only RS256 supported');
            return false;
        }

        let isValid = false;

        for (let key of jwtkeys.keys) {
            if (key.kid === kid) {
                let publickey = KEYUTIL.getKey(key);
                isValid = KJUR.jws.JWS.verify(id_token, publickey, ['RS256']);
                return isValid;
            }
        }

        return isValid;
    }

    // Access Token Validation
    // access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA] for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.
    // access_token C2: Take the left- most half of the hash and base64url- encode it.
    // access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash is present in the ID Token.
    validate_id_token_at_hash(access_token: any, at_hash: any): boolean {

        let hash = KJUR.crypto.Util.hashString(access_token, 'sha256');
        let first128bits = hash.substr(0, hash.length / 2);
        let testdata = hextob64u(first128bits);

        if (testdata === at_hash) {
            return true; // isValid;
        }

        return false;
    }

    private getTokenExpirationDate(dataIdToken: any): Date {
        if (!dataIdToken.hasOwnProperty('exp')) {
            return null;
        }

        let date = new Date(0); // The 0 here is the key, which sets the date to the epoch
        date.setUTCSeconds(dataIdToken.exp);

        return date;
    }

    private urlBase64Decode(str: string) {
        let output = str.replace('-', '+').replace('_', '/');
        switch (output.length % 4) {
            case 0:
                break;
            case 2:
                output += '==';
                break;
            case 3:
                output += '=';
                break;
            default:
                throw 'Illegal base64url string!';
        }

        return window.atob(output);
    }
}

Setting and reset the Authorization Data in the client

The received tokens and authorization data are saved or removed to the local storage and also the possible roles for the user. The application has only an ‘dataEventRecords.admin’ role or not. This matches the policy defined on the resource server.

private resetAuthorizationData() {
	this.isAuthorized = false;
	this.oidcSecurityCommon.resetStorageData();
	this.checkSessionChanged = false;
}

private setAuthorizationData(access_token: any, id_token: any) {
	if (this.oidcSecurityCommon.retrieve(this.oidcSecurityCommon.storage_access_token) !== '') {
		this.oidcSecurityCommon.store(this.oidcSecurityCommon.storage_access_token, '');
	}

	this.oidcSecurityCommon.logDebug(access_token);
	this.oidcSecurityCommon.logDebug(id_token);
	this.oidcSecurityCommon.logDebug('storing to storage, getting the roles');
	this.oidcSecurityCommon.store(this.oidcSecurityCommon.storage_access_token, access_token);
	this.oidcSecurityCommon.store(this.oidcSecurityCommon.storage_id_token, id_token);
	this.isAuthorized = true;
	this.oidcSecurityCommon.store(this.oidcSecurityCommon.storage_is_authorized, true);
}

private getUserData = (): Observable<string[]> => {
	this.setHeaders();
	return this._http.get('https://localhost:44318/connect/userinfo', {
		headers: this.headers,
		body: ''
	}).map(res => res.json());
}

private setHeaders() {
	this.headers = new Headers();
	this.headers.append('Content-Type', 'application/json');
	this.headers.append('Accept', 'application/json');

	var token = this.getToken();

	if (token !== "") {
		this.headers.append('Authorization', 'Bearer ' + token);
	}
}

The private retrieve and store methods are just used to store the data or get it from the local storage in the browser.

private retrieve(key: string): any {
	var item = this.storage.getItem(key);

	if (item && item !== 'undefined') {
		return JSON.parse(this.storage.getItem(key));
	}

	return;
}

private store(key: string, value: any) {
	this.storage.setItem(key, JSON.stringify(value));
}

Using the token to access the data

Now that the local storage has a token, this can then be used and added to the HTTP request headers. This is implemented in the DataEventRecordsService class. The setHeaders method is used to add the Authorization header with the token. Each request to the resource server uses this then.

import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { OidcSecurityService } from '../auth/services/oidc.security.service';
import { DataEventRecord } from './models/DataEventRecord';

@Injectable()
export class DataEventRecordsService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: OidcSecurityService) {
        this.actionUrl = `${_configuration.Server}api/DataEventRecords/`;   
    }

    private setHeaders() {

        console.log("setHeaders started");

        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');

        var token = this._securityService.GetToken();
        if (token !== "") {
            let tokenValue = 'Bearer ' + token;
            console.log("tokenValue:" + tokenValue);
            this.headers.append('Authorization', tokenValue);
        }
    }

    public GetAll = (): Observable<DataEventRecord[]> => {
        this.setHeaders();
        let options = new RequestOptions({ headers: this.headers, body: '' });

        return this._http.get(this.actionUrl, options).map(res => res.json());
    }

    public GetById = (id: number): Observable<DataEventRecord> => {
        this.setHeaders();
        return this._http.get(this.actionUrl + id, {
            headers: this.headers,
            body: ''
        }).map(res => res.json());
    }

    public Add = (itemToAdd: any): Observable<Response> => {       
        this.setHeaders();
        return this._http.post(this.actionUrl, JSON.stringify(itemToAdd), { headers: this.headers });
    }

    public Update = (id: number, itemToUpdate: any): Observable<Response> => {
        this.setHeaders();
        return this._http
            .put(this.actionUrl + id, JSON.stringify(itemToUpdate), { headers: this.headers });
    }

    public Delete = (id: number): Observable<Response> => {
        this.setHeaders();
        return this._http.delete(this.actionUrl + id, {
            headers: this.headers
        });
    }

}

The token is then used to request the resource data and displays the secured data in the client application.

angular2_IdentityServer4_03

Using the roles, IsAuthorized check

The securityService.HasAdminRole is used to remove links and replace them with texts if the logged-in user has no claims to execute an edit entity. The delete button should also be disabled using this property, but this is left active to display the forbidden redirect with a 403.

<div class="col-md-12" *ngIf="securityService.isAuthorized" >
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">{{message}}</h3>
        </div>
        <div class="panel-body">
            <table class="table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Timestamp</th>
                    </tr>
                </thead>
                <tbody>
                    <tr style="height:20px;" *ngFor="let dataEventRecord of DataEventRecords" >
                        <td>
                            <a *ngIf="hasAdminRole" href="" [routerLink]="['/dataeventrecords/edit/' + dataEventRecord.Id]" >{{dataEventRecord.Name}}</a>
                            <span *ngIf="!hasAdminRole">{{dataEventRecord.Name}}</span>
                        </td>
                        <td>{{dataEventRecord.Timestamp}}</td>
                        <td><button (click)="Delete(dataEventRecord.Id)">Delete</button></td>
                    </tr>
                </tbody>
            </table>

        </div>
    </div>
</div>

The private getData method uses the DataEventRecordsService to get the secured data. The SecurityService service is used to handle the errors from a server HTTP request to the resource server.

import { Component, OnInit } from '@angular/core';
import { OidcSecurityService } from '../auth/services/oidc.security.service';
import { Observable }       from 'rxjs/Observable';
import { Router } from '@angular/router';

import { DataEventRecordsService } from '../dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './models/DataEventRecord';

@Component({
    selector: 'dataeventrecords-list',
    templateUrl: 'dataeventrecords-list.component.html'
})

export class DataEventRecordsListComponent implements OnInit {

    public message: string;
    public DataEventRecords: DataEventRecord[];
   
    constructor(
        private _dataEventRecordsService: DataEventRecordsService,
        public securityService: OidcSecurityService,
        private _router: Router) {
        this.message = "DataEventRecords";
    }

    ngOnInit() {
        this.getData();
    }

    public Delete(id: any) {
        console.log("Try to delete" + id);
        this._dataEventRecordsService.Delete(id)
            .subscribe((() => console.log("subscribed")),
            error => this.securityService.HandleError(error),
            () => this.getData());
    }

    private getData() {
        console.log('DataEventRecordsListComponent:getData starting...');
        this._dataEventRecordsService
            .GetAll()
            .subscribe(data => this.DataEventRecords = data,
            error => this.securityService.HandleError(error),
            () => console.log('Get all completed'));
    }

}

Forbidden, Handle 401, 403 errors

The HandleError method in the SecurityService is used to check for a 401, 403 status in the response. If a 403 is returned, the user is redirected to the forbidden page. If a 401 is returned, the local user is reset and redirected to the Unauthorized route.

handleError(error: any) {
	this.oidcSecurityCommon.logError(error);
	if (error.status == 403) {
		this.router.navigate([this.authConfiguration.forbidden_route]);
	} else if (error.status == 401) {
		this.resetAuthorizationData();
		this.router.navigate([this.authConfiguration.unauthorized_route]);
	}
}

The redirect in the UI.

angular2_IdentityServer4_04

With Angular2 it’s not as simple to implement cross cutting concerns like authorization data for a logged in user. In angular, this was better as you could just use the $rootscope. It is also more difficult to debug, have still to figure out how to debug in visual studio with breakpoints.

Links

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

https://github.com/kjur/jsrsasign

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

http://www.codeproject.com/Articles/1087605/Angular-typescript-configuration-and-debugging-for

Announcing IdentityServer for ASP.NET 5 and .NET Core

https://github.com/IdentityServer/IdentityServer4

https://github.com/IdentityServer/IdentityServer4.Samples

The State of Security in ASP.NET 5 and MVC 6: OAuth 2.0, OpenID Connect and IdentityServer

http://connect2id.com/learn/openid-connect

https://github.com/FabianGosebrink/Angular2-ASPNETCore-SignalR-Demo

Getting Started with ASP NET Core 1 and Angular 2 in Visual Studio 2015

http://benjii.me/2016/01/angular2-routing-with-asp-net-core-1/

http://tattoocoder.azurewebsites.net/angular2-aspnet5-spa-template/

Cross-platform Single Page Applications with ASP.NET Core 1.0, Angular 2 & TypeScript

https://angular.io/docs/ts/latest/guide/router.html

http://victorsavkin.com/post/145672529346/angular-router

141 comments

  1. Manjit singh · · Reply

    And when i run npm run buildProduction in cmd then its giving following log:
    0 info it worked if it ends with ok
    1 verbose cli [ ‘C:\\Program Files\\nodejs\\node.exe’,
    1 verbose cli ‘C:\\Users\\manjit.singh\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js’,
    1 verbose cli ‘run’,
    1 verbose cli ‘ngc’ ]
    2 info using npm@4.5.0
    3 info using node@v7.9.0
    4 verbose run-script [ ‘prengc’, ‘ngc’, ‘postngc’ ]
    5 info lifecycle @1.0.0~prengc: @1.0.0
    6 silly lifecycle @1.0.0~prengc: no script for prengc, continuing
    7 info lifecycle @1.0.0~ngc: @1.0.0
    8 verbose lifecycle @1.0.0~ngc: unsafe-perm in lifecycle true
    9 verbose lifecycle @1.0.0~ngc: PATH: C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\bin\node-gyp-bin;D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\node_modules\.bin;C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\bin\node-gyp-bin;D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\node_modules\.bin;C:\Program Files\Common Files\Microsoft Shared\Microsoft Online Services;C:\Program Files (x86)\Common Files\Microsoft Shared\Microsoft Online Services;C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\ProgramData\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\ManagementStudio\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\dotnet\;C:\Program Files (x86)\Skype\Phone\;C:\Program Files\nodejs\;C:\Program Files (x86)\Microsoft VS Code\bin;C:\Users\manjit.singh\AppData\Roaming\npm
    10 verbose lifecycle @1.0.0~ngc: CWD: D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client
    11 silly lifecycle @1.0.0~ngc: Args: [ ‘/d /s /c’, ‘ngc -p ./tsconfig-aot.json’ ]
    12 silly lifecycle @1.0.0~ngc: Returned: code: 1 signal: null
    13 info lifecycle @1.0.0~ngc: Failed to exec ngc script
    14 verbose stack Error: @1.0.0 ngc: `ngc -p ./tsconfig-aot.json`
    14 verbose stack Exit status 1
    14 verbose stack at EventEmitter. (C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\lib\utils\lifecycle.js:279:16)
    14 verbose stack at emitTwo (events.js:106:13)
    14 verbose stack at EventEmitter.emit (events.js:194:7)
    14 verbose stack at ChildProcess. (C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\lib\utils\spawn.js:40:14)
    14 verbose stack at emitTwo (events.js:106:13)
    14 verbose stack at ChildProcess.emit (events.js:194:7)
    14 verbose stack at maybeClose (internal/child_process.js:899:16)
    14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:226:5)
    15 verbose pkgid @1.0.0
    16 verbose cwd D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client
    17 verbose Windows_NT 6.3.9600
    18 verbose argv “C:\\Program Files\\nodejs\\node.exe” “C:\\Users\\manjit.singh\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js” “run” “ngc”
    19 verbose node v7.9.0
    20 verbose npm v4.5.0
    21 error code ELIFECYCLE
    22 error errno 1
    23 error @1.0.0 ngc: `ngc -p ./tsconfig-aot.json`
    23 error Exit status 1
    24 error Failed at the @1.0.0 ngc script ‘ngc -p ./tsconfig-aot.json’.
    24 error Make sure you have the latest version of node.js and npm installed.
    24 error If you do, this is most likely a problem with the package,
    24 error not with npm itself.
    24 error Tell the author that this fails on your system:
    24 error ngc -p ./tsconfig-aot.json
    24 error You can get information on how to open an issue for this project with:
    24 error npm bugs
    24 error Or if that isn’t available, you can get their info via:
    24 error npm owner ls
    24 error There is likely additional logging output above.
    25 verbose exit [ 1, true ]

    1. Manjit singh · · Reply

      And i have the latest version of node.js installed on my machine

  2. error Failed at the @1.0.0 ngc script ‘ngc -p ./tsconfig-aot.json’

    Is ngc installed and running from the command line?

    1. Manjit Singh · · Reply

      How can i install and run this?

      1. npm install ngc -g

      2. Manjit singh · ·

        I have installed this now its giving following error:

        0 info it worked if it ends with ok
        1 verbose cli [ ‘C:\\Program Files\\nodejs\\node.exe’,
        1 verbose cli ‘C:\\Users\\manjit.singh\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js’,
        1 verbose cli ‘run’,
        1 verbose cli ‘ngc’ ]
        2 info using npm@4.5.0
        3 info using node@v7.9.0
        4 verbose run-script [ ‘prengc’, ‘ngc’, ‘postngc’ ]
        5 info lifecycle @1.0.0~prengc: @1.0.0
        6 silly lifecycle @1.0.0~prengc: no script for prengc, continuing
        7 info lifecycle @1.0.0~ngc: @1.0.0
        8 verbose lifecycle @1.0.0~ngc: unsafe-perm in lifecycle true
        9 verbose lifecycle @1.0.0~ngc: PATH: C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\bin\node-gyp-bin;D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\node_modules\.bin;C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\bin\node-gyp-bin;D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\node_modules\.bin;C:\Program Files\Common Files\Microsoft Shared\Microsoft Online Services;C:\Program Files (x86)\Common Files\Microsoft Shared\Microsoft Online Services;C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\ProgramData\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\ManagementStudio\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\dotnet\;C:\Program Files (x86)\Skype\Phone\;C:\Program Files\nodejs\;C:\Program Files (x86)\Microsoft VS Code\bin;C:\Users\manjit.singh\AppData\Roaming\npm
        10 verbose lifecycle @1.0.0~ngc: CWD: D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client
        11 silly lifecycle @1.0.0~ngc: Args: [ ‘/d /s /c’, ‘ngc -p ./tsconfig-aot.json’ ]
        12 silly lifecycle @1.0.0~ngc: Returned: code: 1 signal: null
        13 info lifecycle @1.0.0~ngc: Failed to exec ngc script
        14 verbose stack Error: @1.0.0 ngc: `ngc -p ./tsconfig-aot.json`
        14 verbose stack Exit status 1
        14 verbose stack at EventEmitter. (C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\lib\utils\lifecycle.js:279:16)
        14 verbose stack at emitTwo (events.js:106:13)
        14 verbose stack at EventEmitter.emit (events.js:194:7)
        14 verbose stack at ChildProcess. (C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\lib\utils\spawn.js:40:14)
        14 verbose stack at emitTwo (events.js:106:13)
        14 verbose stack at ChildProcess.emit (events.js:194:7)
        14 verbose stack at maybeClose (internal/child_process.js:899:16)
        14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:226:5)
        15 verbose pkgid @1.0.0
        16 verbose cwd D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client
        17 verbose Windows_NT 6.3.9600
        18 verbose argv “C:\\Program Files\\nodejs\\node.exe” “C:\\Users\\manjit.singh\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js” “run” “ngc”
        19 verbose node v7.9.0
        20 verbose npm v4.5.0
        21 error code ELIFECYCLE
        22 error errno 1
        23 error @1.0.0 ngc: `ngc -p ./tsconfig-aot.json`
        23 error Exit status 1
        24 error Failed at the @1.0.0 ngc script ‘ngc -p ./tsconfig-aot.json’.
        24 error Make sure you have the latest version of node.js and npm installed.
        24 error If you do, this is most likely a problem with the package,
        24 error not with npm itself.
        24 error Tell the author that this fails on your system:
        24 error ngc -p ./tsconfig-aot.json
        24 error You can get information on how to open an issue for this project with:
        24 error npm bugs
        24 error Or if that isn’t available, you can get their info via:
        24 error npm owner ls
        24 error There is likely additional logging output above.
        25 verbose exit [ 1, true ]

        2nd log file:

        0 info it worked if it ends with ok
        1 verbose cli [ ‘C:\\Program Files\\nodejs\\node.exe’,
        1 verbose cli ‘C:\\Users\\manjit.singh\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js’,
        1 verbose cli ‘run’,
        1 verbose cli ‘buildProduction’ ]
        2 info using npm@4.5.0
        3 info using node@v7.9.0
        4 verbose run-script [ ‘prebuildProduction’,
        4 verbose run-script ‘buildProduction’,
        4 verbose run-script ‘postbuildProduction’ ]
        5 info lifecycle @1.0.0~prebuildProduction: @1.0.0
        6 silly lifecycle @1.0.0~prebuildProduction: no script for prebuildProduction, continuing
        7 info lifecycle @1.0.0~buildProduction: @1.0.0
        8 verbose lifecycle @1.0.0~buildProduction: unsafe-perm in lifecycle true
        9 verbose lifecycle @1.0.0~buildProduction: PATH: C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\bin\node-gyp-bin;D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\node_modules\.bin;C:\Program Files\Common Files\Microsoft Shared\Microsoft Online Services;C:\Program Files (x86)\Common Files\Microsoft Shared\Microsoft Online Services;C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\ProgramData\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\ManagementStudio\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\dotnet\;C:\Program Files (x86)\Skype\Phone\;C:\Program Files\nodejs\;C:\Program Files (x86)\Microsoft VS Code\bin;C:\Users\manjit.singh\AppData\Roaming\npm
        10 verbose lifecycle @1.0.0~buildProduction: CWD: D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client
        11 silly lifecycle @1.0.0~buildProduction: Args: [ ‘/d /s /c’, ‘npm run ngc && npm run webpack-prod’ ]
        12 silly lifecycle @1.0.0~buildProduction: Returned: code: 1 signal: null
        13 info lifecycle @1.0.0~buildProduction: Failed to exec buildProduction script
        14 verbose stack Error: @1.0.0 buildProduction: `npm run ngc && npm run webpack-prod`
        14 verbose stack Exit status 1
        14 verbose stack at EventEmitter. (C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\lib\utils\lifecycle.js:279:16)
        14 verbose stack at emitTwo (events.js:106:13)
        14 verbose stack at EventEmitter.emit (events.js:194:7)
        14 verbose stack at ChildProcess. (C:\Users\manjit.singh\AppData\Roaming\npm\node_modules\npm\lib\utils\spawn.js:40:14)
        14 verbose stack at emitTwo (events.js:106:13)
        14 verbose stack at ChildProcess.emit (events.js:194:7)
        14 verbose stack at maybeClose (internal/child_process.js:899:16)
        14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:226:5)
        15 verbose pkgid @1.0.0
        16 verbose cwd D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client
        17 verbose Windows_NT 6.3.9600
        18 verbose argv “C:\\Program Files\\nodejs\\node.exe” “C:\\Users\\manjit.singh\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js” “run” “buildProduction”
        19 verbose node v7.9.0
        20 verbose npm v4.5.0
        21 error code ELIFECYCLE
        22 error errno 1
        23 error @1.0.0 buildProduction: `npm run ngc && npm run webpack-prod`
        23 error Exit status 1
        24 error Failed at the @1.0.0 buildProduction script ‘npm run ngc && npm run webpack-prod’.
        24 error Make sure you have the latest version of node.js and npm installed.
        24 error If you do, this is most likely a problem with the package,
        24 error not with npm itself.
        24 error Tell the author that this fails on your system:
        24 error npm run ngc && npm run webpack-prod
        24 error You can get information on how to open an issue for this project with:
        24 error npm bugs
        24 error Or if that isn’t available, you can get their info via:
        24 error npm owner ls
        24 error There is likely additional logging output above.
        25 verbose exit [ 1, true ]

      3. Manjit singh · ·

        I think its giving the same error 😦

  3. Andrew · · Reply

    How to refresh token ? I’ve gotten access_token, id_token, but dont understand how to refresh access_token.
    There is no refresh_token in result.
    let result: any = hash.split(‘&’).reduce(function (result: any, item: string) {
    let parts = item.split(‘=’);
    result[parts[0]] = parts[1];
    return result;
    }, {});

    1. You need to implement openId session management for this if using this flow. I plan to do this when I get time.

      Greetings Damien

      1. Andrew · ·

        Thanks for quick answer

  4. […] Angular 2 with OpenID Connect Implicit Flow from Damien Bowden […]

  5. Great tutorial! The thing I’m a bit concerned about is using jsrsasign. It’s a massive library and worst of all, includes a version of yui from 2011, which apparently manages to hook itself into all kinds of places, thus slowing down the entire site it’s used in. Have you come across anything that could be used to replace jsrsasign? I just recently started looking into this myself… Of course, I could just rely on the server side to validate the token, which it will do anyway, but it would be nice to do some checking on the client side as well.

    1. Thanks, I have done very little research into jsrasign, thanks for this info, very useful. Just one note, you must validate the signature of the token and also the token on the client, without the client side validation, the application is way less secure. Validating on the server is no help. I will research into this further, or if you find an replacement, please let me know.

      Greetings Damien

      1. You’re right, of course. Looks like Auth0 (unsurprisingly) has several packages that may be suitable for replacing jsrsasign for the JWT stuff. Their https://github.com/auth0/idtoken-verifier looks like it might work, or even https://github.com/auth0/angular-jwt for a more complete solution.

  6. […] ANGULAR OPENID CONNECT IMPLICIT FLOW WITH IDENTITYSERVER4 ASP.NET Core & Angular2 + OpenID Connect using Visual Studio Code Repo for the previous link Repo for with example Angular OidcClient […]

  7. […] Angular OpenID Connect Implicit Flow with IdentityServer4 […]

  8. Jared Gombert · · Reply

    Hi Damien, your blog is AMAZING. I wanted to have the angular client automatically take the user to the login page if they are not logged in. Where would be the best place to implement this in the app? Thanks in advance.

    1. Oleg Krums · · Reply

      Hi Jared,

      I’m having the same problem.

      When calling this.oidcSecurityService.authorize() on component init it keeps redirecting to Identity Server even when authenticated. It only works when this.oidcSecurityService.authorize() is called from Login button.

      Did you manage to resolve it?

      1. Jared Gombert · ·

        Hi Oleg, I hadn’t spent any time on finding a solution to this yet so thank you too much for sharing the solution, I really appreciate it!

  9. Wondering, how can we manage concurrent logins, I see you are using reference tokens, however silent authentication kept re-issueing the token in my case short lived access token, though it expired IDM is creating a new token without taking the user credentials ( if it was I get a chance to check the DB and put a check here)

  10. Jonathan · · Reply

    Hi Damien,
    I see that you just updated the associated example program that goes with this article, and that you include the /app/auth classes instead of pulling in https://github.com/damienbod/angular-auth-oidc-client , which looks to be the same code npm’d. Is there a reason for this, and do you recommend the npm package over the actual source in this example? I’m trying to piece together how to proceed from the myriad of (different) articles out there and (different) example projects. I have to say, this is an instance where too many choice is actually a bad thing….
    Thanks in advance

  11. Hi Damien, I downloaded the code from GitHub. How to run the application? I see the AngularClient and StsServerIdentity projects. What’s the process to run them? Thanks

  12. […] Damien published a great article that shows how to use Angular 2 with OpenID Connect Implicit Flow and Identity Server 4. […]

  13. scott kirsten-roger · · Reply

    B’jour Damien. Is there a reason you embed the angular app within a .NET app? Is there a way to free the Angular app from the .NET Core framework? Also, thank you for everything that you blog about and giving out your knowledge and experience to the world.

  14. Hi Damien,
    under which license did you publish the demo application on GitHub?
    Would it be okay to fork it for my own purposes and use it in a project?
    Thanks
    Marco

    1. Hi Marco MIT, I’ll add the license if it’s missing. Use the code anyway you want

      Greetings Damien

  15. so how do I use your code form your git is it download or run or should I use this blog because I am learning to use IdentityServer and need to implement it into an angular project so just want to look how your program is working but don’t get it to start

Leave a comment

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