In my post on bearer token authentication in ASP.NET Core, I mentioned that there are a couple good third-party libraries for issuing JWT bearer tokens in .NET Core. In that post, I used OpenIddict to demonstrate how end-to-end token issuance can work in an ASP.NET Core application.
Since that post was published, I’ve had some requests to also show how a similar result can be achieved with the other third-party authentication library available for .NET Core: IdentityServer4. So, in this post, I’m revisiting the question of how to issue tokens in ASP.NET Core apps and, this time, I’ll use IdentityServer4 in the sample code.
As before, I think it’s worth mentioning that there are a lot of good options available for authentication in ASP.NET Core. Azure Active Directory Authentication is an easy way to get authentication as a service. If you would prefer to own the authentication process yourself, I’ve used and had success with both OpenIddict and IdentityServer4.
Bear in mind that both IdentityServer4 and OpenIddict are third-party libraries, so they are maintained and supported by community members – not by Microsoft.
The Scenario
As you may remember from last time, the goal of this scenario is to setup an authentication server which will allow users to sign in (via ASP.NET Core Identity) and provides a JWT bearer token that can be used to access protected resources from a SPA or mobile app.
In this scenario, all the components are owned by the same developer and trusted, so an OAuth 2.0 resource owner password flow is acceptable (and is used here because it’s simple to use in a demonstration). Be aware that this model exposes user credentials and access tokens (both of which are sensitive and could be used to impersonate a user) to the client. In more complex scenarios (especially if clients shouldn’t be trusted with user credentials or access tokens), OpenID Connect flows such as implicit or hybrid flows are preferable. IdentityServer4 and OpenIddict both support those scenarios. One of IdentityServer4’s maintainers (Dominick Baier) has a good blog post on when different flows should be used and IdentityServer4 quickstarts include a sample of using the implicit flow.
As we walk through this scenario, I’d also encourage you to check out IdentityServer4 documentation, as it gives more detail than I can fit into this (relatively) short post.
Getting Started
As before, my first step is to create a new ASP.NET Core web app from the ‘web application’ template, making sure to select “Individual User Accounts” authentication. This will create an app that uses ASP.NET Core Identity to manage users. An Entity Framework Core context will be auto-generated to manage identity storage. The connection string in appsettings.json points to the database where this data will be stored.
Because it’s interesting to understand how IdentityServer4 includes role and claim information in its tokens, I also seed the database with a couple roles and add a custom property (OfficeNumber
) to my ApplicationUser
type which can be used as a custom claim later.
These initial steps of setting up an ASP.NET Core application with identity are identical to what I did in my previously with OpenIddict, so I won’t go into great detail here. If you would like this setup explained further, please see my previous post.
Adding IdentityServer4
Now that our base ASP.NET Core application is up and running (with Identity services), we’re ready to add IdentityServer4 support.
- Add
"IdentityServer4": "1.0.2"
as a dependency in the app’s project.json file. - Add IdentityServer4 to the HTTP request processing pipeline with a call to
app.UseIdentityServer()
in the app’sStartup.Configure
method.- It’s important that the
UseIdentityServer()
call come after registering ASP.NET Core Identity (app.UseIdentity()
).
- It’s important that the
- Register IdentityServer4 services in the
Startup.ConfigureServices
method by callingservices.AddIdentityServer()
.
We’ll also want to specify how IdentityServer4 should sign tokens. During development, an auto-generated certificate can be used to sign tokens by calling AddTemporarySigningCredential
after the call to AddIdentityServer
in Startup.ConfigureServices
. Eventually, we’ll want to use a real cert for signing, though. We can sign with an x509 certificate by calling AddSigningCredential
:
services.AddIdentityServer()
// .AddTemporarySigningCredential() // Can be used for testing until a real cert is available
.AddSigningCredential(new X509Certificate2(Path.Combine(".", "certs", "IdentityServer4Auth.pfx")))
Note that you should not load the certificate from the app path in production; there are other AddSigningCredential
overloads that can be used to load the certificate from the machine’s certificate store.
As mentioned in my previous post, it’s possible to create self-signed certificates for testing this out with the makecert
and pvk2pfx
command line tools (which should be on the path in a Visual Studio Developer Command prompt).
makecert -n "CN=AuthSample" -a sha256 -sv IdentityServer4Auth.pvk -r IdentityServer4Auth.cer
- This will create a new self-signed test certificate with its public key in IdentityServer4Auth.cer and it’s private key in IdentityServer4Auth.pvk.
pvk2pfx -pvk IdentityServer4Auth.pvk -spc IdentityServer4Auth.cer -pfx IdentityServer4Auth.pfx
- This will combine the pvk and cer files into a single pfx file containing both the public and private keys for the certificate. Our app will use the private key from the pfx to sign tokens. Make sure to protect this file. The .cer file can be shared with other services for the purpose of signature validation.
Token issuance from IdentityServer4 won’t yet be functional, but this is the skeleton of how IdentityServer4 is connected to our ASP.NET Core app.
Configuring IdentityServer4
Before IdentityServer4 will function, it must be configured. This configuration (which is done in ConfigureServices
) allows us to specify how users are managed, what clients will be connecting, and what resources/scopes IdentityServer4 is protecting.
Specify protected resources
IdentityServer4 must know what scopes can be requested by users. These are defined as resources. IdentityServer4 has two kinds of resources:
- API resources represent some protected data or functionality which a user might gain access to with an access token. An example of an API resource would be a web API (or set of APIs) that require authorization to call.
- Identity resources represent information (claims) which are given to a client to identify a user. This could include their name, email address, or other claims. Identity information is returned in an ID token by OpenID Connect flows. In our simple sample, we’re using an OAuth 2.0 flow (the password resource flow), so we won’t be using identity resources.
The simplest way to specify resources is to use the AddInMemoryApiResources
and AddInMemoryIdentityResources
extension methods to pass a list of resources. In our sample, we do that by updating our services.AddIdentityServer()
call to read as follows:
services.AddIdentityServer()
// .AddTemporarySigningCredential() // Can be used for testing until a real cert is available
.AddSigningCredential(new X509Certificate2(Path.Combine(".", "certs", "IdentityServer4Auth.pfx")))
.AddInMemoryApiResources(MyApiResourceProvider.GetAllResources()); // <- THE NEW LINE
The MyApiResourceProvider.GetAllResources()
method just returns an IEnumerable
of ApiResources.
return new[]
{
// Add a resource for some set of APIs that we may be protecting
// Note that the constructor will automatically create an allowed scope with
// name and claims equal to the resource's name and claims. If the resource
// has different scopes/levels of access, the scopes property can be set to
// list specific scopes included in this resource, instead.
new ApiResource(
"myAPIs", // Api resource name
"My API Set #1", // Display name
new[] { JwtClaimTypes.Name, JwtClaimTypes.Role, "office" }) // Claims to be included in access token
};
If we also needed identity resources, they could be added with a similar call to AddInMemoryIdentityResources
.
If more flexibility is needed in specifying resources, this can be accomplished by registering a custom IResourceStore
with ASP.NET Core’s dependency injection. An IResourceStore
allows for finer control over how resources are created, allowing a developer to read resource information from an external data source, for example. An IResourceStore
which works with EntityFramework.Core (IdentityServer4.EntityFramework.Stores.ResourceStore
) is available in the IdentityServer4.EntityFramework package.
Specify Clients
In addition to specifying protected resources, IdentityServer4 must be configured with a list of clients that will be requesting tokens. Like configuring resources, client configuration can be done with an extension method: AddInMemoryClients
. Also like configuring resources, it’s possible to have more control over the client configuration by implementing our own IClientStore
. In this sample, a simple call to AddInMemoryClients
would suffice to configure clients, but I opted to use an IClientStore
to demonstrate how easy it is to extend IdentityServer4 in this way. This would be a useful approach if, for example, client information was read from an external database. And, as with IResourceStore
, you can find a ready-made IClientStore
implementation for working with EntityFramework.Core in the IdentityServer4.EntityFramework package.
The IClientStore
interface only has a single method (FindClientByIdAsync
) which is used to look up clients given a client ID. The returned object (of type Client
) contains, among other things, information about the client’s name, allowed grant types and scopes, token lifetimes, and the client secret (if it has one).
In my sample, I added the following IClientStore
implementation which will yield a single client configured to use the resource owner password flow and our custom ‘myAPIs’ resource:
public class CustomClientStore : IClientStore
{
public static IEnumerable<Client> AllClients { get; } = new[]
{
new Client
{
ClientId = "myClient",
ClientName = "My Custom Client",
AccessTokenLifetime = 60 * 60 * 24,
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
RequireClientSecret = false,
AllowedScopes =
{
"myAPIs"
}
}
};
public Task<Client> FindClientByIdAsync(string clientId)
{
return Task.FromResult(AllClients.FirstOrDefault(c => c.ClientId == clientId));
}
}
I then registered the store with ASP.NET Core dependency injection (services.AddSingleton<IClientStore, CustomClientStore>()
in Startup.ConfigureServices
).
Connecting IdentityServer4 and ASP.NET Core Identity
To use ASP.NET Core Identity, we’ll be using the IdentityServer4.AspNetIdentity
package. After adding this package to our project.json, the previous app.AddIdentityServer()
call in Startup.ConfigureServices
can be updated to look like this:
services.AddIdentityServer()
// .AddTemporarySigningCredential() // Can be used for testing until a real cert is available
.AddSigningCredential(new X509Certificate2(Path.Combine(".", "certs", "IdentityServer4Auth.pfx")))
.AddInMemoryApiResources(MyApiResourceProvider.GetAllResources())
.AddAspNetIdentity<ApplicationUser>(); // <- THE NEW LINE
This will cause IdentityServer4 to get user profile information from our ASP.NET Core Identity context, and will automatically setup the necessary IResourceOwnerPasswordValidator
for validating credentials. It will also configure IdentityServer4 to correctly extract JWT subject, user name, and role claims from ASP.NET Core Identity entities.
Putting it Together
With configuration done, IdentityServer4 should now work to serve tokens for the client we defined. The registering of IdentityServer4 services in Startup.ConfigureServices
ends up looking like this all together:
// Add IdentityServer services
services.AddSingleton<IClientStore, CustomClientStore>();
services.AddIdentityServer()
// .AddTemporarySigningCredential() // Can be used for testing until a real cert is available
.AddSigningCredential(new X509Certificate2(Path.Combine(".", "certs", "IdentityServer4Auth.pfx")))
.AddInMemoryApiResources(MyApiResourceProvider.GetAllResources())
.AddAspNetIdentity<ApplicationUser>();
As before, a tool like Postman can be used to test out the app. The scope we specify in the request should be our custom API resource scope (‘myAPIs’).
Here is a sample token request:
POST /connect/token HTTP/1.1
Host: localhost:5000
Cache-Control: no-cache
Postman-Token: 958df72b-663c-5638-052a-aed41ba0dbd1
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=Mike%40Contoso.com&password=MikesPassword1!&client_id=myClient&scope=myAPIs
The returned access token in our app’s response (which can be decoded using online utilities) looks like this:
{
alg: "RS256",
kid: "671A47CE65E10A98BB86EDCD5F9684E9D048FAE9",
typ: "JWT",
x5t: "ZxpHzmXhCpi7hu3NX5aE6dBI-uk"
}.
{
nbf: 1481054282,
exp: 1481140682,
iss: "http://localhost:5000",
aud: [
"http://localhost:5000/resources",
"myAPIs"
],
client_id: "myClient",
sub: "f6435683-f81c-4bd4-9c14-c7c09b236f4e",
auth_time: 1481054281,
idp: "local",
name: "Mike@Contoso.com",
role: "Administrator",
office: "300",
scope: [
"myAPIs"
],
amr: [
"pwd"
]
}.
[signature]
You can read more details about how to understand the JWT fields in my previous post. Note that there are a few small differences between the tokens generated with OpenIddict and those generated with IdentityServer4.
- IdentityServer4 includes the amr (authentication method references) field which lists authentication methods used.
- IdentityServer4 always requires a client be specified in token requests, so it will always have a client_id in the response whereas OpenIddict treats the client as optional for some OAuth 2.0 flows.
- IdentityServer4 does not include the optional iat field indicating when the access token was issued, but does include the auth_time field (defined by OpenID Connect as an optional field for OAuth 2.0 flows) which will have the same value.
In both cases, it’s possible to customize claims that are returned for given resources/scopes, so developers can make sure claims important to their scenarios are included.
Conclusion
Hopefully this walkthrough of a simple IdentityServer4 scenario is useful for understanding how that package can be used to enable authentication token issuance in ASP.NET Core. Please be sure to check out the IdentityServer4 docs for more complete documentation. As IdentityServer4 is not a Microsoft-owned library, support questions or issue reports should be directed to IdentityServer or the IdentityServer4 GitHub repository.
The scenario implemented here is no different from what was covered previously, but serves as an example of how different community-driven libraries can work to solve a given problem. One of the most exciting aspects of .NET Core is the tremendous community involvement we’ve seen in producing high-quality libraries to extend what can be done with .NET Core and ASP.NET Core. I think token-based authentication is a great example of that.
I have checked in sample code that shows the end product of the walk-through in this blog. Reviewing that repository may be helpful in clarifying any remaining questions.
No comments:
Post a Comment