I am currently developing a Single Sign On system, and with this Single Sign On system. I have developed an Admin Client app used for user management. There are API calls in the Identity Provider (IDP) that the Admin Client will call when actioning the user data. The reason for this is that user information is stored in the Identity Provider's database so, to manipulate it from the Admin Client, it made sense to do this through API calls rather than storing or accessing the information in multiple different places.
The system uses OpenIddict Server for the IDP, and OpenIddict Client for the Admin Client app. I am trying to make use of multiple authentication flows to make this work. Authorisation Code Flow is used when the user is logging in as a way to retrieve the access token containing their claims. Client Credentials flow is used to retrieve the user's access token when the Admin Client makes a request to the IDP API endpoint.
The Admin Client will check the user's role whenever they attempt to make a Http request to the controllers that will make requests to the API endpoint in the IDP. Because the user's role is checked in the Admin Client, I have opted to use the Client Credentials flow to authorise the request to the IDP from the client.
The method used to request this access token is as follows:
private TokenResponse RequestAccessToken()
{
lock (accessTokenLock)
{
if (DateTime.Now.AddMinutes(1) > accessTokenExpiry)
{
HttpRequestMessage request = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri($"{client.BaseAddress}connect/token"),
Content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret)
})
};
HttpResponseMessage? response = client.Send(request);
response.EnsureSuccessStatusCode();
var stringResponse = response.Content.ReadAsStringAsync().Result;
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(stringResponse);
accessTokenExpiry = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn);
return tokenResponse;
}
return new TokenResponse();
}
}
private class TokenResponse
{
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
}
I use the access token retrieved from the above in the following request to the API endpoint of the IDP:
public async Task<List<UserDTO>> GetAllUsersAsync()
{
try
{
TokenResponse accessToken = RequestAccessToken();
HttpRequestMessage request = new()
{
Method = HttpMethod.Get,
RequestUri = new Uri($"useradministration/getallusersdetailsandclaims", UriKind.Relative),
Headers =
{
Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken)
}
};
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var userData = System.Text.Json.JsonSerializer.Deserialize<List<UserDTO>>(content,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
return userData;
}
catch (Exception ex)
{
throw new InvalidOperationException(ex.Message);
}
}
The useradministration/getallusersdetailsandclaims
endpoint in the IDP is protected with an [Authorize]
tag.
However, when I send the JWT Bearer access token I retrieve, from the RequestAccessToken()
method, in the Authorisation header of the request, I get an unauthorised response from the IDP.
I believe the IDP is not validating the token when it is sent over via the request. The token retrieved from the Client Credentials flow differs slightly from the one retrieved through the Authorization Code flow.
Is it possible to implement two different authentication schemes in the IDP so that it is able to handle both the token retrieved from the Authorization Code flow and the Client Credentials flow? That way it can validate the token sent by the Admin Client during the request, as well as authorise/refuse requests made through other means.
I have tried to keep this post as concise as I can, but if any more information is needed feel free to ask in the comments, I can add more to the post if needed.
Thanks in advance.
I managed to get this working in the end.
I used the "Aridka" sample on the OpenIddict GitHub samples page for a better approach to the client credentials flow. It made the request access token method a lot simpler.
private static async Task<string> RequestAccessToken(IServiceProvider provider)
{
var service = provider.GetRequiredService<OpenIddictClientService>();
var result = await service.AuthenticateWithClientCredentialsAsync(new());
return result.AccessToken;
}
This would properly authenticate and retrieve the access token from the token endpoint of the IDP. I was then able to return this access token and use it in the authorisation header of the client app's request to the API endpoint of the IDP.
var accessToken = RequestAccessToken(_serviceProvider);
HttpRequestMessage request = new(HttpMethod.Get, $"{client.BaseAddress}useradministration/getallusersdetailsandclaims");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Result.ToString());
var response = await client.SendAsync(request);
To make this work, I used a MultipleAuthenticationScheme class, which will interchange the authentication scheme of the request based on if the authorisation header contains a Bearer token or not. So for normal logins it will use Cookie authentication scheme, but for requests from the client app to the IDP's API endpoint where the access token is included, it will use OpenIddict's Client authentication scheme. It will then add a shared cookie which will add the cookie and its options to the authentication settings set.
This class then replaces the usual authentication service in Program.cs.
builder.Services
.AddOpenIddictAndCookiesAuthentication()
.AddSharedCookie(
configuration["CookiesAuthentication:CookieName"],
configuration["CookiesAuthentication:ApplicationName"] ?? configuration["CookiesAuthentication:EncryptionKeyPath"],
configuration["CookiesAuthentication:ApplicationName"] != null);
Finally, the API endpoint of the IDP is protected with [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
.