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 1.1 resource server. The OpenID Connect specification for Implicit Flow can be found here.

Code: VS2017 msbuild | VS2015 project.json

History:
2017.03.24: 2017.03.24: Updated to angular 4.0.0

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 = "angular2client",
	ClientId = "angular2client",
	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": "1.0.0",
  "description": "An Angular VS template",
  "main": "wwwroot/index.html",
  "author": "",
  "license": "ISC",
    "repository": {
    "type": "git",
    "url": "https://github.com/damienbod/Angular2WebpackVisualStudio.git"
  },
  "scripts": {
    "ngc": "ngc -p ./tsconfig-aot.json",
    "start": "concurrently \"webpack-dev-server --hot --inline --port 8080\" \"dotnet run\" ",
    "webpack-dev": "set NODE_ENV=development && webpack",
    "webpack-production": "set NODE_ENV=production && webpack",
    "build-dev": "npm run webpack-dev",
    "build-production": "npm run ngc && npm run webpack-production",
    "watch-webpack-dev": "set NODE_ENV=development && webpack --watch --color",
    "watch-webpack-production": "npm run build-production --watch --color",
    "publish-for-iis": "npm run build-production && dotnet publish -c Release"
  },
  "dependencies": {
    "@angular/common": "4.0.0",
    "@angular/compiler": "4.0.0",
    "@angular/core": "4.0.0",
    "@angular/forms": "4.0.0",
    "@angular/http": "4.0.0",
    "@angular/platform-browser": "4.0.0",
    "@angular/platform-browser-dynamic": "4.0.0",
    "@angular/router": "4.0.0",
    "@angular/upgrade": "4.0.0",
    "@angular/animations": "4.0.0",
    "angular-in-memory-web-api": "0.3.1",
    "core-js": "2.4.1",
    "reflect-metadata": "0.1.9",
    "rxjs": "5.0.3",
    "zone.js": "0.8.4",
    "@angular/compiler-cli": "4.0.0",
    "@angular/platform-server": "4.0.0",
    "bootstrap": "^3.3.7",
    "ie-shim": "~0.1.0"
  },
  "devDependencies": {
     "@types/node": "7.0.8",
    "angular2-template-loader": "0.6.2",
    "angular-router-loader": "^0.5.0",
    "awesome-typescript-loader": "3.1.2",
    "clean-webpack-plugin": "^0.1.15",
    "concurrently": "^3.4.0",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.27.1",
    "file-loader": "^0.10.1",
    "html-webpack-plugin": "^2.28.0",
    "jquery": "^3.1.1",
    "json-loader": "^0.5.4",
    "node-sass": "^4.5.0",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.6.1",
    "sass-loader": "^6.0.3",
    "source-map-loader": "^0.2.0",
    "style-loader": "^0.13.2",
    "ts-helpers": "^1.1.2",
    "tslint": "^4.5.1",
    "tslint-loader": "^3.4.3",
    "typescript": "2.2.1",
    "url-loader": "^0.5.8",
    "webpack": "^2.2.1",
    "webpack-dev-server": "2.4.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,
    "lib": [
      "es2015",
      "dom"
    ],
    "types": [
      "node"
    ]
  },
  "files": [
    "angularApp/app/app.module.ts",
    "angularApp/main.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. This could also be done for example using JSPM, but Visual Studio doesn’t work properly together with JSPM.

<!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 1.0 Angular IdentityServer4 Client</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
	
    <script src="assets/jsrsasign.min.js"></script>
</head>
<body>
    <my-app>Loading...</my-app>
</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/js/bootstrap';

import './css/bootstrap.css';
import './css/bootstrap-theme.css';

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.UseIISPlatformHandler();

         var angularRoutes = new[] {
                "/forbidden",
                "/home",
                "/authorized",
                "/authorize",
                "/unauthorized",
                "/dataeventrecords",
                "/dataeventrecords/create",
                "/dataeventrecords/edit",
                "/logoff",
                "/securefile",
                "/securefile/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.Run(async (context) =>
	{
		await context.Response.WriteAsync("Hello World!");
	});
}

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.

// Entry point for JiT compilation.
declare var System: any;

import './vendor';
import './polyfills';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

// 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, componenets and services.

import { NgModule } 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 { HttpModule, JsonpModule } from '@angular/http';

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

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 { DataEventRecordsListComponent } from './dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords/dataeventrecords-edit.component';
import { AuthModule } from './auth/auth.module';
import { OidcSecurityService } from './auth/services/oidc.security.service';

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        routing,
        HttpModule,
        JsonpModule,
        AuthModule.forRoot(),
    ],
    declarations: [
        AppComponent,
        ForbiddenComponent,
        HomeComponent,
        UnauthorizedComponent,
        SecureFilesComponent,
        DataEventRecordsListComponent,
        DataEventRecordsCreateComponent,
        DataEventRecordsEditComponent
    ],
    providers: [
        OidcSecurityService,
        SecureFileService,
        DataEventRecordsService,
        Configuration
    ],
    bootstrap:    [AppComponent],
})

export class AppModule {}

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" style="margin-top: 15px;">
    <!-- Static navbar -->
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <button aria-controls="navbar" aria-expanded="false" data-target="#navbar" data-toggle="collapse" class="navbar-toggle collapsed" type="button">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a [routerLink]="['/dataeventrecords']" class="navbar-brand"><img src="images/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <div class="navbar-collapse collapse" id="navbar">
                <ul class="nav navbar-nav">
                    <li><a [routerLink]="['/dataeventrecords']">DataEventRecords</a></li>
                    <li><a [routerLink]="['/dataeventrecords/create']">Create DataEventRecord</a></li>
                    <li><a [routerLink]="['/securefile/securefiles']">Secured Files Download</a></li>

                    <li><a class="navigationLinkButton" *ngIf="!securityService.IsAuthorized()" (click)="Login()">Login</a></li>
                    <li><a class="navigationLinkButton" *ngIf="securityService.IsAuthorized()" (click)="Logout()">Logout</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 } from '@angular/core';
import { Router } from '@angular/router';

import { Configuration } from './app.constants';

import { OidcSecurityService } from './auth/services/oidc.security.service';
import { SecureFileService } from './securefile/SecureFileService';
import { DataEventRecordsService } from './dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './dataeventrecords/models/DataEventRecord';

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 { DataEventRecordsListComponent } from './dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords/dataeventrecords-edit.component';

import './app.component.css';

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

export class AppComponent implements OnInit {

    constructor(public securityService: OidcSecurityService) {
    }

    ngOnInit() {
        console.log('ngOnInit _securityService.AuthorizedCallback');

        if (window.location.hash) {
            this.securityService.AuthorizedCallback();
        }
    }

    public Login() {
        console.log('Do login logic');
        this.securityService.Authorize();
    }

    public Logout() {
        console.log('Do logout logic');
        this.securityService.Logoff();
    }
}

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 { DataEventRecordsListComponent } from './dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords/dataeventrecords-edit.component';

const appRoutes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'home', component: HomeComponent },
    { path: 'Forbidden', component: ForbiddenComponent },
    { path: 'Unauthorized', component: UnauthorizedComponent },
    { path: 'securefile/securefiles', component: SecureFilesComponent },
    {
        path: 'dataeventrecords', component: DataEventRecordsListComponent
    },
    {
        path: 'dataeventrecords/create',
        component: DataEventRecordsCreateComponent
    },
    {
        path: 'dataeventrecords/edit/:id',
        component: DataEventRecordsEditComponent
    },
    {
        path: 'dataeventrecords/list',
        component: DataEventRecordsListComponent,
    }
];

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';

@Injectable()
export class AuthConfiguration {

    // The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.
    public iss = 'https://localhost:44318';

    public server = 'https://localhost:44318';

    public redirect_url = 'https://localhost:44311';

    // This is required to get the signing keys so that the signiture of the Jwt can be validated.
    public jwks_url = 'https://localhost:44318/.well-known/openid-configuration/jwks';

    // 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.
    public client_id = 'angular2client';

    public response_type = 'id_token token';

    public scope = 'dataEventRecords securedFiles openid';

    public post_logout_redirect_uri = 'https://localhost:44311/Unauthorized';
}

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.

public Authorize() {
	this.ResetAuthorizationData();

	console.log('BEGIN Authorize, no auth data');

	let authorizationUrl = this._configuration.server + '/connect/authorize';
	let client_id = this._configuration.client_id;
	let redirect_uri = this._configuration.redirect_url;
	let response_type = this._configuration.response_type;
	let scope = this._configuration.scope;
	let nonce = 'N' + Math.random() + '' + Date.now();
	let state = Date.now() + '' + Math.random();

	this.store('authStateControl', state);
	this.store('authNonce', nonce);
	console.log('AuthorizedController created. adding myautostate: ' + this.retrieve('authStateControl'));

	let url =
		authorizationUrl + '?' +
		'response_type=' + encodeURI(response_type) + '&' +
		'client_id=' + encodeURI(client_id) + '&' +
		'redirect_uri=' + encodeURI(redirect_uri) + '&' +
		'scope=' + encodeURI(scope) + '&' +
		'nonce=' + encodeURI(nonce) + '&' +
		'state=' + encodeURI(state);

	window.location.href = 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.

public AuthorizedCallback() {
	console.log('BEGIN AuthorizedCallback, no auth data');
	this.ResetAuthorizationData();

	let hash = window.location.hash.substr(1);

	let result: any = hash.split('&').reduce(function (result: any, item: string) {
		let parts = item.split('=');
		result[parts[0]] = parts[1];
		return result;
	}, {});

	console.log(result);
	console.log('AuthorizedCallback created, begin token validation');

	let token = '';
	let id_token = '';
	let authResponseIsValid = false;

	this.getSigningKeys()
		.subscribe(jwtKeys => {
			this.jwtKeys = jwtKeys;

			if (!result.error) {

				// validate state
				if (this.oidcSecurityValidation.ValidateStateFromHashCallback(result.state, this.retrieve('authStateControl'))) {
					token = result.access_token;
					id_token = result.id_token;
					let decoded: any;
					let headerDecoded;
					decoded = this.oidcSecurityValidation.GetPayloadFromToken(id_token, false);
					headerDecoded = this.oidcSecurityValidation.GetHeaderFromToken(id_token, false);

					// validate jwt signature
					if (this.oidcSecurityValidation.Validate_signature_id_token(id_token, this.jwtKeys)) {
						// validate nonce
						if (this.oidcSecurityValidation.Validate_id_token_nonce(decoded, this.retrieve('authNonce'))) {
							// validate iss
							if (this.oidcSecurityValidation.Validate_id_token_iss(decoded, this._configuration.iss)) {
								// validate aud
								if (this.oidcSecurityValidation.Validate_id_token_aud(decoded, this._configuration.client_id)) {
									// valiadate at_hash and access_token
									if (this.oidcSecurityValidation.Validate_id_token_at_hash(token, decoded.at_hash) || !token) {
										this.store('authNonce', '');
										this.store('authStateControl', '');

										authResponseIsValid = true;
										console.log('AuthorizedCallback state, nonce, iss, aud, signature validated, returning token');
									} else {
										console.log('AuthorizedCallback incorrect aud');
									}
								} else {
									console.log('AuthorizedCallback incorrect aud');
								}
							} else {
								console.log('AuthorizedCallback incorrect iss');
							}
						} else {
							console.log('AuthorizedCallback incorrect nonce');
						}
					} else {
						console.log('AuthorizedCallback incorrect Signature id_token');
					}
				} else {
					console.log('AuthorizedCallback incorrect state');
				}
			}

			if (authResponseIsValid) {
				this.SetAuthorizationData(token, id_token);
				console.log(this.retrieve('authorizationData'));

				// router navigate to DataEventRecordsList
				this._router.navigate(['/dataeventrecords/list']);
			} else {
				this.ResetAuthorizationData();
				this._router.navigate(['/Unauthorized']);
			}
		});
}

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

import { Injectable } from '@angular/core';

// 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 {

    // 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).
    public 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.
    public Validate_id_token_nonce(dataIdToken: any, local_nonce: any): boolean {
        if (dataIdToken.nonce !== local_nonce) {
            console.log('Validate_id_token_nonce failed');
            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.
    public Validate_id_token_iss(dataIdToken: any, client_id: any): boolean {
        if (dataIdToken.iss !== client_id) {
            console.log('Validate_id_token_iss failed');
            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.
    public Validate_id_token_aud(dataIdToken: any, aud: any): boolean {
        if (dataIdToken.aud !== aud) {
            console.log('Validate_id_token_aud failed');
            return false;
        }

        return true;
    }

    public ValidateStateFromHashCallback(state: any, local_state: any): boolean {
        if (state !== local_state) {
            console.log('ValidateStateFromHashCallback failed');
            return false;
        }

        return true;
    }

    public 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;
    }

    public 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;
    }

    public 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.
    public 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) {
            console.log('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.
    public 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.

public ResetAuthorizationData() {
	this.store('authorizationData', '');
	this.store('authorizationDataIdToken', '');

	this._isAuthorized = false;
	this.HasAdminRole = false;
	this.store('HasAdminRole', false);
	this.store('_isAuthorized', false);
}

public SetAuthorizationData(token: any, id_token: any) {
	if (this.retrieve('authorizationData') !== '') {
		this.store('authorizationData', '');
	}

	console.log(token);
	console.log(id_token);
	console.log('storing to storage, getting the roles');
	this.store('authorizationData', token);
	this.store('authorizationDataIdToken', id_token);
	this._isAuthorized = true;
	this.store('_isAuthorized', true);

	this.getUserData()
		.subscribe(data => this.UserData = data,
		error => this.HandleError(error),
		() => {
			for (let i = 0; i < this.UserData.role.length; i++) {
				if (this.UserData.role[i] === 'dataEventRecords.admin') {
					this.HasAdminRole = true;
					this.store('HasAdminRole', true);
				}
				if (this.UserData.role[i] === 'admin') {
					this.HasUserAdminRole = true;
					this.store('HasUserAdminRole', 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="securityService.HasAdminRole" href="" [routerLink]="['/dataeventrecords/edit/' + dataEventRecord.Id]" >{{dataEventRecord.Name}}</a>
                            <span *ngIf="!securityService.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.

public HandleError(error: any) {
        console.log(error);
        if (error.status == 403) {
            this._router.navigate(['Forbidden'])
        }
        else if (error.status == 401) {
            this.ResetAuthorizationData();
            this._router.navigate(['Unauthorized'])
        }
    }

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

Advertisements

110 comments

  1. You sir are one very “updated” gentlemen. Excellenet article

  2. Great article. Are there any changes in how the protocoll is implemented in IdentityServer4, or would this angular2 client implementation also work directly against IdentityServer3?

    1. Thanks

      The protocol is the same. It should work without problems using IdentityServer3 but I have not tested this.

      Greetings Damien

  3. Implicit flow is all fine and dandy until you need to show image resources that are secured.

    1. Hi Dave

      Thanks for your comment. There is no difference between an image resource and a json resource, both can be secured and the Implicit Flow has nothing to do with this. The resource server has to provide secured images. The client needs to add the token to the image HTTP request. If it is not possible to add it to the header on the client, it can be added to the query string and middleware can be added to the resource server to handle this, but this is not a problem of the Implicit flow.

      What type of problem do/did you have with images and Implicit Flow?

      Greetings Damien

      1. Browsers don’t add the token to the header, and if you add it to the query string you’ll break caching.

      2. Hi Dave

        If the image is secured, you need to break the cache (Or use a header token) otherwise the next person to use your browser can view the image as well, without logging in.

        Greetings Damien

      3. Technically, you are correct, but I’m finding that a very weak argument.

      4. True, not important if no users share there browsers, it’s a trade between requirements, you can also change the way the client gets the secured images.

  4. Gaetan · · Reply

    Is it really secure to use implicit flow with a SPA ? Is it secure to store the Token in the browser’s storage ?

  5. Is it really secure to use implicite flow ina SPA ? Is it secure to store the Token in the browser’s storage ?

    1. No security is really secure. Storing the token in local storage has its CONs as with not saving the token client side at all, or saving it in a cookie. All have PROs and CONS.

      Greetings Damien

  6. I’m wondering about security, if it is that simple to get the entire source of everything with just one GET in a browser?

    http://localhost:5000/app/dataeventrecords/dataeventrecords-create.component.ts

    Tada… Login? Nada…

    1. Hi Neil, thanks for your comment.

      The client has always the src code in a SPA,the uglified bundle for production or the dev build ts like you pointed out. That’s why the OpenID Connect Implicit Flow is recommended for SPAs.

      Greetings Damien

  7. Aakash Jain · · Reply

    Thanks for the excellent example. A question please, where is the index.html or other templates file in AngularClient solution. It is very confusing to follow the code with this post.

  8. I wanted to add this link as this seems to be my problem I mentioned in the previous comment:
    https://github.com/aspnet/DataProtection/issues/139
    Any idea how to resolve this?

  9. Vladimir · · Reply

    Hi

    I have three Angular 2 projects:
    1. client.mydomain.com
    2. company.mydomain.com
    3. admin.mydomain.com

    All are subdomains of my domain.
    I want to use identity server to allow single login for all this websites.
    What is the best way to create such flow?

    1. OpenID Connect Implicit Flow, one identityServer Client config per app, and then each IdentityServer user config has the claims for each app. Sign out should do a IdentityServer log out .Greetings Damien

      1. Vladimir · ·

        Yes, I know this.
        I mean that when user already login on client.mydomain.com he has token and when user goes to company.mydomain.com he doesn’t have token, so SPA had to go to identity server and get it.
        The problem is that user already login and identity server just redirects user back. User doesn’t have to enter login and password again because he already logged in and this is GOOD! I just want to remove this unnecessary redirect.

        Can I request token from Identity Server if user already logged in without redirects?

        Thank you!

      2. Hi you could implemented PreAuthenticate

        https://github.com/IdentityServer/IdentityServer3.Samples/issues/61

        Hope this is what your looking for.

        Greetings Damien

      3. Vladimir · ·

        Damien, thank you.

        After reading different posts it looks like I just need to create custom solution using IdentityServer4.Core
        I decided to create auth.mydomain.com
        On this website I will have method that will issue token.
        I’m not sure if I’m right, please provide your idea.

        P.S.
        When user from client.mydomain.com need to access info he will be redirected to auth.mydomain.com
        Then back with token.
        Then when user go to company.mydomain.com, Angular during loading will send GET request to auth.mydomain.com. (e.g. auth.mydomain.com/token) and because user already authorized app will get token in response.

        Is that correct flow?

  10. Hi Damien,

    Can you share how the process of updating to webpack? It is not very clear from the code. How does the application determine the entry point when you use webpack.

    Tayo

    1. Hi Tayo, sounds like a good idea, need a couple of days though, a busy week coming up, don’t have much time.

      Greetings Damien

  11. Alex Golovan · · Reply

    I am using your demo and this is great – however, I am having some very simple Angular 2.0 issue that I can’t solve. I am not able to use this.router.navigate([‘Unauthorized’]); from SecurityServce.ts, however, it works very well from app.component. I was doing a lot of samples and always was able to navigate from other class than app.component. I am still using router-deprecated but it should not be a problem. Is it any way I can email you my very simple project to take a look? Thanks!

    1. Hi Alex, Maybe your missing an import. Try updating to the new router as the old one is disappearing. I also had a bug with the old router that the IdentityServer redirect did not work for a angular route so I set the redirect to the parent. This bug in angular is closed as the router is no longer supported.

      Greetings Damien

      1. I figured out and updated to use of new router. However, redirect is still does not work when we get back with Denied for example. Very interesting – redirect works from services but not when we are returning back from identity server.

  12. Alex Golovan · · Reply

    What I mean, that when it is returning from IdentityServer, route.Navigate never worked for me.

    However, I have another question, could you recommend this approach for building real web site? Another approach is to use Identity Server at API level, only to return data when propertu authenticated. What do you think?

    1. Hi Alex

      This depends on your environment. I would always recommend OpenID Connect Implicit Flow for SPAs. The big choice is, if this should be a separate application (IdentityServer) or included in the server application. Your deployment, target clients define this. This depends on your system, targets.

      Hope this helps

      Greetings Damien

      1. We are building SPA application with Angular 2.0 front end – do you mean that the approach in this article is enough and there no need to hook identity server at API level? Just your opinion 🙂

  13. John · · Reply

    Hey man, thanks a lot for your efforts!
    I’m wondering if there would be a lot of work to implement hybrid flow? Mainly I’m curious because it would make it great for using refresh tokens.

  14. Alex · · Reply

    I have to put this app into different folder structure to debug using Chrome debugger.

  15. Alex Golovan · · Reply

    great new version – thanks a lot!

      1. Alex Golovan · ·

        Another interesting question – when I updated ts file, SecurityService for example, build and run, I don’t see my changes. Actually, I am using the Chrome debugger and look at webpack://, wwwroot/app. I did not make any config changes, just got this sample. Any ideas? Thanks!

      2. Hi Alex, you need to run the webpack build

        greetings Damien

  16. Alex Golovan · · Reply

    Very interesting – I am working with latest version – and redirect after returning back from Identity Server is still not working for me:

    if (authResponseIsValid) {
    this.SetAuthorizationData(token, id_token);
    console.log(this.retrieve(“authorizationData”));

    // router navigate to DataEventRecordsList
    this._router.navigate([‘/dataeventrecords’]);
    }
    else {
    this.ResetAuthorizationData();
    this._router.navigate([‘/Unauthorized’]);
    }

    When authResponseIsValid, it updates menu system to show Logout but does not display data event records. When authResponseIsValid is not valid, it does not display anything just still has Login at menu. Could you check please if your local version works correctly? I was having this issue some time ago with depreciated router, however, it is still there! Thanks!

  17. Alex Golovan · · Reply

    This is what I see in Chrome debugger: Uncaught (in promise): Error: Cannot match any routes: ”

    1. Thanks, I need to fix this, I’ll create a github issue, so I don’t forget

  18. Alex Golovan · · Reply

    I also not able to change TS code and see changes in Chrome debugger – it is still same code as in original article. May be it is something with Webpack configuration?

  19. Alex Golovan · · Reply

    I am able to compile code, using Task Runner Explore! This is what I see there – if I put this._router.navigate([‘/Unauthorized’]); in Authorize() it works, if I pit into AuthorizedCallback() – it does not work. Very interesting!

  20. Alex Golovan · · Reply

    Adding redirectTo sections solves this issue for me – not sure if it is the best approach:

    export const routes: RouterConfig = [
    { path: ”, component: HomeComponent, pathMatch: ‘full’ },
    { path: ‘Forbidden’, component: ForbiddenComponent, terminal: true },
    { path: ‘Unauthorized’, component: UnauthorizedComponent, terminal: true },
    { path: ‘securefile/securefiles’, component: SecureFilesComponent, terminal: true },
    { path: “**”, redirectTo: ‘dataeventrecords’ },

    1. cool, thanks

      1. Do you know by any chance that we can use Windows Authentication with Identity Server 4 in same way as it was possible with 3 version?

  21. Alex Golovan · · Reply

    Very interesting question – your TS files are under wwwroot? What would you recommend – under wwwroot or beyond? Thanks!

    1. The TS files in wwwroot are not really a good idea. Here’s a better solution for a angular 2 project

      https://github.com/damienbod/Angular2WebpackVisualStudio

      Microsoft are also working on a aspnetcore angular2 template which is nice.

      The angular-cli project is also good.

      The main or big challenge is to have a proper production build, but also possible to debug in dev.

      greetings Damien

      1. Alex Golovan · ·

        Thanks, Demien. Is not enough to put just bundle files into production and debug minified version there?

      2. Hi Alex

        Yes, but everything inside the wwwroot is published per default, so the question is does it matter if you deliver your src files.

        If the app src, debug files were outside the wwwroot and could be debugged but only the build files either dev or prod were inside the wwwroot, this would feel better for me.

        And at present most of the aspnetcore, angular2 templates cannot be used in production…

        Webpack solutions or angular-cli are the ones which will work.

        Greetings Damien

  22. Alex Golovan · · Reply

    What is the best way to get Token in ResourceServer (Web API) and get user name of the logged user?

    1. Never thought about this. The user name is sent with every request in the access_token. You could use this to get the user name (for example a filter) or send it as payload in the request.

      Greetings Damien

  23. Alex Golovan · · Reply

    Very interesting – when I load URL from browser, it actually does not work. Like in my app, the following url from RouteLink, will open correct page: https://localhost:44312/request/list/136.
    However, if I reload, it does not work correctly.

    I am trying to put into Startup.cs code like that:

    var angularRoutes = new[] {
    “/home”,
    “/request”,
    “/request/details”,
    “/request/list”
    };

    It still does not work correctly. I think I can create MVC controller but I don’t want to do something like that. I also reading on Web – this is what they are telling:

    That’s a browser feature.
    Angular by default uses HTML5 pushstate (PathLocationStrategy in Angular slang).
    You either need a server that processes all requests like it were requesting index.html or you switch to HashLocationStrategy (with # in the URL for routes)

    What is the best approach according with your experience?

  24. Alex Golovan · · Reply

    I figured out this issue – however, I am not sure that this is the best approach. I put to the index.html, added additional path, request, to Startup.css: var angularRoutes = new[] { “/home”, “/request” }; plus some more small changes and I can reload page or just use url to activate corresponding state. Thanks again for all those samples and help!

  25. Hello,

    what is the workflow to work with Angular2Client project? (With angular-client is easier because ng serve do all it for you. What is the right approach with this type of project?)

    I’ve added AuthManager.ts in Angular2Client project. The path for this service is: wwwroot\app\services\AuthManager.ts

    The problem is that this added file is not autogenerating the .js file file from the ts file.

    Also I’ve changed the app.module.ts but when I open it in the browser the code is not the code I have written.

    How could I agilize this autogeneration/updating proccess? it would be great some webpack task or something like this.

    1. Ok,

      I have installed webpack task runner explorer addin,
      changed tsconfig.json:
      “compileOnSave”: true,
      “buildOnSave”: true
      And runned webpack task: Watch – Development

      Now I’m able to code faster!

      1. Alex Golovan · ·

        I did all of that to code faster, but when I changed index.html for example, it does not change it at index.html under wwwrooot.

        This is what I have – at the top of webpack.config.js:
        ///

        in tsconfig.json:

        {
        “compilerOptions”: {
        “target”: “es5”,
        “module”: “commonjs”,
        “moduleResolution”: “node”,
        “removeComments”: true,
        “emitDecoratorMetadata”: true,
        “experimentalDecorators”: true,
        “noEmitHelpers”: false,
        “sourceMap”: true
        },
        “exclude”: [
        “node_modules”
        ],
        “compileOnSave”: true,
        “buildOnSave”: true
        }

        Am I missing something? Thanks!

  26. cool, I will change to default to this. thanks

    Greetings Damien

  27. What is the best way to make a registration process and user persistence ? Asp core identity or identity server have a user persistence store as configuration persistence ?

    1. Hi Janick I’ve just updated the git repo with an example for this. It uses Identity with SQLite and EFCore with IdentityServer4. I think this is a good solution with maybe a different database.

      Greetings Damien

  28. Fantastic post Damien – Thanks for the hard work. It’s got me motivated to look at .NET Core which i’ve been putting off for a while!

    One quick question – I can’t seem to get access to the username of the user. I can see someone asked this question earlier – but i’m unsure on your response.

    I’ve tried adding additional Allowed Scopes to the Client for “Email” and “profile”. But the username is never on the Claims Principal when i send down the token to the API – any ideas why these claims may be getting missed out on the token?

    Cheers,

    Richard

  29. Fantastic post Damien – Thanks for the hard work. It’s got me motivated to look at .NET Core which i’ve been putting off for a while!

    One quick question – I can’t seem to get access to the username of the user. I can see someone asked this question earlier – but i’m unsure on your response.

    I’ve tried adding additional Allowed Scopes to the Client for “Email” and “profile”. But the username is never on the Claims Principal when i send down the token to the API – any ideas why these claims may be getting missed out on the token?

    Cheers,

    Richard

    1. Hi Richard, thanks for your feedback.

      You could add your claims in an IProfileService implementation and these will be available for the client.

      https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow/blob/master/src/IdentityServerWithIdentitySQLite/IdentityWithAdditionalClaimsProfileService.cs

      1. Perfect ! Works a treat! Thanks!

  30. Sergey Chernyshov · · Reply

    Hi Damien,

    Many thanks for a brilliant and it looks like the only useful post on the Internet for this topic.

    However for some reason I cannot launch Angular2Client and IdentityServerWithAspNetIdentitySqlite projects from VS2015. The problem is that both do not launch IIS Express while starting in debug mode. And at the same time the ResourceServer project can be launched normally: it starts IIS Express and browser and works fine.

    Any ideas how to fix this please?

    Regards,
    Sergey

    1. Hi Sergey, thanks. Both projects should run. 2 possible problems, the IIS does not start using the configured HTTPS port, this is configured in the project settings and also the client code configuration. Also, maybe the webpack build needs to be started for the Angular 2 project.

      Greetings Damien

  31. Damien thanks for the good article. I am looking at a few things and I think you might also have things to say on this…

    1) using /.well-known/jwks to get the list of end points and configure the client.

    2) making a connection to the Routing system with the Guard hooks to make routing and a users roles work together.

    I am looking at your other blog posts right now as it does look like you may have already written something…

    1. Hi figuerres, thanks, I planning to look into the routing with guards etc.
      Greetings Damien

  32. […] am following the example provided by damienbod Ang2 Id Serv 4 OIDC, where I have the following : an identity Server (modified implementation), Resource API and ng-2 […]

  33. I’m not able to make the migration of databse. Do you have an idea to resolve this problem ?

    1. Which database, project. Delete the Migrations, change the EF core providers to your DB and add

      dotnet ef migrations add yourName

      dotnet ef update database

      The connectionString must match your db,

      Greetings Damien

  34. This is awesome, is there an easy way to add social logins to this example?

    1. Yes, exactly the same as the examples from IdentityServer4, shouldjust require a change in the startup and then the razor view

      Thanks and greetings Damien

  35. Sharath · · Reply

    Hi This is a great article, Can you please let me know if there is any sample with angular2 with out asp.net Identity (As we are planning to have our own identity/user management system with dapper implementation)

    1. Worth mentioning that this exists if you are following along with the Identity quickstart tutorial and want to use the client they reference, or if you are using Angular CLI (which this sample makes use of):
      https://github.com/jmurphzyo/Angular2OidcClient

  36. Hi,

    Thank you for these great articles. Running into issue when I am trying to run the sample with Angular2 Client. The page keeps showing “This is server routing, not angular2 routing”. It seems like the Angular app is not loading at all. Really appreciate your efforts.

    Thanks

    1. Hi Kay

      I’ll check this, will get back to you, the client routes should be executed first, then the server. If the requested route is not defined in the startup.cs, the server routing is used.

      I will remove the MVC routing completely for this project and use just static files.

      Greetings Damien

      1. Hi Kay
        I do’t have this problem, just checked. Can you send me more info?, maybe a github issue

        Greetings Damien

  37. Hi Damien,

    Thank you for following up on this. I am not sure either why this is not running on my machine. I am trying to run application directly from your repo at this point and still facing the same issue. Makes me think I am doing something wrong on how to run the application may be ?

    I have set IdentityServerWithAspNetIdentitySqlite & Anguar2client as startup projects and running the solution from VS 2015.

    The IdentityServer launches just fine but the Angular2Client at following URL
    https://localhost:44311/ shows “This is server routing, not angular2 routing” message.

    What am I missing here ?

    Thanks,
    Kay

    1. Hi Kay you might need to build the angular app
      $ npm install
      $ npm run build-production

      You also need to start the 2 resource servers

  38. Great, it’s working now. The only thing I was missing was npm run build-production. Thanks for all your help and these great samples.

    There another question I had regarding Identityserver, unrelated to this specific post. I was wondering if I could reach out to you separately over an email ?

    Thanks

    1. cool
      sent
      cheers Damien

  39. Great, thanks. Sent you an email with the details.

  40. jacob cheriathundam · · Reply

    Thank you so much for your great articles!

    I am trying to implement this (IdentityServer4 with .net core Identity and Angular 2 app) and am running into an issue with redirection back to the client once they login. My angular 2 app will go to the connect/authorize endpoint of identity server then redirect to the login page and have something like this:

    http://localhost:5000/account/login?returnUrl=%2Fconnect%2Fauthorize%2Flogin%3Fresponse_type%3Did_token%2520token%26client_id%3Dangular2%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A3000%252Fsecure%26scope%3Dapiprovider%2520profile%2520openid%26nonce%3DN0.70919710628708831486571833014%26state%3D14865718330140.6582151509561325

    The user enters their credentials and clicks on login, then the AccountController.Login functionality kicks off but the returnURL information is obviously not right because it contains the entire set of parameters sent up from the Angular 2 app (I copied what you had in the Angular2App SecurityService file to make the request so that makes sense that it would). If using identity server with identity, do I manually parse this query string to get the actual return URL or is there another method that I am missing in my pipeline that handles this automatically. I get nervous about the idea of parsing this manually, not because I cant do it, but because if it ever changes, it would break everything.

    1. It just hit me that I did not understand the flow of this process correctly. I think I had the order wrong. The user logs in via the AccountController.Login method, then is redirected to the connect/authorize endpoint defined in the query string that I pasted above. The user is granted a id token by the identity server and then sent to the consent path if that is required. My issue was that I didnt explicitly set Consent being false, so it was forwarding to consent and failing there. Once I changed the configuration of the angular2 client to not require consent, it redirected back to the angular 2 app correctly.

      So I think I answered my own question. Now to see if I can authorize against an resource endpoint and add a client that DOES require consent!

  41. terrychild · · Reply

    Hi Damien,

    Thanks for the article and github repo.

    I’m fairly new to OpenId Connect and I’ve been trying to understand the implicit flow by reading the specs and looking at your examples.

    One thing I have noticed is that the Angular example does not validate the id token, apart from checking the nonce.

    Looking at the specs, they say “Clients MUST validate the ID Token per Section 2.2.1.”.

    If the nonce from the id token is being used, then should the client validate the signature of the id token too?

    Thanks

    Terry

    1. Hi terry, I thought I validated, will fix this, thanks

  42. where is the OidcSecurityService?

  43. Thanks a lot
    Very Helpfull

  44. Jamie · · Reply

    Hey Damien.
    First of all, great posts! Thank you very much for all the useful knowledge you shared.
    Keep up the good work!

    I’m having problems when trying to access protected files from client (angular client).

    Actually problem is that API – ResourceFileServer (44378 port) can’t hit Identity server – IdentityServerWithIdentitySQLite (44318 port )

    I can access the discovery address from browser – https://localhost:44318/.well-known/openid-configuration, but it seems API can’t access it for some reason. Any ideas please?

    No Errors in logs for Identity Server app.

    Log from Web API:

    An unhandled exception was thrown by the application. (8c14976b)
    System.InvalidOperationException: Discovery endpoint https://localhost:44318/.well-known/openid-configuration is unavailable: System.Net.Http.HttpRequestException: An error occurred while sending the request. —> System.Net.Http.WinHttpException: A security error occurred
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
    at System.Net.Http.WinHttpHandler.d__105.MoveNext()
    — End of inner exception stack trace —

    1. Thanks
      Seems like the port is not configured. Try resetting this in the debug properties page, if you change this, you will have to re-configure the client etc. Greetings Damien

      1. Hey Damien,

        Thanks for your reply!!

        Just a notice, I can’t access any of the sites without confirming security exception about SSL.

        Could that be an issue?

        I did import my certificate into local machine -> trusted root certification authorities

        Thanks!

      2. Also, I tried changing port in debug settings, but that didn’t help so I’m out of ideas.

      3. Hey there.

        Even after moving IdentityServer to IIS site i still get the same error:

        https://www.angularcoreidentityserver.com/.well-known/openid-configuration is unavailable: System.Net.Http.HttpRequestException: An error occurred while sending the request. —> System.Net.Http.WinHttpException: A security error occurred

      4. Do you see anything more in the logs?

      5. Nothing else in logs,
        except: Even after moving IdentityServer to IIS site i still get the same error:

        https://www.angularcoreidentityserver.com/.well-known/openid-configuration is unavailable: System.Net.Http.HttpRequestException: An error occurred while sending the request. —> System.Net.Http.WinHttpException: A security error occurred

        Any ideas?

  45. possibly CORS issue as this is two different ports ?? site a on port 123 not connecting to site b on port 456 ??

  46. Not working. Error:
    An unhandled exception was thrown by the application. (8c14976b)
    System.InvalidOperationException: Discovery endpoint https://localhost:44318/.well-known/openid-configuration is unavailable: System.Net.Http.HttpRequestException:

    1. Is the IdentityServer4 running at this port and https?

      1. Yeah, same for me, and it’s running at the port and https.

  47. At Ani, Jamie

    I’m trying to reproduce this and I can’t. I created a new issue for this.

    https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow/issues/54

    Could you try the following

    1) Rebuild the whole solution
    2) Stop all scripts running in the task runner
    2) delete everything in .\src\AngularClient\node_modules
    3) using the commandline at .\src\AngularClient
    > npm install
    > npm run build-production
    4) start IdentityServerWithIdentitySQLite
    5) start ResourceServer
    6) start ResourceFileServer
    7) start AngularClient

    Greetings Damien

    1. Jamie · · Reply

      Can’t find ngc when i try to run build-production task. I think you should install cli as dev-dependency.

  48. Jamie · · Reply

    if I try to create a new API from scratch get data from .well-known URL – “https://localhost:44318/.well-known/openid-configuration”, I still get the error – kinda same error as ResourceFileServer API produces.

    However, If I use it like this (with ServerCertificateCustomValidationCallback returning true):

    using (var httpClientHandler = new HttpClientHandler())
    {

    httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
    {
    return true;
    };
    using (var client = new HttpClient(httpClientHandler))
    {
    try
    {
    var response = await client.GetAsync(“https://localhost:44318/.well-known/openid-configuration”);
    return response;
    }
    catch (HttpRequestException ex)
    {
    var c = ex;
    throw;
    }
    }
    }

    That works. Any ideas?

  49. Manjit singh · · Reply

    when i run the wepack task runner and run the Production mode its give me following errors but its working fine with Development:

    ERROR in ./angular2App/main-aot.ts
    Module not found: Error: Can’t resolve ‘../aot/angular2App/app/app.module.ngfactory’ in ‘D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\angular2App’
    @ ./angular2App/main-aot.ts 5:0-81

    ERROR in [default] D:\Sprints\Partner View\Sprint-3.0.7\PV\dev-latest\partnerview-v3\PartnerView Angular2\PartnerView.UI\src\Angular2Client\angular2App\main-aot.ts:7:35
    Cannot find module ‘../aot/angular2App/app/app.module.ngfactory’.

    1. seems like the path is incorrect. Check the aot folder and see if it matches the paths defined in the webpack.prod file and the tsconfig-aot.json file.

      1. Manjit singh · ·

        there is a line ‘import { AppModuleNgFactory } from ‘../aot/angular2App/app/app.module.ngfactory’;’ in ‘main-aot.ts’ file but in my solution there is not aot folder exist and its giving the error due to this aot folder. Like you can see we have following hierarchy “src/AngularClient/angularApp/app/” so how this aot folder will create.
        Kindly help me i am stuck on this its working fine with dev but when i use Run- Production from webpack task runner its giving the upper error

      2. If the path is incorrect, you need to change this in the webpack.prod.js file and also in the tsconfig-aot file
        Greetings Damien

Leave a 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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: