Angular OpenID Connect Implicit Flow with IdentityServer4

This article shows how to implement the OpenID Connect Implicit Flow using Angular. This previous blog implemented the OAuth2 Implicit Flow which is not an authentication protocol. 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.07.03: Updated to ASP.NET Core RTM
2016.05.22: Updated to ASP.NET Core RC2 dotnet

Other posts in this series:

In the application, the SecurityService implements all the authorization, local storage, login, authorize, logoff and reset. The AuthorizationInterceptor is used to intercept the http requests, responses.

Client Authentication, User Authorization

The client authentication and user authorization is started by clicking the Login button. This calls indirectly the DoAuthorization function in the SecurityService.

openid_implicitFlow_01

The DoAuthorization is used to request the authorization and also to handle authorization callback logic. If a windows hash exists, the application knows that the login is completed and returning the token and the id_token from the login, authentication, authorization process.

var DoAuthorization = function () {
  ResetAuthorizationData();

  if ($window.location.hash) {
    authorizeCallback();
  }
  else {
    authorize();
  }
}

The authorize function is used to create the request for the OpenID login and the redirect. The /connect/authorize on IdentityServer4 is called with the parameters described in the OpenID Connect Implicit Flow specification. The scope MUST contain the openid scope, otherwise the request will fail. The response_type defines the flow which should be used. The OpenID Connect Implicit Flow requires the id_token token or the id_token definition. The supported OpenID flows are also defined in the specification.

“response_type” OpenID connect definitions:

code : Authorization Code Flow
id_token : Implicit Flow
id_token token : Implicit Flow
code id_token : Hybrid Flow
code token : Hybrid Flow
code id_token token : Hybrid Flow

All other “response_type” definitions which are supported by IdentityServer4 are not OpenID connect flows.

The nonce and state parameters for the auth request are created and saved to the local storage. The nonce and the state are used to validate the response to prevent against Cross-Site Request Forgery (CSRF, XSRF) attacks.

The redirect_uri parameter must match the definition in the IdentityServer4 project. The client_id must also match the client definition in IdentityServer4. The matching client configuration for this application can be found here.

var authorize = function () {
	console.log("AuthorizedController time to log on");

	var authorizationUrl = 'https://localhost:44345/connect/authorize';
	var client_id = 'angularclient';
	var redirect_uri = 'https://localhost:44347/authorized';
	var response_type = "id_token token";
	var scope = "dataEventRecords aReallyCoolScope openid";
	var nonce = "N" + Math.random() + "" + Date.now();
	var state = Date.now() + "" + Math.random();

	localStorageService.set("authNonce", nonce);
	localStorageService.set("authStateControl", state);
	console.log("AuthorizedController created. adding myautostate: " + localStorageService.get("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 = url;
}

The login page:

openid_implicitFlow_02

Information about what the client is requesting:

openid_implicitFlow_03

Authorization Callback Validation

The authorizeCallback function is used to validate and process the token/id_token response. The method takes the returned hash and then validates that the nonce and also the state are the same values which were sent to IdentityServer4. The token and also the id_token are extracted from the result. The getDataFromToken function is used to get the id_token values of the response. The nonce value can then be read and validated.

var authorizeCallback = function () {
	console.log("AuthorizedController created, has hash");
	var hash = window.location.hash.substr(1);

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

	var token = "";
	var id_token = "";
	var authResponseIsValid = false;
	if (!result.error) {
		
			if (result.state !== localStorageService.get("authStateControl")) {
				console.log("AuthorizedCallback incorrect state");
			} else {

				token = result.access_token;
				id_token = result.id_token

				var dataIdToken = getDataFromToken(id_token);
				console.log(dataIdToken);

				// validate nonce
				if (dataIdToken.nonce !== localStorageService.get("authNonce")) {
					console.log("AuthorizedCallback incorrect nonce");
				} else {
					localStorageService.set("authNonce", "");
					localStorageService.set("authStateControl", "");

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

	if (authResponseIsValid) {
		SetAuthorizationData(token, id_token);
		console.log(localStorageService.get("authorizationData"));

		$state.go("overviewindex");
	}
	else {
		ResetAuthorizationData();
		$state.go("unauthorized");
	}
}

If the tokens are ok, the SetAuthorizationData method is used to save the token payload to the local storage. This method saves both the token and the id_token to the storage. The method also uses the token, to check if the logged on user has the admin role claim. The HasAdminRole property and the IsAuthorized property is set on the root scope as this is a required throughout the angular application.

var SetAuthorizationData = function (token, id_token) {
	
	if (localStorageService.get("authorizationData") !== "") {
		localStorageService.set("authorizationData", "");
	}

	localStorageService.set("authorizationData", token);
	localStorageService.set("authorizationDataIdToken", id_token);
	$rootScope.IsAuthorized = true;

// This requires an extra round trip with IdentityServer rc4 to get the user claims, role
	var data = getDataFromToken(token);
	for (var i = 0; i < data.role.length; i++) {
		if (data.role[i] === "dataEventRecords.admin") {
			$rootScope.HasAdminRole = true;                    
		}
	}
}

Using the Bearer Token

Now that the angular app has a token, an Authorization Interceptor is used to intecept all http requests and add the Bearer token to the header. If a 401 is returned, the application alerts with a unauthorized and resets the local storage. If a 403 is returned, the application redirects to the forbidden angular route.

(function () {
    'use strict';

    var module = angular.module('mainApp');

    function AuthorizationInterceptor($q, localStorageService) {

        console.log("AuthorizationInterceptor created");

        var request = function (requestSuccess) {
            requestSuccess.headers = requestSuccess.headers || {};

            if (localStorageService.get("authorizationData") !== "") {
                requestSuccess.headers.Authorization = 'Bearer ' + localStorageService.get("authorizationData");
            }

            return requestSuccess || $q.when(requestSuccess);
        };

        var responseError = function(responseFailure) {

            console.log("console.log(responseFailure);");
            console.log(responseFailure);
            if (responseFailure.status === 403) {
                alert("forbidden");
                window.location = "https://localhost:44347/forbidden";
                window.href = "forbidden";

            } else if (responseFailure.status === 401) {

                alert("unauthorized");
                localStorageService.set("authorizationData", "");
            }

            return this.q.reject(responseFailure);
        };

        return {
            request: request,
            responseError: responseError
        }
    }

    module.service("authorizationInterceptor", [
            '$q',
            'localStorageService',
            AuthorizationInterceptor
    ]);

    module.config(["$httpProvider", function ($httpProvider) {
        $httpProvider.interceptors.push("authorizationInterceptor");
    }]);

})();

The application also only displays the data to an authorized user using the IsAuthorized angular property. A ng-if is used with the HasAdminRole property. This is read only without the admin role. If a script kiddy plays with the HTML, this does not matter as the role is validated on the server, this is just user validation, or user experience.

<div class="col-md-12"  ng-show="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;" ng-repeat="dataEventRecord in dataEventRecords">
                        <td>
                            <a ng-if="HasAdminRole" href="/details/{{dataEventRecord.Id}}">{{dataEventRecord.Name}}</a>
                            <span ng-if="!HasAdminRole">{{dataEventRecord.Name}}</span>
                        </td>
                        <td>{{dataEventRecord.Timestamp}}</td>
                        <td><button ng-click="Delete(dataEventRecord.Id)">Delete</button></td>
                    </tr>
                </tbody>
            </table>

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

openid_implicitFlow_04

Logoff Client application, and IdentityServer4

IdentityServer4 (will) supports server logoff. This is done using the /connect/endsession endpoint in IdentityServer4. At present, this is not supported in IdentityServer4 but will be some time after the ASP.NET Core RC2 release. When implemented, the server method will be called using the specification from openID and the local storage will be reset.

var Logoff = function () {
	var id_token = localStorageService.get("authorizationDataIdToken");     
	var authorizationUrl = 'https://localhost:44345/connect/endsession';
	var id_token_hint = id_token;
	var post_logout_redirect_uri = 'https://localhost:44347/unauthorized.html';
	var state = Date.now() + "" + Math.random();

	var url =
		authorizationUrl + "?" +
		"id_token_hint=" + id_token_hint + "&" +
		"post_logout_redirect_uri=" + encodeURI(post_logout_redirect_uri) + "&" +
		"state=" + encodeURI(state);

	ResetAuthorizationData();
	$window.location = url;
}

Links:

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

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

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

10 comments

  1. Hi, I like you post very much. It is very helpful for me in code organization but one thing bothers me a little. Why you are using localStorage to store access token? Almost every security course states that storing sensitive data in localStorage is antipattern because it is not removed after session ends.

    1. Hi Bartek.

      Thanks, yes saving the token in the local storage has its security risks but it’s that or cookies or log in everytime you change the page. No other options exists. It can be removed with a logout, or the lifetime of the token can be limited.

      Do you have any other recommendations, or ways of improving this?

      Thanks and greetings Damien

  2. The problem of storing sensitive data in browser is old as Internet. There is not much options: localstorage, sessionstorage and cookie. The cookie is most secure but only if you set the httponly attribute which unfortunatelly cannot be done in AngularJS app. I would use session storage because it can mitigate some developers bugs if he/she forget to remove data from localstorage.

    One thing I would suggest is to use reference token instead of self-contained token but only if you have control over API you are calling from the app. In this case API can validate the token in Authorization Server and you do not expose directly data from token. Check this link from Dominic Baier’s blog: https://leastprivilege.com/2015/11/25/reference-tokens-and-introspection/

    Greetings Bartek

    1. Thanks Bartek, I will look into this further.
      Greetings Damien

  3. Aakash Jain · · Reply

    Hi,
    You have mentioned that “nonce and the state are used to validate the response to prevent against Cross-Site Request Forgery (CSRF, XSRF) attacks.” Could you please tell more about this or point to a resource on internet.

    thanks.

  4. Roman Molchanov · · Reply

    Hi Damien,

    The example of AngularClient doesn’t work for me.
    When I press the button “Yes, Allow” at the screen “angularclient is requesting your permission” I have an error:
    GET https://localhost:44347/authorized 404 (not Found) :44347/authorized#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjA2RDNFNDZFOTEwNzNDNUQ0QkMyQzk5ODNCRTlGRjQ0OENGNjQwRDQiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJCdFBrYnBFSFBGMUx3c21ZTy1uX1JJejJRTlEifQ.eyJuYmYiOjE0NjkxMzk4MzcsImV4cCI6MTQ2OTE0MDEzNywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNDUiLCJhdWQiOiJhbmd1bGFyY2xpZW50Iiwibm9uY2UiOiJOMC45OTIxNDI5Mjk4ODQzOTcxMTQ2OTEzOTgxMjAyMCIsImlhdCI6MTQ2OTEzOTgzNywiYXRfaGFzaCI6IjlxODRDQU9DUy1BTzc3NnNDRjY3UXciLCJzaWQiOiJhYjcwNjViNzk3MDRmYjI5ZTU0MzViNGM1OTI4M2ZkMSIsInN1YiI6IjQ4NDIxMTU3IiwiYXV0aF90aW1lIjoxNDY5MTM0MDg2LCJpZHAiOiJpZHN2ciJ9.ZZR_Z7OEyMLG8bkM5EJtbMgFREGVekn2Wtt-dD9CxQcgI224MS5LDn-lmncswNibhch5_-wRGFNsXVq1gFOycyZPNfNywPE2KQG1288ETthRIurqfzMKUeCZtcMlIQxqdbItrt0_Fce_EHFPA-i2qU9-lxJkFwMcNIbE3vo1t5jfUvZgvYdCKK3R9JTtfB36Pu3yqmtP7_weR0fgPVG9kv1il5X7d917Ii0p4DlwSb4InLmGPUhm7DwK19nVpkuF82dVVbIw0Wa_v9be37AbqzqP50mqI3zm11OEIDVLkvZ_mN0HSOj1iPJd67oQg4jF2tM8viNIUi9ZoFpkjk9WQA&access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjA2RDNFNDZFOTEwNzNDNUQ0QkMyQzk5ODNCRTlGRjQ0OENGNjQwRDQiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJCdFBrYnBFSFBGMUx3c21ZTy1uX1JJejJRTlEifQ.eyJuYmYiOjE0NjkxMzk4MzYsImV4cCI6MTQ2OTE0MzQzNiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNDUiLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo0NDM0NS9yZXNvdXJjZXMiLCJjbGllbnRfaWQiOiJhbmd1bGFyY2xpZW50Iiwic2NvcGUiOlsiZGF0YUV2ZW50UmVjb3JkcyIsImFSZWFsbHlDb29sU2NvcGUiLCJzZWN1cmVkRmlsZXMiLCJvcGVuaWQiXSwic3ViIjoiNDg0MjExNTciLCJhdXRoX3RpbWUiOjE0NjkxMzQwODYsImlkcCI6Imlkc3ZyIiwicm9sZSI6WyJhZG1pbiIsImRhdGFFdmVudFJlY29yZHMuYWRtaW4iLCJkYXRhRXZlbnRSZWNvcmRzLnVzZXIiLCJkYXRhRXZlbnRSZWNvcmRzIiwic2VjdXJlZEZpbGVzLnVzZXIiLCJzZWN1cmVkRmlsZXMuYWRtaW4iLCJzZWN1cmVkRmlsZXMiXX0.BLBOH2073mIIyf6x18dmX2XA_SXfDgcwlp4wgG49qsHwa5WTFDQ6no5GFyc3zjLiLl6r4KSGcbuFJo7xs5ZjDpS7dFHMrR3MJZlhaLaKAWE8pJG2c8qP-qnB_rvv8pozjHeA669Hlv00DKCR0gwGW1ofTabvND2IW__c0imeSIVjnGss_uIki8Bw-sJbg1-lI69Q8D_jDwmhAMQ5xpVe_qTRbGQ8IqUmneUo32vaevfGlP2ZoliDCwVBX6zrjYRxuYPXGMlDLjdPkAxXf4Y2hVVFG0WX8Q7kvcN-yHmwD34LcFfBaHy__yuO9ruqJlumfXRJC6rCZNwtNrvdXIin5Q&token_type=Bearer&expires_in=3600&scope=dataEventRecords%20aReallyCoolScope%20securedFiles%20openid&state=14691398120200.29998245613025465&session_state=Z0-tJB9tY_8iRKn71hvKcWqc65qBofanBeyzKRFmowg.f29d233f97b69156b213b236de2cd1f2

    Could you help me what is wrong?

    Thanks,
    Roman.

    1. Hi Roman

      This is now fixed, I removed the server routing for the client URL, thanks for the feedback.

      Greetings Damien

  5. Roman Molchanov · · Reply

    Hi Damien,

    The example of AngularClient doesn’t work for me.
    When I press the button “Yes, Allow” at the screen “angularclient is requesting your permission” I have an error:
    GET https://localhost:44347/authorized 404 (not Found) :44347/authorized#id_token=eyJhbGciOiJSUzI1NiIsImtp…

    Could you help me what is wrong?

    Thanks,
    Roman.

    1. Hi Roman

      I’ll check this, I updated the server but never checked the angular client, only the angular2 client. The authorized route seems to be handled by the server and not the client.

      I’ll create an issue on gitHub for this, and fix it when I get time.

      thanks 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: