Most of the literature concerning the theme of authentication in ASP.NET Core focuses on the use of the ASP.NET Identity framework. In that context, things don’t seem to have changed much or, more precisely, all the changes that occurred in the infrastructure have been buried in the folds of the framework so that it looks nearly the same on the surface.
If you look at user authentication in ASP.NET Core outside the comfortable territory of ASP.NET Identity, you might find it quite different from what it was in past versions. ASP.NET Identity is a full-fledged, comprehensive, big framework that's overkill if all you need is to authenticate users via plain credentials from a simple database table. In this case, you'll see that the overall approach to authentication is still based on familiar concepts such as principal, login form, challenge and authorization attributes, except that the way you implement them is radically different. In this month's column, I'll explore the cookie authentication API as made available in ASP.NET Core, including the core facts of external authentication.
Foundation of ASP.NET Authentication
In ASP.NET, user authentication involves the use of cookies. Any users that attempt to visit a private page are redirected to a login page if they don't carry a valid authentication cookie. The login page, after having verified provided creden-tials, emits the cookie, which then travels with any subsequent requests from that user through the same browser until it expires. This is the same basic workflow you might know from past versions of ASP.NET. In ASP.NET Core, it only looks different because of the different middleware and the different configuration of the runtime environment.
There are two major changes in ASP.NET Core for those coming from an ASP.NET Web Forms and ASP.NET MVC background. First, there's no longer a web.config file, meaning that configuration of the login path, cookie name and expiration is retrieved differently. Second, the IPrincipal object—the object used to model user identity — is now based on claims rather than the plain user name. To enable cookie authentication in a brand-new ASP.NET Core 1.x application, you first reference the Microsoft.AspNetCore.Authentication.Cookies package and then add the code snippet in Figure 1.
Figure 1 Registering Middleware for Cookie Authentication
// This code is for ASP.NET Core 1.x
public void Configure(IApplicationBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies",
CookieName = "YourAppCookieName",
LoginPath = new PathString("/Account/Login"),
ExpireTimeSpan = TimeSpan.FromMinutes(),
SlidingExpiration = true,
ReturnUrlParameter = "original",
AccessDeniedPath = new PathString("/Account/Denied")
});
}
Most of the information that classic ASP.NET MVC applications stored in the <authentication> section of the web.config file are configured as middleware options. The snippet in Figure 1 comprehends canonical options you might want to choose. Figure 2 explains each in more detail.
Figure 2 Cookie Authentication Options
Property | Description |
AccessDeniedPath | Indicates the path where an authenticated user will be redirected if the provided identity doesn’t have permission to view the requested resource. The same as getting an HTTP 403 status code. |
AutomaticAuthenticate | Indicates the middleware runs on every request and attempts to validate cookie and build an identity object from content. |
AutomaticChallenge | Indicates the middleware redirects the browser to a login page if the user isn’t authenticated or to the access denied page if the user is authenticated but not authorized on the requested resource. |
AuthenticationScheme | Name of the middleware. This property works in conjunction with AutomaticChallenge to selectively pick up the authentication middleware on a per-request basis. |
CookieName | Name of the authentication cookie being created. |
ExpireTimeSpan | Sets the expiration time of the authentication cookie. Whether the time has to be intended as absolute or relative is determined by the value of the SlidingExpiration property. |
LoginPath | Indicates the path where an anonymous user will be redirected to sign in with her own credentials. |
ReturnUrlParameter | Name of the parameter being used to pass the originally requested URL that caused the redirect to the login page in case of anonymous users. |
SlidingExpiration | Indicates whether the ExpireTimeSpan value is absolute or relative. In the latter case, the value is considered as an interval and the middleware will reissue the cookie if more than half the interval has elapsed. |
注意上面这个表格的很多属性从ASP.NET Core 2.X开始,应该在Startup类的ConfigureServices方法中进行设置,例如下面就演示了如何在ASP.NET Core 2.X中设置Cookie认证的Cookie名字:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//注册Cookie认证服务
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option =>
{
option.Cookie.Name = "AspNetCookieAuth";//设置Cookie认证的Cookie名字
}); services.AddMvc();
}
Note that path properties are not of type string. LoginPath and AccessDeniedPath are of type PathString, which, compared to the plain String type, provides correct escaping when building a request URL.
The overall design of the user authentication workflow in ASP.NET Core gives you an unprecedented amount of flexi-bility. Every aspect of it can be customized at will. As an example, let's see how you can control the authentication work-flow being used on a per-request basis.
Dealing with Multiple Authentication Schemes
By setting AutomaticChallenge to false, you instruct the middleware not to react to the [Authorize] challenges by per-forming, say, a redirect. If no middleware will handle the challenge an exception is thrown. Automatic challenge was the norm in past versions of ASP.NET and there was almost nothing you could do about it.
In ASP.NET Core, you can register multiple and distinct pieces of authentication middleware and determine either algorithmically or via configuration which middleware has to be used for each request. When multiple authentication middleware is used, Automatic Authenticate can be true on multiple middleware. AutomaticChallenge, instead, should only be enabled on zero or one middleware. For more details, see bit.ly/2tS07Sm.
Common examples of authentication middleware are cookie-based authentication, bearer authentication, authentication through social networks, or an identity server and whatever else you can ever think to implement. Suppose you have the following code in the Configure method of your startup class:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "Cookies",
LoginPath = new PathString("/Account/Login/"),
AutomaticAuthenticate = true
});
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
AuthenticationScheme = "Bearer",
AutomaticAuthenticate = true
}
Be aware that there are constants to be used in place of magic strings like "Cookies" just to limit typos. In particular, the string "Cookies" can be replaced as below:
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme
Note that UseIdentityServerAuthentication isn't part of the ASP.NET Core framework but belongs to the Identity Server framework (see github.com/IdentityServer). To choose the authentication scheme on a per-request basis, you use a new attribute on the Authorize attribute that in ASP.NET MVC marks actions as subject to authentication and authorization:
[Authorize(ActiveAuthenticationSchemes = "Bearer")]
public class ApiController : Controller
{
// Your API action methods here
...
}
The net effect of the code snippet is that all public endpoints of the sample ApiController class are subject to the identity of the user as authenticated by the bearer token.
Modeling the User Identity
In ASP.NET, the IPrincipal interface defines the software contract that defines the core of the user identity. The logged user is exposed through the User property of the HttpContext controller property. IPrincipal has the same implementation in ASP.NET 4.x (including ASP.NET MVC) and ASP.NET Core. However, in ASP.NET Core the default principal object isn't GenericPrincipal, but the new ClaimsPrincipal type. The difference is relevant.
GenericPrincipal wraps up one key piece of user information—the user name—even though custom user data can be added to the authentication ticket encrypted in the cookie. Over the years, the sole user name has become too little for the needs of modern applications. The role of the user, as well as some other chunks of information, most noticeably picture and display name, appear absolutely required today, forcing every realistic application to create its own custom principal type or query user information for each and every request using the user name as the key. ClaimsPrincipal just brilliantly solves the problem.
A claim is a key/value pair that describes a property of the logged user. The list of properties is up to the application, but includes name and role at the very minimum. Claims, in other words, are the ASP.NET Core way to model identity information. Claims can be read from any source, whether databases, cloud or local storage, even hardcoded. The Claim class is as simple as this:
public class Claim
{
public string Type { get; }
public string Value { get; } // More properties ...
}
The login process in ASP.NET Core passes through three classes: Claim, ClaimIdentity and ClaimsPrincipal.
Collecting the list of claims is a simple matter of populating an array of Claim objects:
public Claims[] LoadClaims(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.Role),
new Claim("Picture", user.Picture)
};
return claims;
}
The name of the claim is a plain descriptive name rendered as a string. However, most common claim types have been grouped as constants into the ClaimTypes class. Before you create the principal, which is required to call the authentication workflow completed, you must get hold of an identity object:
var identity = new ClaimsIdentity(claims, "Password");
The first argument is self-explanatory—the list of claims associated with the identity being created. The second argument is a string that refers to the authentication scheme required to verify the identity. The string "Password" is a reminder of what will be required by the system for a user to prove her identity. The string "Password" is informational only and not a syntax element.
Another interesting aspect of the previous example is that an explicit user name is not strictly required. Any claim, regardless of the declared type, can be used to name the user. The following code snippet shows another equivalent way to have a new identity object:
var identity = new ClaimsIdentity(claims,
CookieAuthenticationDefaults.AuthenticationScheme,
"Nickname",
ClaimTypes.Role);
The new identity has "Cookies" as the authentication scheme and Nickname is the name of the claim in the provided list to be used to provide the name of the user. Role, instead, is the name of the claim in the same list determining the role. If not specified, the last two parameters default to ClaimTypes.Name and ClaimTypes.Role. Finally, you create the principal from the identity. It's worth noting, though, that a principal may have multiple identities. If it sounds weird, think that different areas of the same application might need different information to authenticate the user in much the same way an ID is required to identify yourself at some hotel desks and an electronic key to get into the elevator. The ClaimsPrincipal class has both an Identities property (a collection) and an Identity property. The latter is only a reference to the first item in the collection.
External Authentication
ASP.NET Core supports external authentication via identity providers from the ground up. Most of the time, all you do is install the appropriate NuGet package for the task. To rely on Twitter for authenticating users, you bring in the Microsoft.AspNetCore.Authentication.Twitter package and install the related middleware:
app.UseTwitterAuthentication(new TwitterOptions()
{
AuthenticationScheme = "Twitter",
SignInScheme = "Cookies",
ConsumerKey = "...",
ConsumerSecret = "..."
});
The SignInScheme property is the identifier of the authentication middleware that will be used to persist the resulting identity. In the example, an authentication cookie will be used. To see its effects, you first add a controller method to call into Twitter:
public async Task TwitterAuth()
{
var props = new AuthenticationProperties
{
RedirectUri = "/"
};
await HttpContext.Authentication.ChallengeAsync("Twitter", props);
}
Next, once Twitter has successfully authenticated the user, the SignInScheme property instructs the application on what to do next. A value of "Cookies" is acceptable if you want a cookie out of the claims returned by the external provider (Twitter, in the example). If you want to review and complete the information through, say, an intermediate form, then you have to break the process in two, introducing a temporary sign-in scheme. In addition to the standard cookie middleware you have the following:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "ExternalCookie",
AutomaticAuthenticate = false
});
app.UseTwitterAuthentication(new TwitterOptions
{
AuthenticationScheme = "Twitter",
SignInScheme = "ExternalCookie"
});
When the external provider returns, a temporary cookie is created using the ExternalCookie scheme. Having set the redirect path appropriately, you have a chance to inspect the principal returned by Twitter and edit it further:
var props = new AuthenticationProperties
RedirectUri = "/account/external"
};
To complete the workflow you also need to sign in in the cookies scheme and sign out of temporary scheme (ExternalCookie):
public async Task<IActionResult> External()
{
var principal = await HttpContext
.Authentication
.AuthenticateAsync("ExternalCookie");
// Edit the principal
... await HttpContext.Authentication.SignInAsync("Cookies", principal);
await HttpContext.Authentication.SignOutAsync("ExternalCookie "); return View();
}
ExternalCookie, as well as cookies, are just internal identifiers and can be renamed as long as they remain consistent throughout the application.
Wrapping Up
In ASP.NET Core many things seem to be radically different, but in the end most of the concepts you might know from ASP.NET remain unchanged. You still need to have an authentication cookie created, and you can still control the name of the cookie and the expiration date. External authentication is supported and login pages have the same structure as before. However, configuration and underlying working of the authentication infrastructure are different, while retaining their previous flexibility.
Everything stated in this article refers to ASP.NET Core 1.x. There are a few things that will work differently in ASP.NET Core 2.0. In particular, authentication middleware is now exposed as services and must be configured on Configure-Services:
services.AddCookieAuthentication(options =>
{
Options.LoginPath = new PathString("/Account/Login"),
options.AutomaticAuthenticate = true,
options.AutomaticChallenge = true,
options.AuthenticationScheme = "Cookies",
...
});
In the Configure method of the Startup class of ASP.NET Core 2.0 applications, you just declare your intention to use authentication services without any further options:
app.UseAuthentication();
Also note that the SignInAsync method you use in your code to create the authentication cookie is also exposed from the HttpContext object directly, instead of passing through an intermediate Authentication property as shown in the last code snippet for ASP.NET Core 1.x.