This post is the fifth 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
Implementing the resource owner password credentials grant (abbreviated ROPC
for brevity) is quite easy with ASOS as the only thing you have to do is to provide your own implementation of ValidateTokenRequest
and HandleTokenRequest
.
But to properly implement these events, you first need to determine what's the best client authentication policy for your application.
Implementing ValidateTokenRequest
to validate the grant type and the client application credentials
When implementing flows using backchannel communication (i.e resource owner password credentials grant, client credentials grant, authorization code flow or refresh token grant), the ValidateTokenRequest
event must be overridden to validate the token request.
So, what are you supposed to validate in this event? Mainly two things:
- The grant type: in most cases, you'll likely want to restrict the grants a client application is allowed to use (e.g resource owner password credentials only):
ValidateTokenRequest
is the best place for that.
It should be noted that ASOS doesn't validate the grant_type
value, that can even contain a custom value for extension grants: if you only want to support standard grants, it's up to you to reject the token request by calling context.Reject()
.
IsAuthorizationCodeGrantType()
, IsRefreshTokenGrantType()
, IsPasswordGrantType()
and IsClientCredentialsGrantType()
can be used for this exact purpose.
- The client credentials (
client_id
/client_secret
): the OAuth 2.0 specification explicitly states that confidential applications (i.e applications that are able to keep their credentials secret, like server-side apps) must authenticate when using the token endpoint. This security measure is extremely important as it's the only way to prevent malicious applications from retrieving an access token on behalf of a legitimate confidential application.
Contrary to popular belief, client authentication is never mandatory when using the token endpoint (except for the client credentials grant), which means that public applications like JS or mobile apps are allowed to use the resource owner password grant without having to send their credentials.
In practice, it's up to you to decide whether your token endpoint should accept unauthenticated requests or not, depending on the type of client you'll use.
No big surprise here: while a few rules apply to most implementations (e.g brute force countermeasures), the exact implementation of this event will mainly depend on your specific requirements:
Do you need to support multiple grant types, or just the resource owner password credentials grant?
Do you need to support public clients? If you plan to use ROPC with JS or mobile applications, you won't be able to make client authentication mandatory, since these applications cannot safely store their credentials. Conversely, if you only target public apps, rolling your own client authentication policy is likely to be pointless.
When supporting both public and confidential clients, the recommended approach is to skip client authentication when client_id
is missing and validate it when present, to make sure counterfeit applications cannot impersonate confidential applications.
- Do you have multiple clients? If you have more than one client application, you'll probably want to store the application details in a database instead of hardcoding them in the provider class.
Implementing a policy skipping client authentication (for JS/mobile apps-only scenarios)
Using the resource owner password credentials grant with SPA apps is a very popular scenario. Since these apps can't store their credentials in a safe place, client authentication cannot be enforced.
Here's how you could implement ValidateTokenRequest
to avoid making client authentication mandatory:
1 | public override Task ValidateTokenRequest(ValidateTokenRequestContext context) |
Though additional checks can be added (e.g Origin
header validation), you have no way to ensure that the caller sending the token request is really a trusted application when skipping client authentication: keep in mind that access tokens may be issued to unauthorized parties impersonating legitimate applications.
When client authentication is not enforced, the resource owner password credentials grant offers the same security level as the implicit flow and shares a similar threat model.
Implementing a policy requiring client authentication (for server-side apps-only scenarios)
This scenario is the exact opposite of the previous one: when targeting confidential applications, you MUST enforce client authentication to prevent client impersonation by unauthorized parties and the token request must be rejected if the client credentials are missing or invalid.
1 | public override Task ValidateTokenRequest(ValidateTokenRequestContext context) |
Implementing an hybrid policy supporting both public and confidential applications
In a few cases, you'll want to support both public clients (e.g mobile apps) and confidential applications (e.g MVC clients). For that, you'll need to implement an hybrid policy, supporting both types of clients:
1 | public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) |
Implementing HandleTokenRequest
to issue an authentication ticket containing the user claims
HandleTokenRequest
is the event responsible for processing the token request and preparing the authentication ticket used to serialize the access token.
It's important to note that HandleTokenRequest
is invoked for every token request. User implementations should only process token requests that use a supported grant_type
(e.g password
) and let ASOS automatically handle or reject the other grants (e.g authorization_code
or refresh_token
).
Like ValidateTokenRequest
, the exact implementation of HandleTokenRequest
will mainly depend on your application, and specially on the membership stack you're using. That said, a few generic rules apply to all implementations:
- Brute force countermeasures MUST be implemented, as required by the OAuth 2.0 specification. This is usally done by using key derivation (ideally with a large number of iterations, to slow down the authentication process) and by implementing account lockout.
- The token request MUST be rejected if the user account is configured to require two-factor authentication as the resource owner password credentials grant cannot be used in this case (at least, not in a standard way).
- Implementations SHOULD avoid revealing whether the username is valid or not, for privacy reasons.
Here's an example of how you can leverage ASP.NET Core Identity (previously known as ASP.NET Identity 3) to implement HandleTokenRequest
:
1 | public override async Task HandleTokenRequest(HandleTokenRequestContext context) |
Next part: Implementing the authorization code and implicit flows.