Asp.Net Core RC2, OpenIdConnect, JWT, Swagger, AutoRest and Angular 2 SPA - Part 2

Continuing on from a previous post this article details my journey in upgrading a Service Fabric multi-tenant application from .Net Core RC1 to RC2, which turned out to be a breaking albeit worthwhile change, specifically for the Startup.cs class and related boot strapping code for Swagger, CookieAuthentication, OpenIdConnectAuthentication and JwtBearerAuthentication. In subsequent posts we’ll explore how .Net Core RC2 hosts web applications but for now let’s look at the first challenge encountered during the upgrade, which was to chase down all required libraries that are also .Net Core RC2 compatible.

As of the time of writing, I could only get Swashbuckle version 6.0.0-beta9 to work with .Net Core RC2.

The below code supports multi-tenant Azure AD authentication and is meant for development scenarios as ValidateIssuer and RequireHttpsMetadata are both set to false for simplicity.

The full dependencies section of your project.json should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"dependencies": {
"Microsoft.AspNetCore.Hosting": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Authentication": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Diagnostics": "1.0.0-rc2-final",
"Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000004",
"Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Mvc.Core": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0-rc2-final",
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final",
"Microsoft.IdentityModel.Clients.ActiveDirectory": "3.9.302261508-alpha",
"Microsoft.Extensions.Configuration.Binder": "1.0.0-rc2-final",
"Swashbuckle": "6.0.0-beta9",
"Swashbuckle.SwaggerUi": "6.0.0-beta9",
"Swashbuckle.SwaggerGen": "6.0.0-beta9"
}

Your Startup.cs usings should look something like the below:

1
2
3
4
5
6
7
8
9
10
11
12
13
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using System;
using System.Net;

Having sourced the relevant libraries and compatible versions, it’s now time to turn our attention to the ConfigureServices method wherein we’ll setup Swagger, tweak Json formatting for JavaScript clients such as our Angular 2 SPA, and finally also tweak how AutoRest generates client code. I want AutoRest to generate separate files per server side controller which is achieved through a custom SwaggerOperationNameFilter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public IServiceProvider ConfigureServices(IServiceCollection services)
{

// Add MVC service
services.AddMvc().AddJsonOptions(options =>
{
// Support for JavaScript clients which assume CamelCase - starting with lower case
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});

// Add Swagger API service
services.AddSwaggerGen();
services.ConfigureSwaggerGen(options =>
{
options.SingleApiVersion(new Swashbuckle.SwaggerGen.Generator.Info
{
Version = "v1",
Title = "Acme API",
Description = "Acme API Home",
TermsOfService = "Legal"
});

// Controls how tools like AutoRest generate client code (separate files per server side controller)
options.OperationFilter<SwaggerOperationNameFilter>();
options.DescribeStringEnumsInCamelCase();
options.DescribeAllEnumsAsStrings();
});

var acmeOptions = new AcmeOptions();
Configuration.Bind(acmeOptions);
services.AddSingleton(acmeOptions);

// Configure IoC service
var builder = new ContainerBuilder();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}

Code for the custom SwaggerOperationNameFilter:

1
2
3
4
5
6
7
internal class SwaggerOperationNameFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{

operation.OperationId = context.ApiDescription.GroupName + "_" + operation.OperationId;
}
}

Concluding the changes required for the .Net Core RC2 upgrade, we dive into the Configure method. Canny readers will notice that UseCookieAuthentication, UseOpenIdConnectAuthentication and UseJwtBearerAuthentication have been refactored to handle options in a more consistent manner with the rest of the .Net Core APIs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
CookieSecure = CookieSecureOption.Never,
// The default setting for cookie expiration is 14 days. SlidingExpiration is set to true by default
ExpireTimeSpan = TimeSpan.FromHours(1),
SlidingExpiration = true
});

var acmeOptions = app.ApplicationServices.GetService<AcmeOptions>();

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
ClientId = acmeOptions.ClientId,
Authority = AcmeConstants.AuthEndpointPrefix + "common/",
PostLogoutRedirectUri = acmeOptions.PostLogoutRedirectUri,
CallbackPath = AcmeRouteConstants.LoginCallbackRoute,
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false },
RequireHttpsMetadata = false,
Events = new OpenIdConnectAuthenticationEvents(acmeOptions)
{
OnAuthenticationFailed = context => OpenIdConnectAuthenticationEvents.GetFailedResponse(context)
}
});

// Add JwtBearerAuthentication middleware
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AuthenticationScheme = JwtBearerDefaults.AuthenticationScheme,
Audience = acmeOptions.JwtAudience,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = AcmeConstants.AuthEndpointPrefix + "common/",
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
},
RequireHttpsMetadata = false,
Events = new JwtBearerAuthenticationEvents(acmeOptions)
{
OnAuthenticationFailed = context => JwtBearerAuthenticationEvents.GetFailedResponse(context)
}
});

app.UseMvc(routes =>
{
routes.MapRoute(
name: "webapi",
template: "api/{controller}/{action}/{id?}");

routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
});

// Enable Use of Swagger
app.UseSwaggerGen();
app.UseSwaggerUi();
}

If you’re wondering why I left the Microsoft.IdentityModel.Clients.ActiveDirectory library at “3.9.302261508-alpha”, in upcoming posts we’ll detail a strategy for automated integration testing of your .Net Core APIs using xUnit and optionally a BDD approach (SpecFlow), but more on that topic soon…