2019年10月2日 星期三

[ASP.NET Core] Identity Server 4 – Role based authorization


 ASP.NET Core   Identity Server 4   Role based  


Introduction


In this article, we will learn how to apply Role-based authorization to Backend APIs (Web API) by adding Role claim(s) to user when issuing a JWT token.
So that we can easily set required role(s) to our APIs to protect them. 

The goals are
·  Learn how to set role claim(s) for Client in Identity Server 4 (Auth Server).
·  Issue JWT token with role claim(s)
·  Secure API with role(s)










Environment


docker-openldap 1.2.4 (OpenLDAP 2.4.47)
ASP.NET Core 2.2.402
IdentityServer4 2.4.0
Nordes/IdentityServer4.LdapExtension 2.1.8
IdentityModel 3.10.10



Implement


The source code is on my Github.


Set Role Claim(s) for Client

In Identity Server 4, we can add Role Claim(s) for Clients on design time as following,

 new Client
 {
      // skip other settings…

       Claims = new Claim[]  
      {
             new Claim(JwtClaimTypes.Role"admin"),
             new Claim(JwtClaimTypes.Role"user")
      }
}


Here is a full sample code of in-memory configuration.

(Auth Server) InMemoryInitConfig.cs

public class InMemoryInitConfig
    {
        public static IEnumerable<IdentityResourceGetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Email(),
                new IdentityResources.Profile(),
                new IdentityResources.Phone(),
                new IdentityResources.Address()
            };
        }

        public static IEnumerable<ApiResourceGetApiResources()
        {
            return new ApiResource[]
            {
                new ApiResource("MyBackendApi2""My Backend API 2"new List<string>(){ ClaimTypes.Role }),
            };
        }

        public static IEnumerable<ClientGetClients()
        {
            return new[]
            {
                 new Client
                {
                    Enabled = true,
                    ClientId = "RoleBasedBackend",
                    ClientName = "MyBackend Client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    AccessTokenType = AccessTokenType.Jwt,
                    AllowedScopes = {
                        "MyBackendApi2",
                        IdentityServerConstants.StandardScopes.OpenId,
                    },
                    AlwaysSendClientClaims = true,
                    UpdateAccessTokenClaimsOnRefresh = true,
                    AlwaysIncludeUserClaimsInIdToken = true,
                    AllowAccessTokensViaBrowser = true,
                    IncludeJwtId = true,
                    ClientSecrets = { new Secret("secret".Sha256()) },
                    AllowOfflineAccess = true,
                    AccessTokenLifetime = 3600,

                    RefreshTokenUsage = TokenUsage.OneTimeOnly// Or ReUse
                    RefreshTokenExpiration = TokenExpiration.Sliding,
                    AbsoluteRefreshTokenLifetime = 360000,
                    SlidingRefreshTokenLifetime = 36000,
                    
                      ClientClaimsPrefix = string.Empty
                    Claims = new Claim[] // Assign const roles 
                    {
                        new Claim(JwtClaimTypes.Role"admin"),
                        new Claim(JwtClaimTypes.Role"user")
                    }
                }
            };
        }
    }


Notice that the Role claim will be set with default type: client_role, as below.
The prefix "client_" will be set to claim in order to make sure they don’t accidentally collide with user claims. (See reference)





However, the roles-based authorization on ASP.NET Core does not use the claims by type "client_role" but by ClaimTypes.Role.
So we have to set

ClientClaimsPrefix = string.Empty

to make sure the role claim is of right type,




Secure Backend API with Role(s)

Since we had added the Role Claim(s) to JWT token, now we can secure the API with Role(s).

For example,

(Backend) API Controller

 [HttpGet]
 [Route("Admin/Get")]
 [Authorize(Roles = "admin")]
 public ActionResult<stringAdminGet()
 {
      return "Yes, only an Admin can access this API!";
 }

 [HttpGet]
 [Route("Sit/Get")]
 [Authorize(Roles = "sit")]
 public ActionResult<stringSitGet()
 {
      return "Yes, only an SIT can access this API!";
 }


For example, if the user has the token with Role Claims:




Then the user can successfully access the API with required Role: "admin".




But the user will be rejected (403 Forbidden) when accessing the API with required Role: "sit".





Notice that authenticated user can access any API without required Role(s).


Advanced: Dynamically load user’s role(s) and add role claim(s)

At present, we defined the Roles on IdentityServer’s Client configuration on design time.

However, most time we keep the user’s role(s) in database, so it’s common to have a way to generate Role Claim on run time.  
IdentityServer defines an extensibility point for allowing claims to be dynamically loaded as needed for a user by implementing IProfileService.


For example, I store user’s role in Redis and will dynamically load the information to generate Role Claim.



(Auth Server) ProfileService.cs

 public class ProfileService : IProfileService
 {
        private readonly ICacheService cache = null;
        private readonly CacheKeyFactory cacheKeys = null;

        public ProfileService(
            ICacheService cache,
            CacheKeyFactory cacheKeys)
        {
            this.cache = cache;
            this.cacheKeys = cacheKeys;
        }
        
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            //sub is User’s Id.
            var subClaim = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");

            if (!string.IsNullOrEmpty(subClaim?.Value))
            {
                context.IssuedClaims = await this.getClaims(subClaim.Value);
            }
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true;
        }

        private async Task<List<Claim>> getClaims(string userName)
        {
            var claims = new List<Claim>();

            #region Method 1.Add extra const roles
            //claims = new List<Claim>
            //    {
            //        new Claim(JwtClaimTypes.Role, "admin"),
            //        new Claim(JwtClaimTypes.Role, "user")
            //    };
            #endregion

            #region Method 2. Add extra roles from redis
            var cacheKey = this.cacheKeys.GetKeyRoles(userName);
            (UserRole userRolebool isOK) = await this.cache.GetCacheAsync<UserRole>(cacheKey);

            if (isOK)
            {
                claims = userRole.Roles.Split(',').Selectx => new Claim(JwtClaimTypes.Rolex)).ToList();
            }
            #endregion

            return claims;
        }
 }

Don’t forget to remove the Role Claims from Client configuration on IdentityServer.

Then add the custom ProfileService into service container.


(Auth Server) Startup.cs

public void ConfigureServices (IServiceCollection services) 
 {
       // … Skip
      
#region Identity Server
// Skip … Signing credential
// Skip … Set in-memory, code config
            
      builder.AddProfileService<ProfileService>();
#endregion
 }




Result:




Source Code




Reference











3 則留言:

  1. Hi Sir, I did not found dll JB.Infra.Service.Redis ?

    回覆刪除
    回覆
    1. Hi,zip,you can use the master branch. The branch removed the dependency of JB.Infra.Service.Redis.

      刪除
    2. Thanks so much @karatejb (Like)

      刪除