In a previous article, we have looked in detail about the various flows that are prescribed under the OAuth standards for requesting tokens from a SecureTokenServer (STS) and how we can implement these flows using IdentityServer4, which is an open source library that provides functionalities such as session management, identity management and tokens. We have also looked in detail about what is a ClientCredentials Grant flow which is a simple flow used for requesting access_tokens for securing APIs, and have also looked at how to implement it and consume the token in an API.
In this article, let’s talk about another such simple Flow – which can be used for user authentication. It is called as the Resource Owner Password Credentials Grant flow.
What is Resource Owner Password Credentials (ROPC) Flow?
Resource Owner Password Credentials flow, or shortly called as the ROPC flow is a simple grant flow used for authenticating a user identity and requesting for user’s data from the system by a client. It involves the user, who is the "Resource Owner" (resource can be his profile or an API resource) uses his credentials: a username and a password directly to request for access to a client on behalf of himself. The TokenServer validates the user credentials (username and password) against a userstore (basically a database) and upon successful validation grants the requesting client a token of desired type. Since a user is involved in this flow, the client can request for an access_token used to access and API resource which the user has permission or request an id_token representing the user identify for its own use.
"The client requests for a token from the authorization server on behalf of a resource owner by submitting the owner’s credentials directly"
When to use Resource Owner Password Flow?
Resource Owner Password Credentials is generally used in cases when the user (or the resource owner) has a high trust on the client which is requesting for grant from the authorization server. Since this isn’t the case for most of the applications, this flow is allowed by the authorization servers only when any other flow is not viable for communication. Clients such as device operating systems, system applications or apps which are highly privileged are only allowed to use this flow.
"A client must not use this flow to authenticate users against the authorization server unless other grant flows are not viable"
How Resource Owner Password Flow works?
The Resource Owner Password flow generally takes very few steps than the other flows, since the request is very straight forward and contains all the necessary confidential information in one single request. Hence it must be implemented with at most care in mind for both clients and the safeguarding Authorization Servers.
- The user (Resource Owner) submits his username and password to the client via a Form (or by any other means).
- The client requests token from the authorization server’s token endpoint /token along with the credentials submitted by the user in previous step and the scopes it seeks for access.
- The authorization server validates the crdentials along with matching client and scopes. Upon successful validation, the server returns the client with necessary token.
- The client then uses this token for fetching user information via /userinfo endpoint or accesses the API resource seeks by passing the token in the Authorization Header in its requests.
A typical ROPC grant token request looks like below:
POST /connect/token HTTP/1.1
Host: token_server_uri
Content-Type: application/x-www-form-urlencoded
client_id=<client_name>&client_secret=<client_secret>&scope=<desired scopes>&grant_type=password&username=<owner_username>&password=<owner_password>
Observe that there are two new fields we’re sending apart from the usual client_id, client_secret and the scopes fields – the username and password fields, which represent the owner’s credentials posted to the client. Also, the grant_type is password.
Implementing Resource Owner Password Grant Flow in IdentityServer4:
To implement ROPC flow in our IdentityServer4 TokenServer, we undergo the following steps:
- Define a Client that works on ROPC
- Define a UserValidator which is invoked when the call happens – to validate the user credentials
- Configure the API resource with the client information which is created
- Verify the grant flow by making a POST call in the format discussed before and check if it results in a Token
- Verify by invoking the API along with the token obtained in the above step
Let’s begin by adding a new Client that uses the GrantType as "ResourceOwnerPassword" in the TokenServer Config class which we used before.
public static IEnumerable<ApiResource> Apis =>
new List<ApiResource>
{
new ApiResource("api1", "My API")
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client {
ClientId = "ropc_client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = {
new Secret("secret".Sha256())
},
AllowedScopes = { "api1", StandardScopes.OpenId }
}
};
In the scopes, Observe that we’ve added both an ApiResource "api1" and the standard "openid" scope. This indicates that the resultant token is an id_token which contains profile information of the resource owner and can also be used to access an API which represents the "api1" ApiResource.
To validate the user credentials against our own userstore, we need to supply our own implementation of the IResourceOwnerPasswordValidator interface, which the TokenServer invokes when it receives a token request of Password GrantType.
The interface IResourceOwnerPasswordValidator declares a single method ValidateAsync(ResourceOwnerPasswordValidationContext context) in which we validate the credentials and set the context to success or failure.
public class UserValidator : IResourceOwnerPasswordValidator
{
private readonly Dictionary<string, string> users;
public UserValidator()
{
// a static set of username, passwords
// for demonstration
// in production scenarios, we can inject a
// repository or DbContext via DependencyInjection
// into the constructor
this.users = new Dictionary<string, string>() {
{ "admin", "Abcd@1234" }
};
}
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var username = context.UserName;
var password = context.Password;
if (this.users.Any(x => x.Key == username && x.Value == password))
{
// context set to success
context.Result = new GrantValidationResult(
subject: username,
authenticationMethod: "custom",
claims: new Claim[] {
new Claim(ClaimTypes.NameIdentifier, username)
}
);
return Task.FromResult(0);
}
// context set to Failure
context.Result = new GrantValidationResult(
TokenRequestErrors.UnauthorizedClient, "Invalid Crdentials");
return Task.FromResult(0);
}
}
Observe the success case of the flow, we’re passing a few things to the GrantValidationResult() object:
- subject which is a unique identifier representing the user, which we can use to pass user Id if needed. This value comes under the "sub" of the token.
- claims is a collection of claim data containing user attributes which the token contains in its payload section.
Finally, we link this implementation along with other values to the IdentityServer4 service defined in the Startup class.
var builder = services.AddIdentityServer()
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddResourceOwnerValidator<UserValidator>()
.AddInMemoryIdentityResources(Config.Ids);
To test this setup, we pass a POST request with the user credentials requesting an id_token and see how things work out.
POST /connect/token HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
client_id=ropc_client&client_secret=secret&scope=openid api1&grant_type=password&username=admin&password=Abcd@1234
When we make this POST call, the TokenServer invokes the UserValidator.ValidateAsync() method with the username and password passed in the context object. The result is positive since we’re using the same values as we seeded in the UserValidator class. Observe that the scopes we’ve sent is api1 and openid, which is also one of the "Allowed Scopes" of the client "ropc_client".
The response is an id_token which can also be used to access an API resource as follows:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InN2cjJrVjdUWUNDenRSclpScjNJQ2ciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1OTI4NDMyNDcsImV4cCI6MTU5Mjg0Njg0NywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6ImFwaTEiLCJjbGllbnRfaWQiOiJyb3BnX2NsaWVudCIsInN1YiI6ImFkbWluIiwiYXV0aF90aW1lIjoxNTkyODQzMjQ3LCJpZHAiOiJsb2NhbCIsInNjb3BlIjpbIm9wZW5pZCIsImFwaTEiXSwiYW1yIjpbImN1c3RvbSJdfQ.D7bSAmphidTN1xbtv2YvNLTSkcLj4MsO3CGVWA53pSlfbhXCJaiOqmAs8YgMD8KwdaGBgZvGdJBIxVn5tF79ykQmjrcityDUc0Cz9b5JOz_Q0Bgqj_-1DDOz3tcikA7zYPBtaG6ApCclTNePkAbQUtsrMI_jv_xwl7pS0JHoiAyFUHUqsPC9EqvKOk2JGz01XbCBGF9x5vRsjaDriOjWkQ8ZZUeuAe3ehe3ext4mD_eDqB2TvdBsOQvpyHUqtGEmSkpXwdsWaIBnPUfssuWxwV2NH9sobmMRoolEqGQdYajgsinNTdAXTriV9obnbRfIx4hAr3afVspLttyiWSHpWA",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "api1 openid"
}
How do we verify that this is an id_token that represents a user’s identity? There are two ways to do this: either parse the token in a validator such as jwt.io which shows the following claims:
{
"nbf": 1592843247,
"exp": 1592846847,
"iss": "https://localhost:5001",
"aud": "api1",
"client_id": "ropg_client",
"sub": "admin",
"auth_time": 1592843247,
"idp": "local",
"scope": [
"openid",
"api1"
],
"amr": [
"custom"
]
}
Observe that the "sub" claim contains the username we’ve passed in the subject. Secondly, we can pass this token to another endpoint of the TokenServer that returns a matching userprofile – /connect/userinfo.
GET /connect/userinfo HTTP/1.1
Host: localhost:5001
Authorization: Bearer id_token_here
which returns the claims from the passed token.
{
"sub": "admin"
}
To use this token for access the API we secured previously for the scope "api1" and accessed from a token obtained using the client_credentials client, we can simply pass this token and see what happens.
GET /weatherforecast HTTP/1.1
Host: localhost:5003
Authorization: Bearer ropc_client_id_token
The API call is successful and returns data. This is because the token is issued for scope "api1" which the Authentication middleware in the API expects in the token along with the Issuer and the expiry.
In this way, we can implement a Resource Owner Password Credentials grant flow using IdentityServer4 with a custom implementation of the UserValidator.