IdentityServer4, Web API and Angular in a single ASP.NET Core project

This article shows how IdentityServer4 with Identity, a data Web API, and an Angular SPA could be setup inside a single ASP.NET Core project. The application uses the OpenID Connect Implicit Flow with reference tokens to access the API. The Angular application uses webpack to build.

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

History:

2019-09-20: Updated ASP.NET Core 3.0, Angular 8.2.6
2018-06-22: Updated ASP.NET Core 2.1, Angular 6.0.6, ASP.NET Core Identity 2.1

Full history:
https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow#history

Other posts in this series:

Step 1: Create app and add IdentityServer4

Use the Quickstart6 AspNetIdentity from IdentityServer 4 to setup the application. Then edit the project json file to add your packages as required. I added the Microsoft.AspNetCore.Authentication.JwtBearer package and also the IdentityServer4.AccessTokenValidation package. The buildOptions have to be extended to ignore the node_modules folder.

{
    "name": "angular-webpack-visualstudio",
    "version": "5.0.22",
    "description": "An Angular VS template",
    "main": "wwwroot/index.html",
    "author": "",
    "license": "ISC",
    "repository": {
        "type": "git",
        "url": "https://github.com/damienbod/Angular2WebpackVisualStudio.git"
    },
    "scripts": {
        "start": "concurrently \"webpack-dev-server --env=dev --open --hot --inline --port 8080\" \"dotnet run\" ",
        "webpack-dev": "webpack --env=dev",
        "webpack-production": "webpack --env=prod",
        "build": "npm run webpack-dev",
        "build-dev": "npm run webpack-dev",
        "build-production": "npm run webpack-production",
        "watch-webpack-dev": "webpack --env=dev --watch --color",
        "watch-webpack-production": "npm run build-production --watch --color",
        "publish-for-iis": "npm run build-production && dotnet publish -c Release",
        "test": "karma start",
        "test-ci": "karma start --single-run --browsers ChromeHeadless",
        "lint": "tslint -p tsconfig.json angularApp/**/*.ts"
    },
    "dependencies": {
        "@angular/animations": "~8.2.6",
        "@angular/common": "~8.2.6",
        "@angular/compiler": "~8.2.6",
        "@angular/core": "~8.2.6",
        "@angular/forms": "~8.2.6",
        "@angular/http": "~7.2.15",
        "angular-l10n": "^8.1.2",
        "@angular/platform-browser": "~8.2.6",
        "@angular/platform-browser-dynamic": "~8.2.6",
        "@angular/router": "~8.2.6",
        "bootstrap": "4.3.1",
        "core-js": "^2.6.5",
        "ie-shim": "0.1.0",
        "jsrsasign": "8.0.12",
        "popper.js": "^1.15.0",
        "rxjs": "~6.5.3",
        "rxjs-compat": "^6.5.3",
        "zone.js": "~0.10.2"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "~0.803.4",
        "@angular/cli": "~8.3.4",
        "@angular/compiler-cli": "~8.2.6",
        "@angular/language-service": "~8.2.6",
        "@types/node": "~12.7.5",
        "@types/jasmine": "~3.4.0",
        "@types/jasminewd2": "~2.0.6",
        "codelyzer": "~5.1.0",
        "jasmine-core": "~3.4.0",
        "jasmine-spec-reporter": "~4.2.1",
        "karma": "~4.3.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage-istanbul-reporter": "~2.1.0",
        "karma-jasmine": "~2.0.1",
        "@ngtools/webpack": "^6.2.5",
        "typescript": "~3.4.5",
        "karma-jasmine-html-reporter": "^1.4.2",
        "protractor": "~6.0.0",
        "ts-node": "~8.3.0",
        "tslint": "~5.20.0",
        "angular-router-loader": "0.8.5",
        "angular2-template-loader": "^0.6.2",
        "awesome-typescript-loader": "^5.2.1",
        "clean-webpack-plugin": "^2.0.2",
        "concurrently": "^4.1.0",
        "copy-webpack-plugin": "^5.0.3",
        "css-loader": "^2.1.1",
        "file-loader": "^3.0.1",
        "html-webpack-plugin": "^3.2.0",
        "jquery": "^3.4.1",
        "json-loader": "^0.5.7",
        "karma-sourcemap-loader": "^0.3.7",
        "karma-spec-reporter": "^0.0.32",
        "karma-webpack": "3.0.5",
        "raw-loader": "^1.0.0",
        "node-sass": "^4.12.0",
        "rimraf": "^2.6.3",
        "sass-loader": "^7.1.0",
        "source-map-loader": "^0.2.4",
        "style-loader": "^0.23.1",
        "tslint-loader": "^3.5.4",
        "toposort": "2.0.2",
        "uglifyjs-webpack-plugin": "^2.1.3",
        "url-loader": "^1.1.2",
        "webpack": "^4.40.2",
        "webpack-bundle-analyzer": "^3.5.0",
        "webpack-cli": "3.3.8",
        "webpack-dev-server": "^3.8.0",
        "webpack-filter-warnings-plugin": "^1.2.1"
    },
    "-vs-binding": {
        "ProjectOpened": [
            "watch-webpack-dev"
        ]
    }
}

The IProfileService interface is implemented to add your user claims to the tokens. The IdentityWithAdditionalClaimsProfileService class implements the IProfileService interface in this example and is added to the services in the Startup class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServerWithAspNetIdentity.Models;
using Microsoft.AspNetCore.Identity;

namespace IdentityServerWithAspNetIdentitySqlite
{
    using IdentityServer4;

    public class IdentityWithAdditionalClaimsProfileService : IProfileService
    {
        private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
        private readonly UserManager<ApplicationUser> _userManager;

        public IdentityWithAdditionalClaimsProfileService(UserManager<ApplicationUser> userManager,  IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
        {
            _userManager = userManager;
            _claimsFactory = claimsFactory;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();

            var user = await _userManager.FindByIdAsync(sub);
            var principal = await _claimsFactory.CreateAsync(user);

            var claims = principal.Claims.ToList();

            claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
            
            claims.Add(new Claim(JwtClaimTypes.GivenName, user.UserName));

            if (user.IsAdmin)
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
            }
            else
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "user"));
            }

            if (user.DataEventRecordsRole == "dataEventRecords.admin")
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.admin"));
                claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.user"));
                claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords"));
                claims.Add(new Claim(JwtClaimTypes.Scope, "dataEventRecords"));
            }
            else
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.user"));
                claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords"));
                claims.Add(new Claim(JwtClaimTypes.Scope, "dataEventRecords"));
            }

            if (user.SecuredFilesRole == "securedFiles.admin")
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles.admin"));
                claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles.user"));
                claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles"));
                claims.Add(new Claim(JwtClaimTypes.Scope, "securedFiles"));
            }
            else
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles.user"));
                claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles"));
                claims.Add(new Claim(JwtClaimTypes.Scope, "securedFiles"));
            }

            claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email));


            context.IssuedClaims = claims;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
    }
}

Step 2: Add the Web API for the resource data

The MVC Controller DataEventRecordsController is used for CRUD API requests. This is just a dummy implementation. I would implement all resource server logic in a separate project. The Authorize attribute is used with and without policies. The policies are configured in the Startup class.

using ResourceWithIdentityServerWithClient.Model;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System;

namespace ResourceWithIdentityServerWithClient.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class DataEventRecordsController : Controller
    {
        [Authorize("dataEventRecordsUser")]
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new List<DataEventRecord> { new DataEventRecord { Id =1, Description= "Fake", Name="myname", Timestamp= DateTime.UtcNow } });
        }

        [Authorize("dataEventRecordsAdmin")]
        [HttpGet("{id}")]
        public IActionResult Get(long id)
        {
            return Ok(new DataEventRecord { Id = 1, Description = "Fake", Name = "myname", Timestamp = DateTime.UtcNow });
        }

        [Authorize("dataEventRecordsAdmin")]
        [HttpPost]
        public void Post([FromBody]DataEventRecord value)
        {
            
        }

        [Authorize("dataEventRecordsAdmin")]
        [HttpPut("{id}")]
        public void Put(long id, [FromBody]DataEventRecord value)
        {
            
        }

        [Authorize("dataEventRecordsAdmin")]
        [HttpDelete("{id}")]
        public void Delete(long id)
        {
            
        }
    }
}

Step 3: Add client Angular client API

The Angular 4 client part of the application is setup and using the ASP.NET Core, Angular2 with Webpack and Visual Studio article. Webpack is then used to build the client application.

Any SPA client can be used which supports the OpenID Connect Implicit Flow. IdentityServer4 (IdentityModel) also have good examples using the OIDC javascript client.

Step 4: Configure application host URL

The URL host is the same for both the client and the server. This is configured in the Config class as a static property HOST_URL and used throughout the server side of the application.

public class Config
{
        public static string HOST_URL =  "https://localhost:44363";

The client application reads the configuration from the auth.configuration.ts provider.

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { Configuration } from './app.constants';
import { routing } from './app.routes';

import { HttpClientModule } from '@angular/common/http';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { UserManagementComponent } from './user-management/user-management.component';
import { DataEventRecordsModule } from './dataeventrecords/dataeventrecords.module';
import { NavigationComponent } from './navigation/navigation.component';
import { HasAdminRoleAuthenticationGuard } from './guards/hasAdminRoleAuthenticationGuard';
import { HasAdminRoleCanLoadGuard } from './guards/hasAdminRoleCanLoadGuard';
import { UserManagementService } from './user-management/UserManagementService';

import { AuthModule } from './auth/modules/auth.module';
import { OidcSecurityService } from './auth/services/oidc.security.service';
import { AuthWellKnownEndpoints } from './auth/models/auth.well-known-endpoints';
import { OpenIdConfiguration } from './auth/models/auth.configuration';

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        routing,
        HttpClientModule,
        DataEventRecordsModule,
        AuthModule.forRoot(),
    ],
    declarations: [
        AppComponent,
        ForbiddenComponent,
        HomeComponent,
        UnauthorizedComponent,
        UserManagementComponent,
        NavigationComponent,
    ],
    providers: [
        OidcSecurityService,
        UserManagementService,
        Configuration,
        HasAdminRoleAuthenticationGuard,
        HasAdminRoleCanLoadGuard
    ],
    bootstrap: [AppComponent],
})

export class AppModule {
    constructor(
        public oidcSecurityService: OidcSecurityService
    ) {
        const config: OpenIdConfiguration = {
            stsServer: 'https://localhost:44363',
            redirect_url: 'https://localhost:44363',
            client_id: 'singleapp',
            response_type: 'id_token token',
            scope: 'dataEventRecords openid',
            post_logout_redirect_uri: 'https://localhost:44363/Unauthorized',
            start_checksession: false,
            silent_renew: true,
            silent_renew_url: 'https://localhost:44363/silent-renew.html',
            post_login_route: '/dataeventrecords',
            forbidden_route: '/Forbidden',
            unauthorized_route: '/Unauthorized',
            log_console_warning_active: true,
            log_console_debug_active: true,
            max_id_token_iat_offset_allowed_in_seconds: 10
        };

        const authWellKnownEndpoints: AuthWellKnownEndpoints = {
            issuer: 'https://localhost:44363',
            jwks_uri: 'https://localhost:44363/.well-known/openid-configuration/jwks',
            authorization_endpoint: 'https://localhost:44363/connect/authorize',
            token_endpoint: 'https://localhost:44363/connect/token',
            userinfo_endpoint: 'https://localhost:44363/connect/userinfo',
            end_session_endpoint: 'https://localhost:44363/connect/endsession',
            check_session_iframe: 'https://localhost:44363/connect/checksession',
            revocation_endpoint: 'https://localhost:44363/connect/revocation',
            introspection_endpoint: 'https://localhost:44363/connect/introspect'
        };

        this.oidcSecurityService.setupModule(config, authWellKnownEndpoints);
    }
}


IIS Express is configured to run with HTTPS and matches these configurations. If a different port is used, you need to change these two code configurations. In a production environment, the data should be configurable pro deployment.

Step 5: Deactivate the consent view

The consent view is deactivated because the client is the only client to use this data resource and always requires the same consent. To improve the user experience, the consent view is removed from the flow. This is done by setting the RequireConsent property to false in the client configuration.

public static IEnumerable<Client> GetClients()
{
	// client credentials client
	return new List<Client>
	{
		new Client
		{
			ClientName = "singleapp",
			ClientId = "singleapp",
			RequireConsent = false,
			AccessTokenType = AccessTokenType.Reference,
			//AccessTokenLifetime = 600, // 10 minutes, default 60 minutes
			AllowedGrantTypes = GrantTypes.Implicit,
			AllowAccessTokensViaBrowser = true,
			RedirectUris = new List<string>
			{
				HOST_URL

			},
			PostLogoutRedirectUris = new List<string>
			{
				HOST_URL + "/Unauthorized"
			},
			AllowedCorsOrigins = new List<string>
			{
				HOST_URL
			},
			AllowedScopes = new List<string>
			{
				"openid",
				"dataEventRecords"
			}
		}
	};
}

Step 6: Deactivate logout screens

When the Angular 2 client requests a logout, the client is logged out, reference tokens are invalidated for this application and user, and the user is redirected back to the Angular 2 application without the server account logout views. This improves the user experience.

The existing 2 Logout action methods are removed from the AccountController and the following is implemented. The controller requires the IPersistedGrantService to remove the reference tokens.

[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<IActionResult> Logout(LogoutViewModel model)
{
	var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
	var subjectId = HttpContext.User.Identity.GetSubjectId();

	if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
	{
		if (model.LogoutId == null)
		{
			// if there's no current logout context, we need to create one
			// this captures necessary info from the current logged in user
			// before we signout and redirect away to the external IdP for signout
			model.LogoutId = await _interaction.CreateLogoutContextAsync();
		}

		string url = "/Account/Logout?logoutId=" + model.LogoutId;
		try
		{
			// hack: try/catch to handle social providers that throw
			await HttpContext.Authentication.SignOutAsync(idp, new AuthenticationProperties { RedirectUri = url });
		}
		catch(NotSupportedException)
		{
		}
	}

	// delete authentication cookie
	await _signInManager.SignOutAsync();

	// set this so UI rendering sees an anonymous user
	HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());

	// get context information (client name, post logout redirect URI and iframe for federated signout)
	var logout = await _interaction.GetLogoutContextAsync(model.LogoutId);

	var vm = new LoggedOutViewModel
	{
		PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
		ClientName = logout?.ClientId,
		SignOutIframeUrl = logout?.SignOutIFrameUrl
	};

	await _persistedGrantService.RemoveAllGrantsAsync(subjectId, "angular2client");

	return Redirect(Config.HOST_URL + "/index.html");
}

Step 7: Configure Startup to use all three application parts

The Startup class configures all three application parts to run together. The Angular 2 application requires that its client routes are routed on the client and not the server. Middleware is added so that the server does not handle the client routes.

The API service needs to check the reference token and validate. Policies are added for this and also the extension method UseIdentityServerAuthentication is used to check the reference tokens for each request.

IdentityServer4 is setup to use Identity with a SQLite database.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ResourceWithIdentityServerWithClient.Data;
using ResourceWithIdentityServerWithClient.Models;
using QuickstartIdentityServer;
using IdentityServer4.Services;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System;
using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using ResourceWithIdentityServerWithClient.Services.Certificate;
using ResourceWithIdentityServerWithClient.Services;
using Microsoft.Extensions.Hosting;

namespace ResourceWithIdentityServerWithClient
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
            _environment = env;
        }
        public IConfiguration Configuration { get; }
        public IWebHostEnvironment _environment { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            var useLocalCertStore = Convert.ToBoolean(Configuration["UseLocalCertStore"]);
            var certificateThumbprint = Configuration["CertificateThumbprint"];

            X509Certificate2 cert;

            if (_environment.IsProduction())
            {
                if (useLocalCertStore)
                {
                    using (X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
                    {
                        store.Open(OpenFlags.ReadOnly);
                        var certs = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, false);
                        cert = certs[0];
                        store.Close();
                    }
                }
                else
                {
                    // Azure deployment, will be used if deployed to Azure
                    var vaultConfigSection = Configuration.GetSection("Vault");
                    var keyVaultService = new KeyVaultCertificateService(vaultConfigSection["Url"], vaultConfigSection["ClientId"], vaultConfigSection["ClientSecret"]);
                    cert = keyVaultService.GetCertificateFromKeyVault(vaultConfigSection["CertificateName"]);
                }
            }
            else
            {
                cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "damienbodserver.pfx"), "");
            }

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));

            services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));

            services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddCors(options =>
            {
                options.AddPolicy("AllowAllOrigins",
                    builder =>
                    {
                        builder
                            .AllowCredentials()
                            .WithOrigins(
                                "https://localhost:44311",
                                "https://localhost:44352",
                                "https://localhost:44372",
                                "https://localhost:44378",
                                "https://localhost:44390")
                            .SetIsOriginAllowedToAllowWildcardSubdomains()
                            .AllowAnyHeader()
                            .AllowAnyMethod();
                    });
            });

            var guestPolicy = new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .RequireClaim("scope", "dataEventRecords")
           .Build();

            services.AddTransient<IProfileService, IdentityWithAdditionalClaimsProfileService>();

            services.AddIdentityServer()
                .AddSigningCredential(cert)
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients())
                .AddAspNetIdentity<ApplicationUser>()
                .AddProfileService<IdentityWithAdditionalClaimsProfileService>();

            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
              .AddIdentityServerAuthentication(options =>
              {
                  options.Authority = Config.HOST_URL + "/";
                  options.ApiName = "dataEventRecords";
                  options.ApiSecret = "dataEventRecordsSecret";
                  options.SupportedTokens = SupportedTokens.Both;
              });

            services.AddTransient<IEmailSender, EmailSender>();

            services.AddAuthorization(options =>
            {
                options.AddPolicy("dataEventRecordsAdmin", policyAdmin =>
                {
                    policyAdmin.RequireClaim("role", "dataEventRecords.admin");
                });
                options.AddPolicy("admin", policyAdmin =>
                {
                    policyAdmin.RequireClaim("role", "admin");
                });
                options.AddPolicy("dataEventRecordsUser", policyUser =>
                {
                    policyUser.RequireClaim("role", "dataEventRecords.user");
                });
                options.AddPolicy("dataEventRecords", policyUser =>
                {
                    policyUser.RequireClaim("scope", "dataEventRecords");
                });
            });

            services.AddControllers()
                .AddNewtonsoftJson()
                .SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
            services.AddControllersWithViews()
                .SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
               .AddViewLocalization();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            app.UseCors("AllowAllOrigins");

            var angularRoutes = new[] {
                "/Unauthorized",
                "/Forbidden",
                "/uihome",
                "/dataeventrecords",
                "/dataeventrecords/",
                "/dataeventrecords/create",
                "/dataeventrecords/edit/",
                "/dataeventrecords/list",
                "/usermanagement",
                };

            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.UseRouting();
            app.UseIdentityServer();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

The application can then be run and tested. To test, right click the project and debug.

Links

https://github.com/IdentityServer/IdentityServer4

http://docs.identityserver.io/en/dev/

https://github.com/IdentityServer/IdentityServer4.Samples

https://docs.asp.net/en/latest/security/authentication/identity.html

https://github.com/IdentityServer/IdentityServer4/issues/349

https://damienbod.com/2016/06/12/asp-net-core-angular2-with-webpack-and-visual-studio/

48 comments

  1. Thank you for great example! I’d like to know what are the benefits of using Implicit Flow for SPA if the developer is the owner of both backend and frontend part? Is it possible to convert backend of this sample to use ROPC and so use cool modal panels for registration and login right in angular part? If so will ROPC allows me to integrate social integration later?

    1. All-ToR · · Reply

      Hey, I know this is kinda random, but I’m facing the same question right now as you did, can you please tell what option did you choose in the end?

  2. Thank you for great example! I’d like to know what are the benefits of using Implicit Flow for SPA if the developer is the owner of both backend and frontend part? Is it possible to convert backend of this sample to use ROPC and so use cool modal panels for registration and login right in angular part? If so will ROPC allows me to integrate social integration later?

  3. Hi Damien. For anyone interested, I posted an example that uses ROPC: https://github.com/robisim74/Angular2SPAWebAPI. Greetings

    1. Hey Roberto, can you share your thoughts on refreshing the Access Token … couldn’t see that in your code.

      I have something like this in my code:

      public scheduleRefresh() {
      // If the user is authenticated, use the token stream and flatMap the token
      let source = this.tokenStream.flatMap(
      token => {
      return Observable.interval(this.authInfo.expires_in * 1000);
      });

      this.refreshSubscription = source.subscribe(() => {
      this.getNewJwt();
      });
      }

      public startupTokenRefresh() {
      // If the user is authenticated, use the token stream and flatMap the token
      if (this.authInfo && this.authInfo.access_token) {
      if (this.tokenNotExpired(this.authInfo.access_token)) {
      let source = this.tokenStream.flatMap(
      token => {
      // Get the expiry time to generate a delay in milliseconds
      let now: number = new Date().valueOf();
      let jwtExp: number = this.decodeToken(token).exp;
      let exp: Date = new Date(0);
      exp.setUTCSeconds(jwtExp);
      let delay: number = exp.valueOf() – now;

      // Use the delay in a timer to run the refresh at the proper time
      return Observable.timer(delay);
      });

      // Once the delay time from above is reached, get a new JWT and schedule additional refreshes
      source.subscribe(() => {
      this.getNewJwt();
      this.scheduleRefresh();
      });
      }
      }
      }

      public unscheduleRefresh() {
      // Unsubscribe fromt the refresh (logout)
      if (this.refreshSubscription) {
      this.refreshSubscription.unsubscribe();
      }
      }

      public getNewJwt() {
      // Get a new JWT using the refresh token saved in local storage
      if (this.authInfo && this.authInfo.refresh_token) {
      this.refresh();
      } else {
      console.error(JSON.stringify(this.authInfo));
      alert(“Error refreshing token”);
      }
      }

      Just wanted to compare, maybe there is a better way to do this.
      Took the code and idea from here : https://github.com/auth0/angular2-jwt/issues/172

      1. Hi @boban984, please post your questions directly on the project, because this is a different project. There is also a new issue for the refresh token. Greetings

  4. Thank you for great example. However I think a few images would have been nice to show how these parts are working together.

    1. Hi Dave

      Thanks for you feedback. Would make a good improvement. Next time I update this article, I will try to add some.

      Updating the code now.

      Greetings Damien

      1. I also second this. As an Identity Server novice it’s difficult to work out how this all hangs together with the provided code snippets.

  5. I tried to follow your posts about IdentityServer4 to accomplish a scenario which I’m dealing with but so far with no luck. I’m trying to build a very secure application and also offer the API’s to the others.

    After a lot of thinking I decided to build an angular 2 client application and a gateway-api. The users with enough permission may use the UI or the gateway directly. But my gateway-api’s should use the internal api’s(internal microservices).

    After reading the IdentityServer4 documentation, I guess I should use the IdentityServer4(with EntityFramework and AspNetIdentity packages) with hybrid flow.

    I tried to follow your IdentityServer4 posts, but unfortunatly I still can not glue all these together.

    I have to admit that I use https://github.com/aspnet/JavaScriptServices for the angular part.

    Do you have any suggestions or advice for me? Am I totally off-road? Do you know any tutorial or blogpost about my scenario? Because I’m searching for it days and still no luck.

    Thanks in advance.

    1. Hi Dave, JavaScriptServices is tip top, I use the Implicit Flow OpenID with reference tokens. If you use the hybrid, you have to change the client logic, as far as I know, no example exists for a javascript client. Reference tokens are important if you want to control the lifecycle (full logout is important)

      Everything seems good but why the hybrid flow for the SPA?

      Greetings Damien

      1. I am trying to follow all your articles. I have a question for this one – when we logout and press Back button, it works very well at all browsers. It does not take from cache previous page – what part of code preventing that? I am having issue with my current app that it works very well at chrome but still get previous page from cache after logout. Adding cache-control – no-store – to response header helps but it would just disallow any cache.

  6. Hi Damien,

    I’m not sure why Hybrid flow 🙂
    I may have misunderstood this part of the documentation. http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html

    Basically I want to have something like the pictures in this page of the documentation (see http://docs.identityserver.io/en/release/intro/big_picture.html) with OpenId Connect and OAuth2 to call the second level Web API’s which are internal and may not be seen or accessed from outside.

  7. Hi Damien,

    I think I have to use hybrid flow because other kind of clients like mobile apps and console apps may use the gateway api’s as well. So not only a javascript client(SPA) but also other kind of clients may use the public api’s as well.

  8. Sam Ellis · · Reply

    I have never run an Angular app in a .NET app like this. I’ve got the project building and running in VS with IIS Express, but I can’t seem to launch the Angular app itself? I only ever get the “This is server routing, not angular2 routing” returned when I try and hit any of the routes registered in the AngularRoutes array. Is there something I’m missing?

    1. Hi Sam
      You need to build the angular app. Go to the folder where the project file is in the commnad line
      $ npm install
      $ npm run-build-production

      Then it will work. See the packages.json for the possible scripts, to run in dev, production or publish

      Greetings Damien

  9. Hi Damien. Thank you so much for your time and creating this online. I downloaded the VS 2015 version to get familiar. I updated the packages and eventually seem to have everything working. The project builds so off to a great start. There’s a file named Find-VS2017.cs in 2 projects that is causing 10 warnings? For instance, Warning CS0618 ‘UnmanagedType.VariantBool’ is obsolete: ‘Marshalling as VariantBool may be unavailable in future releases…’

    Can you please explain why this file is here?

    Kind regards,

    Michael

    1. Hi Michael, thanks

      I don’t have these files in the VS2017 version. Don’t know what there are, maybe something to do with the upgrade. The master version is a VS2017, I have stopped maintaining the VS2015 version. Maybe you can compare yours to this. Greetings Damien

  10. Hi Damien. I shall. I’ll compare the VS2017 version with the VS2015. I see that you’re maintaining the VS2017 version. Cool! Thanks again!

    ~ Michael

  11. Hi Damien. So, I skipped ahead and I’m using your VS 2017 project. The project built successfully, but I haven’t tried to run it because I’m looking around still. Quite excellent and impressive. I’m a 15-year Microsoft SQL Server DBA. I’m following along in the IdentityServer4 documentation too. I hope you have time for some specific questions. I added the folder “Keys” under “ResourceServer”. I see in the config.json file that you are pointing to this directory. Is something missing from the project or is the empty folder correct? Second, I suppose it’s a good idea to know how to make a .pfx file?

    I hope my questions will help others wanting to understand all of this through your project templates.

    Thanks again!

  12. Hi again. I found that I need to run Update-Database. I had to add a reference to the project AngularClient from IdentityServerWithIdentitySQLite. Now I am only getting the error: SQLite Error 14: ‘unable to open database file’. Not sure yet, but I’m sharing my progress in getting your project working on my machine. 🙂

    ~ Michael

    1. The project uses a sqlite database, which is a file in the same directory as the csproj. you need to change the path in the config file to match this on your PC. Greetings Damien

  13. Hi Damien. I wish I could buy you a beer! I was able to register and that’s so freakin’ awesome to see this in action! Here’s an update on what I did, right I suppose, but possibly unnecessary.

    Recap: The project builds successfully and starts, but when I try and register myself, I get an error that states: A database operation failed while processing the request. SqliteException: SQLite Error 14: ‘unable to open database file’. Applying existing migrations for ApplicationDbContext may resolve this issue …

    The update command Update-Database ran successfully for the sqlite database in the ResourceServer project, but not the 2 others. There are 3 sqlite databases. When I ran update-database, I received the error: Could not load assembly ‘ResourceWithIdentityServerWithClient’. Ensure it is referenced by the startup project ‘IdentityServerWithIdentitySQLite’.

    I added the project reference. Then after running update-database again, I got the error: More than one DbContext was found. Specify which one to use. Use the ‘-Context’ parameter for PowerShell commands and the ‘–context’ parameter for dotnet commands.

    The above project reference issue is not the real issue. There are 3 sqlite databases and the path must be updated in the appsettings.json file for each project. One is in a config.json.

    So far so great! I’m going to read through your tutorial again and look at other related ones, and continue to get intimately comfortable with the application parts.

  14. Hi Damien. I imagine you’ve noticed that when you set the startup project to IdentityServerWithAspNetIdentity, the default page is https://localhost:44363/Index.html, and it’s empty. Going here shows the welcome page: https://localhost:44363/Home/Index … where I can then register.

  15. Okay. A question: How do I get to the Angular pages? I haven’t figured this out today.

    1. JoelS · · Reply

      Open up a package manager console and build the angular pages via webpack – i.e. ‘webpack -d’

      1. use the npm task runner and click on the webpack task you want to execute

      2. Thank you for the reply Joel!

      3. You too Damien. Thank you! I had to shift focus for several weeks. I also took a dive into SQLite, which is quite cool.

  16. Hi Damienbod, I was trying to run the project and couldn’t succeed . I was not able to run core project and also in the angular Uncaught SyntaxError: Unexpected identifier in polyfill . Could you please kindly let me know the steps required for a successful run

    1. Hi raki, you propbably need to build the SPA app. Try from the commandline

      > npm install
      > npm run build-production

      Should build without problems, otherwise you need to install node, npm etc.

      Then try running it from inside VS

      Hope this helps, greetings Damien

  17. Hi, I am getting “Error: Cannot find module ‘interpret'” when running watch-webpack-dev. Any ideea why?

    1. Forgot to mention this happens only for the ResourceWithIdentityServerWithClient project.

    2. No, I’ll have a look, try deleting the node_modules folder, npm install, and npm run build-dev

  18. Hi damienbod, here is the issue, since you combined Web API , Auth Server and Client App in the same , once you logged in , to access the Web API we don’t need to pass the token because you enabled the Cookie auth in the Server(since you conbined all API and AUTH Server in the same Project), In this case how will you for force the API to authenticate only through the token and not through the cookie.

    1. Hi Jay thanks for this info. I need to clean this up. In ASP.NET Core 2.0, the first Scheme is the defualt one. For the API, the Bearer must be just, which I need to fix, in the Auth Attribute on the controller. Thanks and greetings Damien

      1. Damienbod,
        do we need to custom AuthorizeAttribute to validate only token and skip cookie.
        Thanks,

      2. Authorize(AuthenicationScheme=”Bearer”) , I got it

  19. Nice post! Finally someone using Identity with JWT. Thanks for sharing. Here is another repo which uses AspNetCore.Identity with OpenId Connect. https://github.com/kkagill/ContosoUniversity-Backend if anyone is interested.

  20. […] IdentityServer4, Web API and Angular in a single ASP.NET Core project […]

  21. I am going to buy you a case of Bud Light. Dilly Dilly

  22. Hi Damien
    Excellent effort.
    I tried to use your code but not able to authorize with (for DataEventRecordController)
    [Authorize(AuthenticationSchemes = “Bearer”, Policy = “dataEventRecords”)]
    at class level and
    [Authorize(“dataEventRecordsAdmin”)]
    at method level.
    Simple [Authorize] works for both at class and method level.
    Same for UserManagementController.
    Am I missing anything?
    Any help will be highly appreciated.

    1. hi Sahoo

      Thanks

      Updated the repo to dotnet core 3.0 and everything should work now.

      Greetings Damein

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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: