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: VS2017 msbuild | VS2015 project.json
History:
2017.03.18: Updated to angular 2.4.10, oidc client validation
Full history:
https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow#history
Other posts in this series:
- OAuth2 Implicit Flow with Angular and ASP.NET Core 1.1 IdentityServer4
- Authorization Policies and Data Protection with IdentityServer4 in ASP.NET Core
- AngularJS OpenID Connect Implicit Flow with IdentityServer4
- Angular OpenID Connect Implicit Flow with IdentityServer4
- Secure file download using IdentityServer4, Angular2 and ASP.NET Core
- Angular2 Secure File Download without using an access token in URL or cookies
- Full Server logout with IdentityServer4 and OpenID Connect Implicit Flow
- IdentityServer4, WebAPI and Angular in a single ASP.NET Core project
- Extending Identity in IdentityServer4 to manage users in ASP.NET Core
- Implementing a silent token renew in Angular for the OpenID Connect Implicit flow
- OpenID Connect Session Management using an Angular application and IdentityServer4
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.
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:
Information about what the client is requesting:
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>
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
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.
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
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
Thanks Bartek, I will look into this further.
Greetings Damien
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.
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.
Hi Roman
This is now fixed, I removed the server routing for the client URL, thanks for the feedback.
Greetings Damien
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.
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
Issue https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow/issues/23
Hi Damien. Thanks for all you work.
can i please ask, what advanatges did you find manually coding up ur auth service vs using say oidc-client.js from the IdSvr guys? Did you have a play with the oidc-client?
And… did you figure out a way with ur authservice to handle silentrefresh tokens?
Just curious as to why you decided to go custom?
It’s all in Angular and you can change it as you like, and do the AoT stuff and so on. You can also change to logout as required. I wanted to use just angular. silent refresh is still missing plus some client validation checks. oidc-client is certified, and works well in the browser. I believe Typescript exists for this as well.
Thanks for your reply Damien, appreciate it.
Cannot find templates folder (used in app.js like templateUrl: “/templates/authorized.html”)
Where to find it
Hi and thank you
I have php application and want to use serveridentity4 in php
can you please let me know from where i should start?
Hi Sina do you want to use php for the server or the client ?
Hi and tnx for your response
I Need to get latest news from fiba website,
They provide an swagger api which is using IdentityServer4 for authentication,
Api :
https://bbm-api.fiba.com/Swagger
My website created with php
Here’s an example with a PHP STS server
https://github.com/robisim74/angular-openid-connect-php