This article is part of a series called Setting up an Authorization Server with OpenIddict. The articles in this series will guide you through the process of setting up an OAuth2 + OpenID Connect authorization server on the the ASPNET Core platform using OpenIddict.
- Part I: Introduction
- Part II: Create ASPNET project
- Part III: Client Credentials Flow
- Part IV: Authorization Code Flow
- Part V: OpenID Connect
-
Part VI: Refresh tokens
robinvanderknaap / authorization-server-openiddict
Authorization Server implemented with OpenIddict.
In this part we will add OpenIddict to the project and implement out first authorization flow: The Client Credentials Flow.
Add OpenIddict packages
First, we need to install the OpenIddict NuGet packages:
dotnet add package OpenIddict
dotnet add package OpenIddict.AspNetCore
dotnet add package OpenIddict.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Besides the main library we also installed the OpenIddict.AspNetCore
package, this package enables the integration of OpenIddict in an ASPNET Core host.
OpenIddict.EntityFrameworkCore
package enables Entity Framework Core support. For now we will work with an in-memory implementation, for which we use the package Microsoft.EntityFrameworkCore.InMemory
.
Setup OpenIddict
We will start with what is minimal required to get OpenIddict up and running. At least one OAuth 2.0/OpenID Connect flow must be enabled. We choose to enable the the Client Credentials Flow, which is suitable for machine-to-machine applications. In the next part of this series we will implement the Authorization Code Flow with PKCE which is the recommended flow for Single Page Applications (SPA) and native/mobile applications.
Start to make the following changes to Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store.
options.UseInMemoryDatabase(nameof(DbContext));
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
options
.AllowClientCredentialsFlow();
options
.SetTokenEndpointUris("/connect/token");
// Encryption and signing of tokens
options
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey();
// Register scopes (permissions)
options.RegisterScopes("api");
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options
.UseAspNetCore()
.EnableTokenEndpointPassthrough();
});
}
First, the DbContext is registered in the ConfigureServices
method. OpenIddict natively supports Entity Framework Core, Entity Framework 6 and MongoDB out-of-the-box, and you can also provide your own stores.
In this example, we will use Entity Framework Core, and we will use an in-memory database. The options.UseOpenIdDict
call registers the entity sets needed by OpenIddict.
Next, OpenIddict itself is registered. The AddOpenIddict()
call registers the OpenIddict services and returns a OpenIddictBuilder
class with which we can configure OpenIddict.
The core components are registered first. OpenIddict is instructed to use Entity Framework Core, and use the aforementioned DbContext.
Next, the server components are registered and the Client Credentials Flow is enabled. For this flow to work, we need to register a token endpoint. We need to implement this endpoint ourselves. We'll do this later.
For OpenIddict to be able to encrypt and sign tokens we need to register two keys, one for encrypting and one for signing. In this example we'll use ephemeral keys. Ephemeral keys are automatically discarded when the application shuts down and payloads signed or encrypted using these key are therefore automatically invalidated. This method should only be used during development. On production, using a X.509 certificate is recommended.
RegisterScopes
defines which scopes (permissions) are supported. In this case we have one scope called api
, but the authorization server can support multiple scopes.
The UseAspNetCore()
call is used to setup AspNetCore as a host for OpenIddict. We also call EnableTokenEndpointPassthrough
otherwise requests to our future token endpoint are blocked.
To check if OpenIddict is properly configured we can start the application and navigate to: https://localhost:5001/.well-known/openid-configuration, the response should be something like this:
{
"issuer": "https://localhost:5001/",
"token_endpoint": "https://localhost:5001/connect/token",
"jwks_uri": "https://localhost:5001/.well-known/jwks",
"grant_types_supported": [
"client_credentials"
],
"scopes_supported": [
"openid",
"api"
],
"claims_supported": [
"aud",
"exp",
"iat",
"iss",
"sub"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"subject_types_supported": [
"public"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_parameter_supported": false,
"request_parameter_supported": false,
"request_uri_parameter_supported": false
}
In this guide, we will be using Postman to test the authorization server, but you could just as easily use another tool.
Below you find an example authorization request using Postman. Grant Type is the Client Credentials Flow. We specify the access token url, a client id and secret to authenticate our client. We also request access to the api
scope.
If we request the token, the operation will fail: The client_id is invalid. This makes sense, because we haven't registered any clients yet with our Authorization Server.
We can create a client by adding it to the database. For that purpose we create a class called TestData
. The test data implements the IHostedService
interface, which enables us to execute the generation of test data in Startup.cs
when the application starts.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Abstractions;
namespace AuthorizationServer
{
public class TestData : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public TestData(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("postman", cancellationToken) is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "postman",
ClientSecret = "postman-secret",
DisplayName = "Postman",
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "api"
}
}, cancellationToken);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
A client is registered in the test data. The client id and secret are used to authenticate the client with the authorization server. The permissions determine what this client's options are.
In this case we allow for the client to use the Client Credentials Flow, access the token endpoint and allow the client to request the api
scope.
Register the test data service in Startup.cs
, so it is executed when the application starts:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHostedService<TestData>();
}
If we try to obtain an access token with Postman again, the request will still fail. This is because we haven't created the token endpoint yet. We'll do that now.
Create a new controller called AuthorizationController
, we will host the endpoint here:
using System;
using System.Security.Claims;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
namespace AuthorizationServer.Controllers
{
public class AuthorizationController : Controller
{
[HttpPost("~/connect/token")]
public IActionResult Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Subject (sub) is a required field, we use the client id as the subject identifier here.
identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException());
// Add some claim, don't forget to add destination otherwise it won't be added to the access token.
identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);
claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
}
else
{
throw new InvalidOperationException("The specified grant type is not supported.");
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
}
One action is implemented, Exchange
. This action is used by all flows, not only the Client Credentials Flow, to obtain an access token.
In the case of the Client Credentials Flow, the token is issued based on the client credentials. In the case of Authorization Code Flow, the same endpoint is used but then to exchange an authorization code for a token. We'll see that in part IV.
For now, we need to focus on the Client Credentials Flow. When the request enters the Exchange
action, the client credentials (ClientId and ClientSecret) are already validated by OpenIddict. So we don't need to authenticate the request, we only have to create a claims principal and use that to sign in.
The claims principal is not the same as we used in the Account controller, that one was based on the Cookie authentication handler and is only used within the context of the Authorization server itself to determine if the user has been authenticated or not.
The claims principal we have to create is based on the OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
. This way, when we call SignIn
at the end of this method, the OpenIddict middleware will handle the sign in and return an access token as response to the client.
The claims defined in the claims principal are included in the access token only when we specify the destination. The some-value
claim in the example will be added to the access token.
The subject
claim is required and you don't need to specify a destination, it will be included in the access token anyway.
We also grant all the requested scopes by calling claimsPrincipal.SetScopes(request.GetScopes());
. OpenIddict has already checked if the requested scopes are allowed (in general and for the current client). The reason why we have to add the scopes manually here is that we are able to filter the scopes granted here if we want to.
One token to rule them all
Let's try to obtain an access token with Postman again, this time it should work.
As of v3 of OpenIddict, the access token is in Jason Web Token (JWT) format by default. This enables us to examine the token with jwt.io. (Thanks Auth0, for hosting that service!)
One problem, the token is not only signed, but also encrypted. OpenIddict encrypts the access token by default. We can disable this encryption when configuring OpenIddict in Startup.cs
:
// Encryption and signing of tokens
options
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption();
Now, when we restart our authorization server and request a new token. Paste the token to jwt.io and view the content of the token:
As you can see the client id postman
is set as subject sub
. Also the some-claim
claim is added to the access token.
Next
Congratulations, you have implemented the Client Credentials Flow
with OpenIddict!
As you may have noticed, the login page is unused by the Client Credentials Flow. This flow exchanges the client credentials for a token immediately, which is suitable for machine-to-machine applications.
Next up, we will implement the Authorization Code Flow with PKCE
, which is the recommended flow for single page applications (SPA) and mobile apps. This flow will involve the user, so our login page will come in to play.
Top comments (10)
Hi,
for client credential flow, code below not lead to include the claim in the access token
identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);
The correct code is as below
identity.AddClaim(new Claim("some-claim2", "some-value2").SetDestinations(OpenIddictConstants.Destinations.AccessToken));
The first syntax not working (tested on OpenIddict V4 with dotnet 6.0)
I wonder could you expand a little on how you got to that "GET NEW ACCESS TOKEN" UI in Postman. I've installed it but never used it and I can't figure out how to follow your instructions from that point. UPDATE I figured it out but others would probably appreciate knowing how to do the test.
Hi Peter,
Here's an article which covers all you need to know about setting authorization headers for Postman requests: learning.postman.com/docs/sending-...
Focus on this part specifically: learning.postman.com/docs/sending-...
Hi Peter Wone,
It is "hidden" in the "Authorization" tab. After you click on it, select "OAuth 2.0" for the "Type" dropdown box.
To illustrate this, here's a simple picture with everything:
I believe this tiny bit should be added to Part III of this guide. It will avoid a HUGE time loss for many. Those already familiar with Postman probably can't tell how important a step this is.
This aside, this is an excellent guide, Robin! Thank you for making it so skillfully, thoughtfully and for keeping it both simple and easy to follow.
The only other suggestion I could give, though, would be to include steps on how to use OpenIddict with NHibernate instead, which is a much more robust, compatible and problem-free ORM than both Entity Framework and EFCore. There's currently no guide for this that I could tell, and since OpenIddict doesn't support it out-of-the-box, it might be a guide in and of itself (a "Part VII" for this guide, if you will).
Thank you!
Hi.
When I finnished up this page I get an error frm JWT.IO saying
Error: Looks like tour JWT payload is not a valid JSON object. JWT payloads must be top level JSON objects as per tools.ietf.org/html/rfc7519#sectio...
Did I do something wrong in my coding or is this a normal behaviour ?
hahaha. And this is what happens when you dont read the complete instructions :D
Simply forgot to add the DisableEncryption method :D
Hands are faster than brains
thanks.
Machine A get token from AuthorizationServer, and try to send a request to machine B , how Machine B should connect to AuthorizationServer and validate the incoming token ?!
both Machine A and Machine B are Asp.Net Core app
Hi Reza,
That process is called introspection, where machine B asks the Authorization Server to validate the token.
First you need to set the introspection endpoint when setting up the Authorization Server:
You also need to give permission to Machine B (client) to use the introspection endpoint:
Regards,
Robin
thanks