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

This post was updated to include code snippets demonstrating how to register OpenIddict in an ASP.NET Core 2.x application. When using OpenIddict in an ASP.NET Core 2.x application, make sure you're referencing the OpenIddict 2.x packages.


Update your .csproj file to reference the OpenIddict packages

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

  • OpenIddict, that references the core services, the token server and the validation services.
  • OpenIddict.EntityFrameworkCore, that contains the Entity Framework Core stores.

ASP.NET Core 1.x

1
2
3
4
5
6
7
8
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<PackageReference Include="OpenIddict" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="1.0.0-*" />
</ItemGroup>

</Project>

ASP.NET Core 2.x

1
2
3
4
5
6
7
8
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<PackageReference Include="OpenIddict" Version="2.0.0-*" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="2.0.0-*" />
</ItemGroup>

</Project>

Register the OpenIddict services in the DI container and the ASP.NET Core pipeline

ASP.NET Core 1.x

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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()

// Register the OpenIddict core services.
.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})

// Register the OpenIddict server handler.
.AddServer(options =>
{
// Register the ASP.NET Core MVC services used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseMvc();

// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");

// Enable the password flow.
options.AllowPasswordFlow();

// Accept anonymous clients (i.e clients that don't send a client_id).
options.AcceptAnonymousClients();

// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
})

// Register the OpenIddict validation handler.
// Note: the OpenIddict validation handler is only compatible with the
// default token format or with reference tokens and cannot be used with
// JWT tokens. For JWT tokens, use the Microsoft JWT bearer handler.
.AddValidation();
}

public void Configure(IApplicationBuilder app)
{
// Register the OpenIddict token validation middleware.
app.UseOpenIddictValidation();

// Register the OpenIddict server middleware.
app.UseOpenIddictServer();

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.

ASP.NET Core 2.x

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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(nameof(DbContext));

// 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()

// Register the OpenIddict core services.
.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})

// Register the OpenIddict server handler.
.AddServer(options =>
{
// Register the ASP.NET Core MVC services used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseMvc();

// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");

// Enable the password flow.
options.AllowPasswordFlow();

// Accept anonymous clients (i.e clients that don't send a client_id).
options.AcceptAnonymousClients();

// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
})

// Register the OpenIddict validation handler.
// Note: the OpenIddict validation handler is only compatible with the
// default token format or with reference tokens and cannot be used with
// JWT tokens. For JWT tokens, use the Microsoft JWT bearer handler.
.AddValidation();

services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictValidationDefaults.AuthenticationScheme;
});
}

public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();

app.UseMvcWithDefaultRoute();
}
}

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(OpenIddictServerDefaults.AuthenticationScheme);
}

// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIddictServerDefaults.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 OAuth 2.0 token response.
return SignIn(principal, OpenIddictServerDefaults.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:

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)

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.