I am having an issue including role claim after a successful login into Identity Server 4 (IS4) with integration with AspNet Core Identity. This prevents me from using the "Authorize(Roles=xxx)" attribute to secure the access to the api.
I was following samples provided in the Identity Server 4/AspNet Identity integration documentations. Suprisingly the documentation don't have an example to include a role claim which I think is a very common scenario.
I setup 3 projects as per the IS4 documentation with HybridClientCredential grant type specified in the host, created the AspNet Identity DB, and add the role ("Admin") manually into the database generated by EF Core. Provided I've setup things correctly, I am expecting the role to be automatically included into the user claims after a successful login.
This is the code I am using:
Host:
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
// expanded version if more control is needed
new ApiResource
{
Name = "api1",
Description = "My API",
// secret for using introspection endpoint
ApiSecrets =
{
new Secret("secret".Sha256())
},
// include the following using claims in access token (in addition to subject id)
UserClaims = { "role" },
// this API defines two scopes
Scopes =
{
new Scope()
{
Name = "api1",
DisplayName = "Full access to API 1",
UserClaims = new [] { "role" }
}
}
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>()
{
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AlwaysIncludeUserClaimsInIdToken = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
}
};
}
}
Client:
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies"
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ClientId = "mvc",
ClientSecret = "secret",
ResponseType = "code id_token",
Scope = { "api1", "offline_access" },
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
}
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Api:
[Route("api/[controller]")]
[Authorize(Roles="Admin")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
I got the setup working except that the host not including the role claim after a successful login.
I was wondering if anyone could help to let me know how to resolve this issue? Thanks.
Hi this is the way I create custom Policy role-based,
1)
in Config -> Client section add:
Claims = new Claim[]
{
new Claim("Role", "admin")
}
2)
Then in the Api -> startup.cs -> ConfigureServices add:
services.AddAuthorization(options =>
{
options.AddPolicy("admin", policyAdmin =>
{
policyAdmin.RequireClaim("client_Role", "Admin");
});
//otherwise you already have "api1" as scope
options.AddPolicy("admin", builder =>
{
builder.RequireScope("api1");
});
});
3)
Then use it this way:
[Route("api/[controller]")]
[Authorize("admin")]
public class ValuesController : Controller
If you analize the token you'll have something like this:
{
"alg": "RS256",
"kid": "2f2fcd9bc8c2e54a1f29acf77b2f1d32",
"typ": "JWT"
}
{
"nbf": 1513935820,
"exp": 1513937620,
"iss": "http://localhost/identityserver",
"aud": [
"http://localhost/identityserver/resources",
"MySecuredApi"
],
"client_id": "adminClient",
"client_Role": "admin", <---------------
"scope": [
"api.full_access",
"api.read_only"
]
}
PS:
you cannot use "RequireRole" because Identity Server 4 when you use:
Claims = new Claim[]
{
new Claim("Role", "admin")
},
will create:
client_Role: admin
but "RequireRole" uses:
Role: admin
so it won't match.
As you can test:
using System.Security.Claims;
MessageBox.Show("" + new Claim("Role", "admin"));
UPDATE WITH RequireRole : Clear "ClientClaimsPrefix"
in Config -> Client section add:
ClientClaimsPrefix = "",
Claims = new Claim[]
{
new Claim(ClaimTypes.Role, "Admin")
}
Then in the Api -> startup.cs -> ConfigureServices add:
services.AddAuthorization(options =>
{
options.AddPolicy("admin", builder =>
{
builder.RequireRole(new[] { "Admin" });
});
});
Then use it this way:
[Route("api/[controller]")]
[Authorize("admin")]
public class ValuesController : Controller
Otherwise without the "Policy" use like this:
[Authorize(Roles = "Admin")]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With