ASP.NET
Core Identity Server 4 Client Credential Machine to Machine
It is used for Machine-to-Machine
communications, and no interactive use is involved.
For example, we
have two applications, A and B, and they are both Resource Servers.
A can get the
Access Token that can be used to access B’s resource(APIs) by Client
Credential flow.
In this
scenario, A is the Client, B is the Resource Server.
On the other hand,
B can get the Access Token that can be used to access A’s resource(APIs) by Client
Credential flow.
In this
scenario, B is the Client, A is the Resource Server.
We will demo
with a simple sample to simulate that granting a Client to access a Resource
Server:
·
Client: create a Client in Unit Test
·
Resource Server: the original Backend Server
▋Docker 18.05.0-ce
▋ASP.NET Core 3.0.100
▋IdentityServer4 3.0.1
▋IdentityModel 3.10.10
The source code
is on my Github.
▋Backend Server (Resource Server)
Notice that the Audience
is “MyBackendApi2”.
▋Startup.cs: ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
#region Enable Authentication
IdentityModelEventSource.ShowPII = true; //Add this line
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
string authServerBaseUrl = this.Configuration["Host:AuthServer"];
bool isRequireHttpsMetadata = (!string.IsNullOrEmpty(authServerBaseUrl) && authServerBaseUrl.StartsWith("https")) ? true : false;
options.Authority = string.IsNullOrEmpty(authServerBaseUrl) ? "https://localhost:6001" : authServerBaseUrl;
options.RequireHttpsMetadata = isRequireHttpsMetadata;
options.Audience = "MyBackendApi2"; // API Resource name
options.TokenValidationParameters.ClockSkew = TimeSpan.Zero; // The JWT security token handler allows for 5 min clock skew in default
options.BackchannelHttpHandler = AuthMetadataUtils.GetHttpHandler();
});
#endregion
// …
skip
}
And then we can
secure the APIs on the Resource Server (Backend Server).
[HttpGet]
[Authorize]
public ActionResult<string> MyGet()
{
// ...
}
▋Auth Server
We have to define
a Client-Credential-grant-type Client on Auth Server’s configuration (Class file
or JSON file).
Notice that the
Client config MUST have allowed scope: “MyBackendApi2”,
so that the Client can access the Resource Server.
▋InMemoryInitconfig.cs
public class InMemoryInitConfig
{
// ... Skip GetIdentityResources()
public static IEnumerable<ApiResource> GetApiResources()
{
return new ApiResource[]
{
new ApiResource("MyBackendApi2", "My Backend API 2")
};
}
public static IEnumerable<Client> GetClients()
{
// Client credentials
new Client
{
Enabled = true,
ClientId = "Resources",
ClientName = "Resource Owners",
AllowedScopes =
{
"MyBackendApi2",
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenType = AccessTokenType.Jwt,
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
IncludeJwtId = true,
ClientSecrets = { new Secret("secret".Sha256()) },
AccessTokenLifetime = 36000,
}
};
}
}
▋Client
I will use an
Unit Test to simulate a Client to access the API on Resource Server.
The flow is
1. Use the Client Credential (Client ID and Secret) to request an
Access Token
2. Create a HttpClient and send request with Authorization Header to
access a secured API
▋ClientCredentialTest.cs
public class TestResourceApi
{
private const string SECRETKEY = "secret";
private const string CLIENTID = "Resources";
private readonly string remoteServiceBaseUrl = "https://localhost:6001";
private DiscoveryResponse discoResponse = null;
private HttpClient httpClient = null;
public TestResourceApi()
{
this.httpClient = new HttpClient();
}
[Test]
public async Task TestSend()
{
#region Get access token
if (this.discoResponse == null)
{
this.discoResponse = await this.DiscoverDocumentAsync();
}
var tokenResponse = await this.httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = this.discoResponse.TokenEndpoint,
ClientId = CLIENTID,
ClientSecret = SECRETKEY,
Scope = "MyBackendApi2"
});
if (tokenResponse.IsError)
{
Assert.Fail(tokenResponse.Error);
}
#endregion
#region Call protected API
// Test with correct access token
this.httpClient.SetBearerToken(tokenResponse.AccessToken);
var response = await this.httpClient.GetAsync("https://localhost:5001/api/DemoPolicyBased/Everyone/Get");
if (!response.IsSuccessStatusCode)
{
Assert.Fail(response.StatusCode.ToString());
}
else
{
Assert.True(true);
}
#endregion
}
private async Task<DiscoveryResponse> DiscoverDocumentAsync()
{
var isRequireHttps = this.remoteServiceBaseUrl.StartsWith("https");
this.discoResponse = await this.httpClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = this.remoteServiceBaseUrl,
Policy =
{
RequireHttps = isRequireHttps,
ValidateIssuerName = true
}
});
if (this.discoResponse.IsError)
{
throw new Exception(this.discoResponse.Error);
}
return this.discoResponse;
}
}
▋Unit Test results
If we request the
Access Token with wrong scope, we will get the error: “invalid_scope”.
If we use the
Access Token with scope “MyBackendApi2”, but try to access the Resource Server
with audience: “MyBackendApi1”, we will get the UnAuthorized response.
▋Furthermore
We can also
enable Role|Policy based authorization by adding mapping user claim(s).
See