This post is the fourth part of a series of blog posts entitled Creating your own OpenID Connect server with ASOS:
- Introduction
- Choosing the right flow(s)
- Registering the middleware in the ASP.NET Core pipeline
- Creating your own authorization provider
- Implementing the resource owner password credentials grant
- Implementing the authorization code and implicit flows
- Adding custom claims and granting scopes
- Testing your authorization server with Postman
- Conclusion
ASOS leverages the same events model as the rest of the ASP.NET Core security stack: often hard to understand for beginners, this pattern (inherited from OWIN/Katana) proved to be extremely powerful by offering full flexibility on the request processing.
To help make things clearer before trying to implement a concrete flow, here's a quick overview of how it works with ASOS:
OpenIdConnectServerProvider
and the events model
OpenIdConnectServerProvider
is ASOS' main extensibility hook: its methods (named events or notifications) are invoked by OpenIdConnectServerHandler
for every OpenID Connect request to give you a chance to control how the request is handled. Depending on the flows you want to support, you'll need to implement different events.
You have 2 options to create your own provider:
- Directly instantiante an
OpenIdConnectServerProvider
and use inline delegates. This approach is perfect when implementing a simple server that mainly relies on hardcoded values.
1 | app.UseOpenIdConnectServer(options => |
You can also directly set the events properties without having to manually instantiate a OpenIdConnectServerProvider
, as ASOS always registers a default OpenIdConnectServerProvider
instance for you:
1 | app.UseOpenIdConnectServer(options => |
- Create your own subclass of
OpenIdConnectServerProvider
and override the virtual methods you want to implement. This is clearly the best approach when implementing a more complex authorization server.
1 | public sealed class AuthorizationProvider : OpenIdConnectServerProvider |
1 | app.UseOpenIdConnectServer(options => |
It's important to note that the authorization provider is always a singleton: don't try to inject scoped dependencies in its constructor. To resolve scoped dependencies (e.g an Entity Framework DbContext
), use the context.HttpContext.RequestServices
property to access the scoped container.
You can read this thread for more information about this limitation/design choice, which is not specific to ASOS and impacts all the security middleware sharing the same events model. It might be fixed in a future version, though.
Working with the different categories of events
ASOS has 5 different categories of events:
- The events called to extract or restore an OpenID Connect request from an HTTP request (e.g
ExtractAuthorizationRequest
). - The events responsible for validating requests (e.g
ValidateAuthorizationRequest
). - The events handling requests (e.g
HandleAuthorizationRequest
). - The events that can be used to alter or replace the response before it is returned to the caller (e.g
ApplyAuthorizationResponse
). - The events in charge of serializing and deserializing tokens (e.g
SerializeAccessToken
).
Request extraction events
Immediately after validating the HTTP method and extracting the request parameters from the query string or from the request form (depending on the endpoint type), ASOS invokes one of the Extract*Request
events to give you a chance to manually replace, restore or alter the request before it is validated.
For instance, ExtractAuthorizationRequest
can be used to restore an OpenID Connect authorization request from the user session, which can be useful if you need to save POST or large GET authorization requests before redirecting the user to an external provider:
1 | public override Task ExtractAuthorizationRequest(ExtractAuthorizationRequestContext context) |
Another concrete use case is when you have to support non-standard clients that don't send the parameters required by the OAuth 2.0/OIDC specifications, as ExtractAuthorizationRequest
can be used to remove, replace or even add a missing parameter before ASOS starts validating the request:
1 | public override async Task ExtractAuthorizationRequest(ExtractAuthorizationRequestContext context) |
Request validation events
Implementing validation events is generally required to allow ASOS to process OpenID Connect requests. It's particularly true with ValidateAuthorizationRequest
and ValidateTokenRequest
, that must be implemented to support interactive and non-interactive flows.
To allow full flexibility, ASOS always gives you 2 or 3 options, depending on the exact event you're implementing:
- Validate the request: it's typically what you'll want to do after checking that the request was fully valid (e.g the client application was allowed to use the requested grant type and its client credentials were valid).
1 | context.Validate(); |
When implementing ValidateTokenRequest
, context.Validate()
shouldn't be called for public applications like JS, mobile or desktop apps. If you want to make client authentication optional, consider using context.Skip()
instead, as explained below.
- Reject the request: when the request doesn't meet your specific requirements (e.g the client credentials are missing or invalid), you can reject it with an error code and a description explaining why the request was rejected.
1 | context.Reject( |
- Skip validation: under certain circumstances, ASOS allows you to skip request validation. Calling
context.Skip()
informs ASOS that the request was not fully validated (e.g because the client credentials were missing) but should be accepted nevertheless.
1 | context.Skip(); |
At the time of writing, only ValidateIntrospectionRequest
, ValidateRevocationRequest
and ValidateTokenRequest
allow using context.Skip()
, to make client authentication optional.
Though particularly useful when using the resource owner password credentials grant with JS applications, that's something you should avoid when dealing with confidential applications using the authorization code flow, as it drastically reduces the overall security level.
Here's an example of how ValidateTokenRequest
can be implemented to reject specific grant types while allowing all your client applications to use the token endpoint without having to authenticate:
1 | public override Task ValidateTokenRequest(ValidateTokenRequestContext context) |
More samples can be found in the next part, that explains how to implement the ValidateTokenRequest
event to support the resource owner password credentials grant with different scenarios.
Request handling events
Implementing these events is generally not required, but can be useful to control how ASOS handles a request. Similarly to what the security middleware built in ASP.NET Core offer, you have 3 options to control the request processing:
- Let ASOS determine how the request will be processed: in most cases, you'll simply want to add your own logic determining what will be returned to the caller and let ASOS handle the rest of the request. For instance, you may want to implement the
HandleUserinfoRequest
event to update, augment, replace or remove the default claims returned by the userinfo endpoint:
1 | public override Task HandleUserinfoRequest(HandleUserinfoRequestContext context) |
- Handle the request manually: by calling
context.HandleRequest()
, you can inform ASOS that its default logic should not be executed and that the request should terminate immediately after invoking your event handler. In doing so, you take full control over the response: you can return a custom status code, render a HTML page or even send back a JSON payload by directly writing to the HTTP response stream.
Here's an example implementing HandleAuthorizationRequest
to immediately return a token to the client application without displaying a consent page or relying on ASP.NET Core MVC to render it (if the user is not already logged in, ChallengeAsync
is immediately called to redirect him/her to Google's authorization endpoint):
1 | public override async Task HandleAuthorizationRequest(HandleAuthorizationRequestContext context) |
- Skip the default logic and delegate the request handling to the next middleware in the pipeline: when calling
context.SkipToNextMiddleware()
, ASOS is informed that the default request processing should not be applied.
Unlike context.HandleResponse()
, context.SkipToNextMiddleware()
doesn't immediately stop the request processing. Instead, the next middleware in the pipeline (i.e all the middleware registered after app.UseOpenIdConnectServer()
) are invoked to give them a chance to handle the request.
A common use case is when you want to handle the userinfo request in your own API controller instead of handling it at the middleware level:
1 | public override Task HandleUserinfoRequest(HandleUserinfoRequestContext context) |
1 | public class UserinfoController : Controller |
Response events
Similarly to how the request handling events work, the Apply*Response
events give you a chance to control how the OpenID Connect responses are serialized and applied just before they are returned to the caller: you can call context.HandleResponse()
to inform ASOS that the response should be processed using your own logic or context.SkipToNextMiddleware()
to bypass the default response logic and to invoke the next middleware.
Here's an implementation of ApplyTokenResponse
that adds a custom parameter to the token response before returning it:
Note that this practice is usually discouraged when using it as a way to flow user attributes. Instead, consider storing them as claims in the identity token.
1 | public override Task ApplyTokenResponse(ApplyTokenResponseContext context) |
Next part: Implementing the resource owner password credentials grant.