Angular2 OpenID Connect Implicit Flow with IdentityServer4

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

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

2016.12.04: Updated to IdentityServer4 rc4
2016.11.18: Updated to Angular 2.2.0, IdentityServer4 rc3
2016.11.03: Updated to Angular 2.1.1, Webpack 2, AoT, treeshaking
2016.10.08: Updated to IdentityServer4 rc2, Angular 2.0.2
2016.09.18: Updated to Angular 2 release, ASP.NET Core 1.0.1
2016.08.11: Updated to Angular 2 rc.6
2016.08.11: Updated to IdentityServer4 1.0.0-beta5 and Angular 2 rc.5
2016.07.03: Updated to ASP.NET Core RTM and Angular 2 rc4
2016.06.26: Updated Angular 2 to rc.3, new Angular 2 routing, IdentityServer4 beta 3, connect/endsession implemented
2016.06.21: Updated to Angular 2 rc.2
2016.05.31: Updated Angular 2 app to use webpack
2016.05.07: Updated to Angular 2 rc.1
2016.05.22: Updated to ASP.NET Core RC2 dotnet
2016.04.08: Update Angular 2 and added Visual Studio 2015 breakpoint debugging

Other posts in this series:

IdentityServer4 Configuration

The client configuration in IdentityServer4 is set up to use the enum Flow.Implicit and the required Angular2 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"
	}
}

Angular2 client using ASP.NET Core

Angular2 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 Angular2 guide on the Angular 2 web page. This changes regularly.

{
  "version": "1.0.0",
  "description": "",
  "main": "wwwroot/index.html",
  "author": "",
  "license": "ISC",
    "scripts": {
        "ngc": "ngc -p ./tsconfig-aot.json",
        "start": "concurrently \"webpack-dev-server --inline --progress --port 8080\" \"dotnet run\" ",
        "webpack-dev": "set NODE_ENV=development&& webpack",
        "webpack-prod": "set NODE_ENV=production&& webpack",
        "build": "npm run webpack-dev",
        "buildProduction": "npm run ngc && npm run webpack-prod"
    },
    "dependencies": {
        "@angular/common": "~2.2.0",
        "@angular/compiler": "~2.2.0",
        "@angular/core": "~2.2.0",
        "@angular/forms": "~2.2.0",
        "@angular/http": "~2.2.0",
        "@angular/platform-browser": "~2.2.0",
        "@angular/platform-browser-dynamic": "~2.2.0",
        "@angular/router": "~3.2.0",
        "@angular/upgrade": "~2.2.0",
        "angular-in-memory-web-api": "~0.1.15",
        "core-js": "^2.4.1",
        "reflect-metadata": "^0.1.8",
        "rxjs": "5.0.0-beta.12",
        "zone.js": "^0.6.25",

        "@angular/compiler-cli": "~2.2.0",
        "@angular/platform-server": "~2.2.0",
        "bootstrap": "^3.3.7",
        "ie-shim": "^0.1.0"
    },
  "devDependencies": {
    "@types/node": "^6.0.45",
    "awesome-typescript-loader": "^2.2.4",
    "angular2-template-loader": "^0.5.0",
    "source-map-loader": "^0.1.5",
    "clean-webpack-plugin": "^0.1.9",
    "copy-webpack-plugin": "^2.1.3",
    "html-webpack-plugin": "^2.8.1",
    "css-loader": "^0.23.0",
    "file-loader": "^0.8.4",
    "jquery": "^2.2.0",
    "json-loader": "^0.5.3",
    "node-sass": "^3.10.1",
    "sass-loader": "^4.0.2",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.2",
    "style-loader": "^0.13.0",
    "ts-helpers": "^1.1.1",
    "typescript": "2.0.3",
    "url-loader": "^0.5.6",
    "webpack": "2.1.0-beta.27",
    "webpack-dev-server": "^1.16.2"
  }
}

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": [
    "angular2App/app/app.module.ts",
    "angular2App/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 Angular2 application is then initialized in the index.html file in the angular2App folder. The required dependencies for Angular2 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 2 IdentityServer4 Client</title>

    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

</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 angular2 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!");
	});
}

Angular2 Authorize

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

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

platformBrowserDynamic().bootstrapModule(AppModule);

The NgModule imports the modules and declares the components.

import { NgModule } from '@angular/core';
import { CommonModule }   from '@angular/common';
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 { SecurityService } from './services/SecurityService';
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';

@NgModule({
    imports: [
        BrowserModule,
        CommonModule,
        FormsModule,
        routing,
        HttpModule,
        JsonpModule
    ],
    declarations: [
        AppComponent,
        ForbiddenComponent,
        HomeComponent,
        UnauthorizedComponent,
        SecureFilesComponent,
        DataEventRecordsListComponent,
        DataEventRecordsCreateComponent,
        DataEventRecordsEditComponent
    ],
    providers: [
        SecurityService,
        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 securityService authorize process. the (click) definition is used to define the click event in Angular2.

<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 { SecurityService } from './services/SecurityService';
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: SecurityService) {  
    }

    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 2 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 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");

	var authorizationUrl = 'https://localhost:44318/connect/authorize';
	var client_id = 'angular2client';
	var redirect_uri = 'https://localhost:44311';
	var response_type = "id_token token";
	var scope = "dataEventRecords securedFiles openid";
	var nonce = "N" + Math.random() + "" + Date.now();
	var state = Date.now() + "" + Math.random();

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

	var 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();

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

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

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

	var token = "";
	var id_token = "";
	var authResponseIsValid = false;
	if (!result.error) {

		if (result.state !== this.retrieve("authStateControl")) {
			console.log("AuthorizedCallback incorrect state");
		} else {

			token = result.access_token;
			id_token = result.id_token

			var dataIdToken: any = this.getDataFromToken(id_token);
			console.log(dataIdToken);

			// validate nonce
			if (dataIdToken.nonce !== this.retrieve("authNonce")) {
				console.log("AuthorizedCallback incorrect nonce");
			} else {
				this.store("authNonce", "");
				this.store("authStateControl", "");

				authResponseIsValid = true;
				console.log("AuthorizedCallback state and nonce validated, returning access token");
			}
		}
	}

	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']);
	}
}

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", "");
	}

	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 (var i = 0; i < this.UserData.role.length; i++) {
				if (this.UserData.role[i] === "dataEventRecords.admin") {
					this.HasAdminRole = true;
					this.store("HasAdminRole", 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 { SecurityService } from '../services/SecurityService';
import { DataEventRecord } from './models/DataEventRecord';

@Injectable()
export class DataEventRecordsService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: SecurityService) {
        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 { SecurityService } from '../services/SecurityService';
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: SecurityService,
        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

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

68 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 […]

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: