After using OWIN for months for basic OAuth authentication, it’s apparent that Microsoft is abandoning OWIN . This isn’t necessarily a bad thing. .NET Core is built on a similar structure as that which was implemented in OWIN. Essentially, we have a familiar middleware pipeline.
.NET Core provides similar packages for OAuth and Cookie-based authentication. You can add these to your packages.json directly:
"Microsoft.AspNetCore.Authentication.OAuth": "1.0.0", "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0"
Let’s look at the Cookie middleware first. We can intercept a specific login/logput path to force the middleware to issue a 302 status on outgoing 401 status codes:
// Add cookie middleware app.UseCookieAuthentication(new CookieAuthenticationOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, LoginPath = new PathString("/login"), LogoutPath = new PathString("/logout") });
With the 302 redirect handler in place, we then need to tell the middle, through the Map interace, to run specific methods against the context. In this case, we are simply performing a challenge or signout as required:
// Listen for login and logout requests app.Map("/login", builder => { builder.Run(async context => { // Return a challenge to invoke the LinkedIn authentication scheme await context.Authentication.ChallengeAsync("Application", properties: new AuthenticationProperties() { RedirectUri = "/" }); }); }); app.Map("/logout", builder => { builder.Run(async context => { // Sign the user out / clear the auth cookie await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // Perform a simple redirect after logout context.Response.Redirect("/"); }); });
Now, if we access those endpoints, the requests are intercepted and treated accordingly. However, we require an authentication middleware to handle the challenges. We can attach the default OAuth middleware to handle the OAuth grant flow. The configuration, again, looks very similar to OWIN:
// Add the OAuth2 middleware app.UseOAuthAuthentication(MyMiddlewareOptions.GetOptions());
The UseOAuthAuthentication method takes an OAuthOptions object. I moved this into a static class while I’m testing. The import bits to configure are below:
public static OAuthOptions GetOptions() { var options = new OAuthOptions { AuthenticationScheme = "Application", ClientId = "someclient", ClientSecret = "somesecret", CallbackPath = new PathString("/myoauth/callback"), AuthorizationEndpoint = "https://oauthserver/OAuth/Authorize", TokenEndpoint = "https://oauthserver/OAuth/Token", UserInformationEndpoint = "https://oauthserver/getuserInfo", Scope = { "identity", "roles" }, Events = new OAuthEvents { OnCreatingTicket = async context => { await CreateAuthTicket(context); } } }; return options; }
The interesting thing here is the Event handler. The OnCreateTicket provides us with a standard mechanism to call the OAuthServer and get a user’s information. The context object will have the Identity, AccessToken, and UserInformationEndpoint available. Using this information, we can query the auth server for claims to attach to the identity. My particular OAuthServer’s endpoint returns a json object with a roles member and a name member. We call this endpoint with HttpClient, parse the result, and attach the claims to the Identity:
private static async Task CreateAuthTicket(OAuthCreatingTicketContext context) { // Get the User info using the bearer token var request = new HttpRequestMessage() { RequestUri = new Uri(context.Options.UserInformationEndpoint), Method = HttpMethod.Get }; request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); response.EnsureSuccessStatusCode(); var converter = new ExpandoObjectConverter(); dynamic user = JsonConvert.DeserializeObject<ExpandoObject>(await response.Content.ReadAsStringAsync(), converter); // Our respone should contain claims we're interested in. context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.name)); foreach (var role in user.roles) { context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, role)); } }
With these bits in place, I was able to get my current OAuth server working in .NET Core quickly. Using the default .NET Core templates, I added an extra list item to display the user’s name if they are logged in.
@if (User.Identity.IsAuthenticated) { <ul class="nav navbar-nav navbar-right"> <li><a href="#">@User.Identity.Name</a></li> </ul> }
If I browse to my login endpoint or logout endpoint, I can verify that the login/logout OAuth flow and claims identity (cookie) creation is working properly.