Quantcast
Channel: ASP.NET Core – Software Engineering
Viewing all articles
Browse latest Browse all 269

OAuth2 Implicit Flow with Angular and ASP.NET Core 1.0 IdentityServer

$
0
0

This article shows how to implement the OAuth2 Implicit Flow with an Angular client and IdentityServer4 hosted in ASP.NET Core 1.0. The code was built using the IdentityServer4.Samples. Thanks to everyone who helped in creating IdentityServer.

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

18.11.2015: Updated to ASP.NET Core 1.0 rc1
15.01.2016: Updated to IdentityServer4

Setting up the resource server

The resource server is a simple Web API service implemented in MVC 6 in ASP.NET 5. A simple controller is used to implement CRUD methods for a SQLite database using Entity Framework 7.
The resource server in the code example is hosted at the URL: https://localhost:44345/

The Startup class configures the security middlerware. CORS is activated because the client application needs to access the resource. The security middleware is configured using the UseJwtBearerAuthentication method and also the RequiredScopesMiddleware implementation taken from the IdentityServer.samples. The UseJwtBearerAuthentication options defines where IdentityServer can be found to authorize HTTP requests.

using AspNet5SQLite.Model;
using AspNet5SQLite.Repositories;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Dnx.Runtime;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.PlatformAbstractions;

namespace AspNet5SQLite
{
    public class Startup
    {
        public IConfigurationRoot Configuration { get; set; }

        public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                .AddJsonFile("config.json");
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            var connection = Configuration["Production:SqliteConnectionString"];

            services.AddEntityFramework()
                .AddSqlite()
                .AddDbContext<DataEventRecordContext>(options => options.UseSqlite(connection));

            //Add Cors support to the service
            services.AddCors();

            var policy = new Microsoft.AspNet.Cors.Infrastructure.CorsPolicy();

            policy.Headers.Add("*");
            policy.Methods.Add("*");
            policy.Origins.Add("*");
            policy.SupportsCredentials = true;

            services.AddCors(x => x.AddPolicy("corsGlobalPolicy", policy));

            services.AddMvc();
            services.AddScoped<IDataEventRecordRepository, DataEventRecordRepository>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.MinimumLevel = LogLevel.Information;
            loggerFactory.AddConsole();
            loggerFactory.AddDebug();

            app.UseIISPlatformHandler();

            app.UseExceptionHandler("/Home/Error");

            app.UseCors("corsGlobalPolicy");

            app.UseStaticFiles();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();

            app.UseJwtBearerAuthentication(options =>
            {
                options.Authority = "https://localhost:44345";
                options.Audience = "https://localhost:44345/resources";
                options.AutomaticAuthenticate = true;
            });

            app.UseMiddleware<RequiredScopesMiddleware>(new List<string> { "dataEventRecords" });
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

        // Entry point for the application.
        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

RequiredScopesMiddleware is used to validate the scopes for each user. This was taken from the IdentityServer.samples project.

namespace AspNet5SQLite
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;

    using Microsoft.AspNet.Builder;
    using Microsoft.AspNet.Http;

    public class RequiredScopesMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IEnumerable<string> _requiredScopes;

        public RequiredScopesMiddleware(RequestDelegate next, List<string> requiredScopes)
        {
            _next = next;
            _requiredScopes = requiredScopes;
        }

        public async Task Invoke(HttpContext context)
        {
            if (context.User.Identity.IsAuthenticated)
            {
                if (!ScopePresent(context.User))
                {
                    context.Response.OnCompleted(Send403, context);
                    return;
                }
            }

            await _next(context);
        }
                
        private bool ScopePresent(ClaimsPrincipal principal)
        {
            foreach (var scope in principal.FindAll("scope"))
            {
                if (_requiredScopes.Contains(scope.Value))
                {
                    return true;
                }
            }

            return false;
        }

        private Task Send403(object contextObject)
        {
            var context = contextObject as HttpContext;
            context.Response.StatusCode = 403;

            return Task.FromResult(0);
        }
    }
}

The Controller class just requires the Authorize attribute to use the security middleware.


[Authorize]
[Route("api/[controller]")]
public class DataEventRecordsController : Controller
{
   // api implementation
}

Configuring the IdentityServer

IdentityServer is hosted in ASP.NET 5. This example is really just the basic configuration as in the example. The configuration has some important details when configuring the client, which must match the configuration in the resource server, and also the angular client. The IdentityServer in the code example is hosted at the URL: https://localhost:44345

The Startup class configures the server. This just adds the middleware and the SigningCertificate for HTTPS and the server is ready. Really simple for such powerful software.

using Microsoft.AspNet.Builder;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.AspNet.Hosting;

namespace IdentityServerAspNet5
{
    using System.IO;

    using IdentityServer4.Core.Configuration;

    using IdentityServerAspNet5.Configuration;
    using IdentityServerAspNet5.UI;
    using IdentityServerAspNet5.UI.Login;

    using Microsoft.Extensions.Logging;

    public class Startup
    {
        private readonly IApplicationEnvironment _environment;

        public Startup(IApplicationEnvironment environment)
        {
            _environment = environment;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            var cert = new X509Certificate2(Path.Combine(_environment.ApplicationBasePath, "damienbodserver.pfx"), "");

            var builder = services.AddIdentityServer(options =>
            {
                options.SigningCertificate = cert;
            });

            builder.AddInMemoryClients(Clients.Get());
            builder.AddInMemoryScopes(Scopes.Get());
            builder.AddInMemoryUsers(Users.Get());

            // for the UI
            services
                .AddMvc()
                .AddRazorOptions(razor =>
                {
                    razor.ViewLocationExpanders.Add(new CustomViewLocationExpander());
                });
            services.AddTransient<LoginService>();
        }

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(LogLevel.Verbose);
            loggerFactory.AddDebug(LogLevel.Verbose);

            app.UseDeveloperExceptionPage();
            app.UseIISPlatformHandler();

            app.UseIdentityServer();

            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
       
        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

The Users class is used to define the Users which can access the resource. This is a demo implementation and just defines the user, password and some default claims.

namespace IdentityServerAspNet5.Configuration
{
    using System.Collections.Generic;
    using System.Security.Claims;

    using IdentityServer4.Core;
    using IdentityServer4.Core.Services.InMemory;

    static class Users
    {
        public static List<InMemoryUser> Get()
        {
            var users = new List<InMemoryUser>
            {
                new InMemoryUser{Subject = "48421156", Username = "damienbod", Password = "damienbod",
                    Claims = new Claim[]
                    {
                        new Claim(Constants.ClaimTypes.Name, "damienbod"),
                        new Claim(Constants.ClaimTypes.GivenName, "damienbod"),
                        new Claim(Constants.ClaimTypes.Email, "damien_bod@hotmail.com"),
                        new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(Constants.ClaimTypes.Role, "Developer")
                    }
                }
            };

            return users;
        }
    }
}

The following class is used to define the scopes. This is important and MUST match the scope defined in the resource server startup class implementation. This software uses the dataEventRecords scope for the resource server. Some standard scopes are also added, but are not used in the client.

namespace IdentityServerAspNet5.Configuration
{
    using System.Collections.Generic;

    using IdentityServer4.Core.Models;

    public class Scopes
    {
        public static IEnumerable<Scope> Get()
        {
            return new[]
            {
                // standard OpenID Connect scopes
                StandardScopes.OpenId,
                StandardScopes.ProfileAlwaysInclude,
                StandardScopes.EmailAlwaysInclude,

                // API - access token will 
                // contain roles of user
                new Scope
                {
                    Name = "dataEventRecords",
                    DisplayName = "Data Event Records Scope",
                    Type = ScopeType.Resource,

                    Claims = new List<ScopeClaim>
                    {
                        new ScopeClaim("role")
                    }
                }
            };
        }
    }
}

The clients are defined in the following class. This must match the angular client implementation. The test server implements two test clients which activate the Implicit Flow. The RedirectUris are important and must match the client request EXACTLY, otherwise it will not work. The AllowedScopes also contain the dataEventRecords scope used for this application. The second client is the demo client from IdentityServer.samples.

namespace IdentityServerAspNet5.Configuration
{
    using System.Collections.Generic;

    using IdentityServer4.Core.Models;

    public class Clients
    {
        public static List<Client> Get()
        {
            return new List<Client>
            {new Client
                {
                    ClientName = "angularclient",
                    ClientId = "angularclient",
                    Flow = Flows.Implicit,
                    RedirectUris = new List<string>
                    {
                        "https://localhost:44347/identitytestclient.html",
                        "https://localhost:44347/authorized"

                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://localhost:44347/identitytestclient.html",
                        "https://localhost:44347/authorized"
                    },
                    AllowedScopes = new List<string>
                    {
                        "openid",
                        "email",
                        "profile",
                        "dataEventRecords"
                    }
                },
                new Client
                {
                    ClientName = "MVC6 Demo Client from Identity",
                    ClientId = "mvc6",
                    Flow = Flows.Implicit,
                    RedirectUris = new List<string>
                    {
                        "http://localhost:2221/",
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "http://localhost:2221/",
                    },
                    AllowedScopes = new List<string>
                    {
                        "openid",
                        "email",
                        "profile",
                        "dataEventRecords"
                    }
                }
            };
        }
    }
}

Implementing the Angular client

The angular client checks if it has a Bearer token to access the resource. If it doesn’t, it redirects to the IdentityServer where the user can logon. If successfully, it is redirected back to client, where it can then access the data in the resource server application. The Angular client in the code example is hosted at the URL: https://localhost:44347.

An AuthorizationInterceptor is used to intercept all http requests to the server and adds a Bearer token to the request, if its stored in the local storage. The angular-local-storage module is used to persist the token. The responseError is used to reset the local storage, if a 401 or a 403 is returned. This could be done better…

(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) {
                localStorageService.set("authorizationData", "");

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

                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 AuthorizedController is used to redirect to the logon, and persist the token to the local storage. The redirect_uri parameter sent in the request token must match the client configuration on the server. The response_type must be set to token as we are using a javascript client. When the token is received in the hash from the IdentityServer, this is then saved to the local storage.

(function () {
	'use strict';

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

	// this code can be used with uglify
	module.controller("AuthorizedController",
		[
			"$scope",
			"$log",
            "$window",
            "$state",
            "localStorageService",
			AuthorizedController
		]
	);

	function AuthorizedController($scope, $log, $window, $state, localStorageService) {
	    $log.info("AuthorizedController called");
		$scope.message = "AuthorizedController created";
	
        // TO force check always
	    localStorageService.set("authorizationData", "");
	    //localStorageService.get("authorizationData");
	    //localStorageService.set("authStateControl", "");
	    //localStorageService.get("authStateControl");

	    console.log(localStorageService.get("authorizationData"));

	    if (localStorageService.get("authorizationData") !== "") {
		    $scope.message = "AuthorizedController created logged on";
		   // console.log(authorizationData);
		    $state.go("overviewindex");
		} else {
		    console.log("AuthorizedController created, no auth data");
		    if ($window.location.hash) {
		        console.log("AuthorizedController created, has hash");
		        $scope.message = "AuthorizedController created with a code";

                    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 = "";
		            if (!result.error) {
		                if (result.state !== localStorageService.get("authStateControl")) {
		                    console.log("AuthorizedController created. no myautostate");                    
		                } else {
		                    localStorageService.set("authStateControl", "");
		                    console.log("AuthorizedController created. returning access token");
		                    token = result.access_token;
		                }
		            }

		            localStorageService.set("authorizationData", token);
		            console.log(localStorageService.get("authorizationData"));

		            $state.go("overviewindex");

		        } else {
		            $scope.message = "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 = "token";
		            var scope = "dataEventRecords";
		            var state = Date.now() + "" + Math.random();

		            localStorageService.set("authStateControl", state);
		            console.log("AuthorizedController created. adding myautostate: " + localStorageService.get("authStateControl"));
		          
		            var url =
                        authorizationUrl + "?" +
                        "client_id=" + encodeURI(client_id) + "&" +
                        "redirect_uri=" + encodeURI(redirect_uri) + "&" +
                        "response_type=" + encodeURI(response_type) + "&" +
                        "scope=" + encodeURI(scope) + "&" +
                        "state=" + encodeURI(state);
		            $window.location = url;
		        }
		}
	}
})();

Now the application can be used. The Visual Studio project is configured to start all three applications.

Once the application is started, you are redirected to the logon:

angular_aspnet5_IdentityServer4_01

You can then view the client requested scopes and allow the application to use the scopes:
angular_aspnet5_IdentityServer4_02

The application can access and use the resource server:
AngularClientIdentityServer3_03

Links:

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

https://github.com/tjoudeh/AngularJSAuthentication



Viewing all articles
Browse latest Browse all 269

Trending Articles