OpenID Connect and ASP.NET Core 1.0

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

usingMicrosoft.Owin.Security;usingMicrosoft.Owin.Security.Cookies;usingMicrosoft.Owin.Security.OpenIdConnect;usingOwin;usingSystem.Collections.Generic;usingSystem.IdentityModel.Tokens;usingSystem.Security.Claims;usingSystem.Threading.Tasks;publicclassStartup{publicvoidConfiguration(IAppBuilderapp){JwtSecurityTokenHandler.InboundClaimTypeMap=newDictionary<string,string>();app.UseCookieAuthentication(newCookieAuthenticationOptions{AuthenticationType="Cookies",CookieName="MyApp",CookieSecure=CookieSecureOption.Always});app.UseOpenIdConnectAuthentication(newOpenIdConnectAuthenticationOptions{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=newOpenIdConnectAuthenticationNotifications(){SecurityTokenValidated=asyncx=>{varidentity=x.AuthenticationTicket.Identity;varsubject=identity.Claims.FirstOfDefault(y=>y.Type=="sub");// Do something with subject like lookup in local users DB.varnewIdentity=newClaimsIdentity(identity.AuthenticationType,"given_name","role");// Do some stuff to `newIdentity` like adding claims.// Create a new ticket with `newIdentity`.x.AuthenticationTicket=newAuthenticationTicket(newIdentity,x.AuthenticationTicket.Properties);awaitTask.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:

usingMicrosoft.AspNet.Authentication;usingMicrosoft.AspNet.Authentication.Cookies;usingMicrosoft.AspNet.Authentication.OpenIdConnect;usingMicrosoft.AspNet.Authorization;usingMicrosoft.AspNet.Authorization.Infrastructure;usingMicrosoft.AspNet.Builder;usingMicrosoft.AspNet.Hosting;usingMicrosoft.AspNet.Mvc.Filters;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingOwin;usingSystem.Collections.Generic;usingSystem.IdentityModel.Tokens.Jwt;usingSystem.Linq;usingSystem.Security.Claims;usingSystem.Threading.Tasks;publicclassStartup{// Some boilerplate removed for readability.publicvoidConfigureServices(IServiceCollectionservices){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(newAuthorizeFilter(newAuthorizationPolicy(requirements:newList<RolesAuthorizationRequirement>(){newRolesAuthorizationRequirement(newList<string>(){"User"})},authenticationSchemes:newList<string>(){"Cookies","oidc"})));});}// Configure is called after ConfigureServices is called.publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv,ILoggerFactoryloggerFactory){app.UseCookieAuthentication(x=>{x.AutomaticAuthenticate=true;x.CookieName="MyApp";x.CookieSecure=CookieSecureOption.Always;x.AuthenticationScheme="Cookies";});JwtSecurityTokenHandler.DefaultInboundClaimTypeMap=newDictionary<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=newOpenIdConnectEvents(){OnAuthenticationValidated=asyncy=>{varidentity=y.AuthenticationTicket.Principal.IdentityasClaimsIdentity;varsubject=identity.Claims.FirstOrDefault(z=>z.Type=="sub");// Do something with subject like lookup in local users DB.varnewIdentity=newClaimsIdentity(y.AuthenticationTicket.AuthenticationScheme,"given_name","role");// Do some stuff to `newIdentity` like adding claims.// Create a new ticket with `newIdentity`.x.AuthenticationTicket=newAuthenticationTicket(newClaimsPrincipal(newIdentity),y.AuthenticationTicket.Properties,y.AuthenticationTicket.AuthenticationScheme);awaitTask.FromResult(0);}};});app.UseMvc(routes=>{routes.MapRoute(name:"default",template:"{controller=Home}/{action=Index}/{id?}");});}}

Here are highlights between the two

AuthenticationType is now AuthenticationScheme.

Scope is now IList instead of string.

RedirectUri is gone in favor of CallbackPath. 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 now Events along with updated property names (i.e. SecurityTokenValidated is now OnAuthenticationValidated and RedirectToIdentityProvider is now OnRedirectToAuthenticationEndpoint).

Some common pitfalls

Using same AuthenticationScheme between UseCookieAuthentication and UseOpenIdConnectAuthentication. 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 within AuthorizationPolicy 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.