OpenID Connect and ASP.NET Core 1.0
Please note this information is current as of ASP.NET Core 1.0 RC1.
Introduction
The overall process of getting OpenID Connect (OIDC) working on ASP.NET Core 1.0 is similar to previous versions of ASP.NET, but does require knowledge of the various property and package changes. This post will highlight some of the major differences and demonstrate a few pitfalls to avoid.
Classic ASP.NET
Here’s an example of pre-ASP.NET Core 1.0 Startup.cs
leveraging OpenID Connect:
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Threading.Tasks;
public class Startup
{
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieName = "MyApp",
CookieSecure = CookieSecureOption.Always
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://example.org/",
ClientId = "my-client",
Scope = "openid profile email",
RedirectUri = "https://path-to-app.com",
ResponseType = "token id_token",
SignInAsAuthenticationType = "Cookies",
AuthenticationType = "oidc",
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = async x =>
{
var identity = x.AuthenticationTicket.Identity;
var subject = identity.Claims.FirstOfDefault(y => y.Type == "sub");
// Do something with subject like lookup in local users DB.
var newIdentity = new ClaimsIdentity(
identity.AuthenticationType,
"given_name",
"role");
// Do some stuff to `newIdentity` like adding claims.
// Create a new ticket with `newIdentity`.
x.AuthenticationTicket = new AuthenticationTicket(
newIdentity,
x.AuthenticationTicket.Properties);
await Task.FromResult(0);
}
}
});
}
}
Then, just prepend a controller or controller-action with [Authorize]
and you are in business.
ASP.NET Core 1.0
Now, the same behavior in ASP.NET Core 1.0:
using Microsoft.AspNet.Authentication;
using Microsoft.AspNet.Authentication.Cookies;
using Microsoft.AspNet.Authentication.OpenIdConnect;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Authorization.Infrastructure;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Owin;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
public class Startup
{
// Some boilerplate removed for readability.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
// This section can also be achieved using attributes on controllers and controller-actions.
services
.AddMvc(x =>
{
x.Filters.Add(new AuthorizeFilter(
new AuthorizationPolicy(
requirements: new List<RolesAuthorizationRequirement>()
{
new RolesAuthorizationRequirement(
new List<string>() { "User" })
},
authenticationSchemes: new List<string>() { "Cookies", "oidc" })));
});
}
// Configure is called after ConfigureServices is called.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCookieAuthentication(x =>
{
x.AutomaticAuthenticate = true;
x.CookieName = "MyApp";
x.CookieSecure = CookieSecureOption.Always;
x.AuthenticationScheme = "Cookies";
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();
app.UseOpenIdConnectAuthentication(x =>
{
x.AutomaticAuthenticate = true;
x.Authority = "https://example.org/";
x.ClientId = "my-client";
x.ResponseType = "token id_token";
x.AuthenticationScheme = "oidc";
x.CallbackPath = "signin-oidc"; // This the default-value.
x.Scope.Add("openid");
x.Scope.Add("profile");
x.Scope.Add("email");
x.Events = new OpenIdConnectEvents()
{
OnAuthenticationValidated = async y =>
{
var identity = y.AuthenticationTicket.Principal.Identity as ClaimsIdentity;
var subject = identity.Claims.FirstOrDefault(z => z.Type == "sub");
// Do something with subject like lookup in local users DB.
var newIdentity = new ClaimsIdentity(
y.AuthenticationTicket.AuthenticationScheme,
"given_name",
"role");
// Do some stuff to `newIdentity` like adding claims.
// Create a new ticket with `newIdentity`.
x.AuthenticationTicket = new AuthenticationTicket(
new ClaimsPrincipal(newIdentity),
y.AuthenticationTicket.Properties,
y.AuthenticationTicket.AuthenticationScheme);
await Task.FromResult(0);
}
};
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Here are highlights between the two
AuthenticationType
is nowAuthenticationScheme
.Scope
is nowIList
instead ofstring
.RedirectUri
is gone in favor ofCallbackPath
. Former is an absolute URI while the latter is a post-domain path. This value cannot be blank. See this issue on GitHub. This may cause the most headache if you have many clients registered pointing to the client’s base-domain.Notifications
is nowEvents
along with updated property names (i.e.SecurityTokenValidated
is nowOnAuthenticationValidated
andRedirectToIdentityProvider
is nowOnRedirectToAuthenticationEndpoint
).
Some common pitfalls
- Using same
AuthenticationScheme
betweenUseCookieAuthentication
andUseOpenIdConnectAuthentication
. Trying this will work up until the request comes back from the authority. You will receive this exception:
System.NotSupportedException: Specified method is not supported.
at Microsoft.AspNet.Authentication.RemoteAuthenticationHandler`1.HandleSignInAsync(SignInContext context)
at Microsoft.AspNet.Authentication.AuthenticationHandler`1.<SignInAsync>d__61.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Http.Authentication.Internal.DefaultAuthenticationManager.<SignInAsync>d__13.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Authentication.RemoteAuthenticationHandler`1.<HandleRemoteCallbackAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.AspNet.Authentication.RemoteAuthenticationHandler`1.<HandleRequestAsync>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.AspNet.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNet.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNet.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Owin.WebSocketAcceptAdapter.<>c__DisplayClass6_0.<<AdaptWebSockets>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()
- Ordering
authenticationSchemes
withinAuthorizationPolicy
is important. In this particular example, reversing the order (i.e.new List<string>() { "oidc", "Cookies" }
) will result in a login redirect loop since cookies set during the initial authentication process are never read by the middleware and the request gets passed back to the authority.
Summary
Aside from middleware name-changes and the separation of concerns between NuGet packages, the new middleware works as-expected with current OIDC providers like IdentityServer.