ASP.NET
Core Identity
Server 4 Role based
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)
▋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
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<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
new IdentityResources.Phone(),
new IdentityResources.Address()
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new ApiResource[]
{
new ApiResource("MyBackendApi2", "My Backend API 2", new List<string>(){ ClaimTypes.Role }),
};
}
public static IEnumerable<Client> GetClients()
{
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<string> AdminGet()
{
return "Yes, only an Admin can access this API!";
}
[HttpGet]
[Route("Sit/Get")]
[Authorize(Roles = "sit")]
public ActionResult<string> SitGet()
{
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 userRole, bool isOK) = await this.cache.GetCacheAsync<UserRole>(cacheKey);
if (isOK)
{
claims = userRole.Roles.Split(',').Select( x => new Claim(JwtClaimTypes.Role, x)).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
Hi Sir, I did not found dll JB.Infra.Service.Redis ?
回覆刪除Hi,zip,you can use the master branch. The branch removed the dependency of JB.Infra.Service.Redis.
刪除Thanks so much @karatejb (Like)
刪除