Implementing simple token authentication in ASP.NET Core with OpenIddict

Introduction

Last year, Mike Rousos posted a great post about token authentication on the .NET blog and demonstrated how you could leverage ASP.NET Core Identity and OpenIddict to create your own tokens in a completely standard way.

Since then, many people emailed me to know if using ASP.NET Core Identity was really mandatory. Good news! While the first OpenIddict alpha bits were tied to Identity, the two have been completely decoupled as part of OpenIddict beta1 and beta2. Concretely, this means you can now use OpenIddict with your own authentication method or your own membership stack.


Get started

Register the aspnet-contrib feed

Before coding, you'll have to register the aspnet-contrib MyGet feed, where the preliminary OpenIddict beta bits are currently hosted. For that, create a new NuGet.config at the root of your solution:

NuGet.config
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="aspnet-contrib" value="https://www.myget.org/F/aspnet-contrib/api/v3/index.json" />
</packageSources>
</configuration>

Update your .csproj file to reference the OpenIddict packages

For this demo, you'll need to reference 4 packages:

  • AspNet.Security.OAuth.Validation, that provides the authentication middleware needed to validate the access tokens issued by OpenIddict.
  • OpenIddict, that references the OpenID Connect server middleware and provides the logic required to validate token requests.
  • OpenIddict.EntityFrameworkCore, that contains the default EntityFramework stores.
  • OpenIddict.Mvc, that provides an ASP.NET Core MVC binder allowing to use OpenIdConnectRequest as an action parameter.
1
2
3
4
5
6
7
8
9
10
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="1.0.0" />
<PackageReference Include="OpenIddict" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.Mvc" Version="1.0.0-*" />
</ItemGroup>
</Project>

Add OpenIddict in the ASP.NET Core application

Register the OpenIddict services in the dependency injection container

Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store.
options.UseInMemoryDatabase();
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
services.AddOpenIddict(options =>
{
// Register the Entity Framework stores.
options.AddEntityFrameworkCoreStores<DbContext>();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.AddMvcBinders();
// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");
// Enable the password flow.
options.AllowPasswordFlow();
// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
});
}
}

Register OpenIddict and the validation middleware in the ASP.NET Core pipeline

Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// Register the validation middleware, that is used to decrypt
// the access tokens and populate the HttpContext.User property.
app.UseOAuthValidation();
// Register the OpenIddict middleware.
app.UseOpenIddict();
app.UseMvcWithDefaultRoute();
}
}

Make sure to always register the validation middleware very early in your pipeline: if the validation middleware is not at the right place, requests won't be correctly authenticated when reaching the next middleware (e.g MVC).

The same remark applies to OpenIddict, that must be inserted before MVC to validate token requests before they reach your own code. If you don't register it correctly, an exception will be thrown at runtime.


Create your own token authentication controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class AuthorizationController : Controller
{
[HttpPost("~/connect/token"), Produces("application/json")]
public IActionResult Exchange(OpenIdConnectRequest request)
{
if (request.IsPasswordGrantType())
{
// Validate the user credentials.
// Note: to mitigate brute force attacks, you SHOULD strongly consider
// applying a key derivation function like PBKDF2 to slow down
// the password validation process. You SHOULD also consider
// using a time-constant comparer to prevent timing attacks.
if (request.Username != "alice@wonderland.com" ||
request.Password != "P@ssw0rd")
{
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIdConnectServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
// Add a "sub" claim containing the user identifier, and attach
// the "access_token" destination to allow OpenIddict to store it
// in the access token, so it can be retrieved from your controllers.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject,
"71346D62-9BA5-4B6D-9ECA-755574D628D8",
OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, "Alice",
OpenIdConnectConstants.Destinations.AccessToken);
// ... add other claims, if necessary.
var principal = new ClaimsPrincipal(identity);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(principal, OpenIdConnectServerDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
}

Create an API controller

1
2
3
4
5
6
7
8
9
10
11
12
public class ApiController : Controller
{
[Authorize, HttpGet("~/api/test")]
public IActionResult GetMessage()
{
return Json(new
{
Subject = User.GetClaim(OpenIdConnectConstants.Claims.Subject),
Name = User.Identity.Name
});
}
}

Test your ASP.NET Core application

Retrieve an access token from your authentication controller

To retrieve an access token, send a POST request to /connect/token with the grant_type=password parameter and the user credentials:

token-request.png
1
2
3
4
5
POST /connect/token HTTP/1.1
Host: localhost:7096
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=alice%40wonderland.com&password=P%40ssw0rd

If the credentials are valid, you'll get a JSON response containing the access token:

1
2
3
4
5
{
"token_type": "Bearer",
"access_token": "CfDJ8Ec0ZpniaHhGg0e0UUvOH9BWZSGrPoEwGd0_Lq2cse-T29YOq985IBiT5fEe5tTSgY1vxq2Z2ZJ7Ikwlpmh0Lrc4x9pqhqHBziUzsP_rkGZkn47TkNkOkzKCwZJZK5x-irH3HROwClFFTq0rgWdb8rZ2xriffNzsby4VwhxhN5soFD435KzmVYkdv-VuaLYo3QiSuexbRi2USVO9LK30vomAG6h2SAxZ7R-jYsXgf0f5gAmdYxg7w3yicv9v8DpUSBiGGRRfymTOnvGEsFJjGuuP8OlY5qzMs6wGaRWkOvCyV2CK_RZF_3TMs7LYCdMQ-dqWY5A03-03OmP8blKzlrKJMDZfrPQHuysbS931xxy8b3kjicfjNLmMHqzQzbUO4fecm4kY8PFnKozojDtqajfTp2bYhxS65bmVYROrswYeUWEKYR6LSdS1K__IDaLoMlLa-Wf6x1wjM2CchzgqbHRF0KEtdL5Ks88dAS44mp9BM6iUOEWyL7VkbazsBdlNciM5ZZB1_6qunufDW_tcaR8",
"expires_in": 3600
}

Query your API controller using a bearer token

To send an authenticated request, simply attach the bearer token to the Authorization header using the following syntax: Authorization: Bearer [your bearer token] (without the square brackets)

api-request.png
1
2
3
GET /api/test HTTP/1.1
Host: localhost:7096
Authorization: Bearer CfDJ8Ec0ZpniaHhGg0e0UUvOH9BWZSGrPoEwGd0_Lq2cse-T29YOq985IBiT5fEe5tTSgY1vxq2Z2ZJ7Ikwlpmh0Lrc4x9pqhqHBziUzsP_rkGZkn47TkNkOkzKCwZJZK5x-irH3HROwClFFTq0rgWdb8rZ2xriffNzsby4VwhxhN5soFD435KzmVYkdv-VuaLYo3QiSuexbRi2USVO9LK30vomAG6h2SAxZ7R-jYsXgf0f5gAmdYxg7w3yicv9v8DpUSBiGGRRfymTOnvGEsFJjGuuP8OlY5qzMs6wGaRWkOvCyV2CK_RZF_3TMs7LYCdMQ-dqWY5A03-03OmP8blKzlrKJMDZfrPQHuysbS931xxy8b3kjicfjNLmMHqzQzbUO4fecm4kY8PFnKozojDtqajfTp2bYhxS65bmVYROrswYeUWEKYR6LSdS1K__IDaLoMlLa-Wf6x1wjM2CchzgqbHRF0KEtdL5Ks88dAS44mp9BM6iUOEWyL7VkbazsBdlNciM5ZZB1_6qunufDW_tcaR8

If the access token is valid, you'll get a JSON payload containing the user details returned by the API:

1
2
3
4
{
"subject": "71346D62-9BA5-4B6D-9ECA-755574D628D8",
"name": "Alice"
}

And voilà, that's done.

Got a question? Feel free to post a comment or open a new thread on StackOverflow.