This post is the fourth part of a series of blog posts entitled Creating your own OpenID Connect server with ASOS:
- 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
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 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
OpenIdConnectServerProviderand use inline delegates. This approach is perfect when implementing a simple server that mainly relies on hardcoded values.
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:
- Create your own subclass of
OpenIdConnectServerProviderand override the virtual methods you want to implement. This is clearly the best approach when implementing a more complex authorization server.
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.
ASOS has 5 different categories of events:
- The events called to extract or restore an OpenID Connect request from an HTTP request (e.g
- The events responsible of validating requests (e.g
- The events handling requests (e.g
- The events that can be used to alter or replace the response before it is returned to the caller (e.g
- The events in charge of serializing and deserializing tokens (e.g
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.
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:
Another concrete use case is when you have to support non-standard clients that don't send the parameters required by the OAuth2/OIDC specifications, as
ExtractAuthorizationRequest can be used to remove, replace or even add a missing parameter before ASOS starts validating the request:
Implementing validation events is generally required to allow ASOS to process OpenID Connect requests. It's particularly true with
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).
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.
- 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.
At the time of writing, only
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:
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.
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
HandleUserinfoRequestevent to update, augment, replace or remove the default claims returned by the userinfo endpoint:
- 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):
- 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.
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:
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.