2019年11月13日 星期三

[ASP.NET Core] Identity Server 4 – Client Credential


 ASP.NET Core   Identity Server 4   Client Credential   Machine to Machine  


Introduction



Client Credential is one of the grant types/flows of the OpenID Connect and OAuth 2.0 specifications.

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


Environment


Docker 18.05.0-ce
ASP.NET Core 3.0.100
IdentityServer4 3.0.1
IdentityModel 3.10.10



Implement


The source code is on my Github.

Backend Server (Resource Server)

The Resource Server must have the Authentication/Authorization configuration. (Reference: [ASP.NET Core] Identity Server 4 – Secure Web API)
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<stringMyGet()
{
    // ...
}




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<ApiResourceGetApiResources()
        {
            return new ApiResource[]
            {
                  new ApiResource("MyBackendApi2""My Backend API 2")
            };
        }

        public static IEnumerable<ClientGetClients()
        {           
              // 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


We will use IdentityModel as the client library. See my facade class for more details on how to use the IdentityModel.



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<DiscoveryResponseDiscoverDocumentAsync()
        {
            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  




Source Code




Reference








沒有留言:

張貼留言