Implementing advanced scenarios using the new OpenIddict RC3 events model

Prior to OpenIddict RC3, the events model used by the OpenID Connect server middleware (i.e the OIDC server framework behind OpenIddict) was deliberately not accessible due to the nature of OpenIddict: being initially designed for non-experts, exposing such a powerful API – that allows altering the way OpenID Connect requests are processed – didn't seem like a good idea at first sight.

With time, the core audience of OpenIddict has evolved a bit to not only include beginners but also developers who were already familiar with OAuth/OpenID Connect or had used OAuthAuthorizationServerMiddleware or ASOS in the past. For them, the fact OpenIddict didn't allow them to take control of the request processing pipeline was often a blocker. With the introduction of OpenIddict RC3, we're changing that.

Using these advanced APIs is not recommended if you're not familiar with the OAuth/OpenID Connect specifications or with the events model used by the OpenID Connect server middleware. If you're not sure whether you should use these APIs, don't hesitate to reach us on Gitter or on GitHub.

Introducing event handlers

The events model is structured around IOpenIddictServerEventHandler<TEvent> and IOpenIddictValidationEventHandler<TEvent>. These 2 interfaces represent handlers that are invoked every time an event of type TEvent is triggered by the OpenIddict server or validation handlers.

At the time of writing, OpenIddict exposes 44 server events – grouped into the OpenIddictServerEvents static class to make them easier to find – and 5 validation events, exposed under OpenIddictValidationEvents.

Each event represents a specific moment in the request processing pipeline (e.g the moment the OpenIddict server determines whether the request is an OpenID Connect request it should handle, the moment it extracts it, handles it or returns a response).

Multiple handlers of the same type can be registered: they will be sequentially invoked in the same order as the one used to register them. As soon as a handler calls a method that indicates the request should no longer be processed (e.g HandleResponse() or SkipHandler()), OpenIddict will stop invoking the handlers and the next ones will be automatically ignored.

For security reasons, the custom handlers will be invoked by OpenIddict after its own validation routines. If a request is rejected by OpenIddict, your own handlers won't be invoked.

Creating and registering a custom event handler

Creating an event handler is straightforward: pick the event you need and add a class that implements either IOpenIddictServerEventHandler<TEvent> (for a handler that receives events triggered by the OpenIddict server services) or IOpenIddictValidationEventHandler<TEvent> (for a handler that receives events triggered by the OpenIddict token validation services):

For instance, to return custom metadata in the discovery document, you'll need to implement IOpenIddictServerEventHandler<OpenIddictServerEvents.HandleConfigurationRequest>:

1
2
3
4
5
6
7
8
9
public class MyEventHandler : IOpenIddictServerEventHandler<OpenIddictServerEvents.HandleConfigurationRequest>
{
public Task HandleAsync(OpenIddictServerEvents.HandleConfigurationRequest notification, CancellationToken cancellationToken)
{
notification.Context.Metadata["company_name"] = "Contoso";

return Task.CompletedTask;
}
}

To register it, use options.AddEventHandler<TEvent, THandler>() (by default, the handler is registered as a scoped service):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void ConfigureServices(IServiceCollection services)
{
services.AddOpenIddict()

// Register the OpenIddict core services.
.AddCore(options =>
{
// ...
})

// Register the OpenIddict server handler.
.AddServer(options =>
{
// ...

options.AddEventHandler<OpenIddictServerEvents.HandleConfigurationRequest, MyEventHandler>();
})

// Register the OpenIddict validation handler.
.AddValidation();
}

Alternatively, if your handler can be trivialy implemented and doesn't use constructor injection, you can register it inline:

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
public void ConfigureServices(IServiceCollection services)
{
services.AddOpenIddict()

// Register the OpenIddict core services.
.AddCore(options =>
{
// ...
})

// Register the OpenIddict server handler.
.AddServer(options =>
{
// ...

options.AddEventHandler<OpenIddictServerEvents.HandleConfigurationRequest>(
notification =>
{
notification.Context.Metadata["company_name"] = "Contoso";

return Task.CompletedTask;
});
})

// Register the OpenIddict validation handler.
.AddValidation();
}

Concrete examples

Tweaking the endpoint detection logic

By default, OpenIddict uses a path-based endpoint resolution logic to determine whether the incoming request is an OpenID Connect request it should handle. This is done by comparing the request path to the endpoint paths registered in the OpenIddict server options. In some cases, you'll probably want to listen on multiple paths at the same time instead of a single one. For that, you can use the OpenIddictServerEvents.MatchEndpoint event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
options.AddEventHandler<OpenIddictServerEvents.MatchEndpoint>(notification =>
{
// By default, only requests sent to /connect/token will be treated as valid
// token requests by OpenIddict. This custom logic allows requests pointing
// to /connect/second-token-endpoint to be treated the same way so that
// requests can be sent to one of the two addresses without any distinction.
var request = notification.Context.HttpContext.Request;
if (request.Path == "/connect/second-token-endpoint")
{
notification.Context.MatchTokenEndpoint();
}

return Task.CompletedTask;
});

Returning the list of supported social providers as part of the discovery document

If you need to expose the external providers that are supported by your server application, you can use the HandleConfigurationRequest event:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyEventHandler : IOpenIddictServerEventHandler<OpenIddictServerEvents.HandleConfigurationRequest>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

public MyEventHandler(IAuthenticationSchemeProvider authenticationSchemeProvider)
=> _authenticationSchemeProvider = authenticationSchemeProvider;

public async Task HandleAsync(OpenIddictServerEvents.HandleConfigurationRequest notification, CancellationToken cancellationToken)
=> notification.Context.Metadata["external_providers_supported"] = new JArray(
from provider in await _authenticationSchemeProvider.GetAllSchemesAsync()
where !string.IsNullOrEmpty(provider.DisplayName)
select provider.Name);
}
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
{
"issuer": "https://localhost:44344/",
"token_endpoint": "https://localhost:44344/connect/token",
"jwks_uri": "https://localhost:44344/.well-known/jwks",
"grant_types_supported": [
"password",
"refresh_token"
],
"scopes_supported": [
"openid",
"offline_access"
],
"claims_supported": [
"aud",
"exp",
"iat",
"iss",
"jti",
"sub"
],
"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,
"external_providers_supported": [
"Google",
"Microsoft",
"Facebook"
]
}

Implementing the token endpoint at the handler level without having an authorization controller

With OpenIddict, the "standard" way to process authorization or token requests is to have an authorization controller dedicated to handling these requests. Starting with RC3, this can also be done directly at the OpenIddict server handler level (e.g for those who don't need or don't want to use ASP.NET Core MVC).

Here's how it could be done by writing an OpenIddictServerBuilder extension:

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
public static class CustomOpenIddictServerExtensions
{
public static OpenIddictServerBuilder UseCustomTokenEndpoint(
this OpenIddictServerBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

return builder.AddEventHandler<OpenIddictServerEvents.HandleTokenRequest>(
notification =>
{
var request = notification.Context.Request;
if (!request.IsPasswordGrantType())
{
return Task.CompletedTask;
}

// 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")
{
notification.Context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified credentials are invalid.");

return Task.CompletedTask;
}

// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
notification.Context.Scheme.Name,
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);
notification.Context.Validate(principal);

return Task.CompletedTask;
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void ConfigureServices(IServiceCollection services)
{
services.AddOpenIddict()

// Register the OpenIddict core services.
.AddCore(options =>
{
// ...
})

// Register the OpenIddict server handler.
.AddServer(options =>
{
// ...

options.UseCustomTokenEndpoint();
})

// Register the OpenIddict validation handler.
.AddValidation();
}

Extracting access tokens from the query string

In some cases, flowing the access token in the HTTP request headers is not possible (e.g when using WebSockets with JS clients). To work around these limitations, you can transfer it as a query string parameter and configure the OpenIddict validation handler to use your extraction logic by adding an event handler for OpenIddictValidationEvents.RetrieveToken:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void ConfigureServices(IServiceCollection services)
{
services.AddOpenIddict()

// Register the OpenIddict validation handler.
.AddValidation(options =>
{
// ...

options.AddEventHandler<OpenIddictValidationEvents.RetrieveToken>(
notification =>
{
notification.Context.Token = notification.Context.Request.Query["access_token"];

return Task.CompletedTask;
});
});
}