Identity Server 4: adding claims to access token - c#

I am using Identity Server 4 and Implicit Flow and want to add some claims to the access token, the new claims or attributes are "tenantId" and "langId".
I have added langId as one of my scopes as below and then requesting that through identity server, but i get the tenantId also. How can this happen?
This the list of scopes and client configuration:
public IEnumerable<Scope> GetScopes()
{
return new List<Scope>
{
// standard OpenID Connect scopes
StandardScopes.OpenId,
StandardScopes.ProfileAlwaysInclude,
StandardScopes.EmailAlwaysInclude,
new Scope
{
Name="langId",
Description = "Language",
Type= ScopeType.Resource,
Claims = new List<ScopeClaim>()
{
new ScopeClaim("langId", true)
}
},
new Scope
{
Name = "resourceAPIs",
Description = "Resource APIs",
Type= ScopeType.Resource
},
new Scope
{
Name = "security_api",
Description = "Security APIs",
Type= ScopeType.Resource
},
};
}
Client:
return new List<Client>
{
new Client
{
ClientName = "angular2client",
ClientId = "angular2client",
AccessTokenType = AccessTokenType.Jwt,
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = new List<string>(redirectUris.Split(',')),
PostLogoutRedirectUris = new List<string>(postLogoutRedirectUris.Split(',')),
AllowedCorsOrigins = new List<string>(allowedCorsOrigins.Split(',')),
AllowedScopes = new List<string>
{
"openid",
"resourceAPIs",
"security_api",
"role",
"langId"
}
}
};
I have added the claims in the ProfileService:
public class ProfileService : IdentityServer4.Services.IProfileService
{
private readonly SecurityCore.ServiceContracts.IUserService _userService;
public ProfileService(SecurityCore.ServiceContracts.IUserService userService)
{
_userService = userService;
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//hardcoded them just for testing purposes
List<Claim> claims = new List<Claim>() { new Claim("langId", "en"), new Claim("tenantId", "123") };
context.IssuedClaims = claims;
return Task.FromResult(0);
}
This is what i am requesting to get the token, the problem is i am only requesting the langId but I am getting both the tenantId and langId in the access token
http://localhost:44312/account/login?returnUrl=%2Fconnect%2Fauthorize%2Flogin%3Fresponse_type%3Did_token%2520token%26client_id%3Dangular2client%26redirect_uri%3Dhttp%253A%252F%252Flocalhost:5002%26scope%3DresourceAPIs%2520notifications_api%2520security_api%2520langId%2520navigation_api%2520openid%26nonce%3DN0.73617935552798141482424408851%26state%3D14824244088510.41368537145696305%26
Decoded access token:
{
"nbf": 1483043742,
"exp": 1483047342,
"iss": "http://localhost:44312",
"aud": "http://localhost:44312/resources",
"client_id": "angular2client",
"sub": "1",
"auth_time": 1483043588,
"idp": "local",
"langId": "en",
"tenantId": "123",
"scope": [
"resourceAPIs",
"security_api",
"langId",
"openid"
],
"amr": [
"pwd"
]
}

This answer was written for Identityserver4 on .Net core 2 to use it for .Net core 3, this answer may help you, but you need to test and change a few things.
I am using asp.net Identity and Entity Framework with Identityserver4.
This is my sample code, works well and JWT contains all roles and claims
You can see how to implement Identityserver4 with ASP.Net core identity here
http://docs.identityserver.io/en/release/quickstarts/6_aspnet_identity.html
https://github.com/IdentityServer/IdentityServer4.Samples/tree/dev/Quickstarts/6_AspNetIdentity
1- identity server startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
//Add IdentityServer services
//var certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "LocalhostCert.pfx"), "123456");
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryIdentityResources(Configs.IdentityServerConfig.GetIdentityResources())
.AddInMemoryApiResources(Configs.IdentityServerConfig.GetApiResources())
.AddInMemoryClients(Configs.IdentityServerConfig.GetClients())
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<Configs.IdentityProfileService>();
}
// 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.UseDatabaseErrorPage();
//app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
// Adds IdentityServer
app.UseIdentityServer();
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=Login}/{id?}");
});
}
2- IdentityServerConfig.cs
using IdentityServer4;
using IdentityServer4.Models;
using System.Collections.Generic;
namespace IdentityAuthority.Configs
{
public class IdentityServerConfig
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
// scopes define the API resources
public static IEnumerable<ApiResource> GetApiResources()
{
//Create api resource list
List<ApiResource> apiResources = new List<ApiResource>();
//Add Application Api API resource
ApiResource applicationApi = new ApiResource("ApplicationApi", "Application Api");
applicationApi.Description = "Application Api resource.";
apiResources.Add(applicationApi);
//Add Application Api API resource
ApiResource definitionApi = new ApiResource("DefinitionApi", "Definition Api");
definitionApi.Description = "Definition Api.";
apiResources.Add(definitionApi);
//Add FF API resource
ApiResource ffApi = new ApiResource("FFAPI", "Fule .netfx API");
ffApi.Description = "Test using .net 4.5 API application with IdentityServer3.AccessTokenValidation";
apiResources.Add(ffApi);
return apiResources;
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
//Create clients list like webui, console applications and...
List<Client> clients = new List<Client>();
//Add WebUI client
Client webUi = new Client();
webUi.ClientId = "U2EQlBHfcbuxUo";
webUi.ClientSecrets.Add(new Secret("TbXuRy7SSF5wzH".Sha256()));
webUi.ClientName = "WebUI";
webUi.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
webUi.RequireConsent = false;
webUi.AllowOfflineAccess = true;
webUi.AlwaysSendClientClaims = true;
webUi.AlwaysIncludeUserClaimsInIdToken = true;
webUi.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
webUi.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
webUi.AllowedScopes.Add("ApplicationApi");
webUi.AllowedScopes.Add("DefinitionApi");
webUi.AllowedScopes.Add("FFAPI");
webUi.ClientUri = "http://localhost:5003";
webUi.RedirectUris.Add("http://localhost:5003/signin-oidc");
webUi.PostLogoutRedirectUris.Add("http://localhost:5003/signout-callback-oidc");
clients.Add(webUi);
//Add IIS test client
Client iisClient = new Client();
iisClient.ClientId = "b8zIsVfAl5hqZ3";
iisClient.ClientSecrets.Add(new Secret("J0MchGJC8RzY7J".Sha256()));
iisClient.ClientName = "IisClient";
iisClient.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
iisClient.RequireConsent = false;
iisClient.AllowOfflineAccess = true;
iisClient.AlwaysSendClientClaims = true;
iisClient.AlwaysIncludeUserClaimsInIdToken = true;
iisClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
iisClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
iisClient.AllowedScopes.Add("ApplicationApi");
iisClient.AllowedScopes.Add("DefinitionApi");
iisClient.AllowedScopes.Add("FFAPI");
iisClient.ClientUri = "http://localhost:8080";
iisClient.RedirectUris.Add("http://localhost:8080/signin-oidc");
iisClient.PostLogoutRedirectUris.Add("http://localhost:8080/signout-callback-oidc");
clients.Add(iisClient);
return clients;
}
}
}
3 - IdentityProfileService.cs
using IdentityServer4.Services;
using System;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityAuthority.Models;
using Microsoft.AspNetCore.Identity;
using IdentityServer4.Extensions;
using System.Linq;
namespace IdentityAuthority.Configs
{
public class IdentityProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
private readonly UserManager<ApplicationUser> _userManager;
public IdentityProfileService(IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, UserManager<ApplicationUser> userManager)
{
_claimsFactory = claimsFactory;
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
if (user == null)
{
throw new ArgumentException("");
}
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
//Add more claims like this
//claims.Add(new System.Security.Claims.Claim("MyProfileID", user.Id));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
}
4 - In my client mvc core project I added 3 nuget packages
.Microsoft.AspNetCore.Authentication.Cookies
.Microsoft.AspNetCore.Authentication.OpenIdConnect
.IdentityModel
5- This is my startup.cs in my client mvc core project
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
//app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
//Setup OpenId and Identity server
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
Authority = "http://localhost:5000",
ClientId = "U2EQlBHfcbuxUo",
ClientSecret = "TbXuRy7SSF5wzH",
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
SaveTokens = true,
RequireHttpsMetadata = false,
GetClaimsFromUserInfoEndpoint = true,
ResponseType = "code id_token",
Scope = { "ApplicationApi", "DefinitionApi", "FFAPI", "openid", "profile", "offline_access" },
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
}
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
6 - In my API I added this nuget package
.IdentityServer4.AccessTokenValidatio
and my startup.cs is like this
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
//IdentityServer4.AccessTokenValidation
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ApiName = "ApplicationApi"
});
app.UseMvc();
}
Now I can use [Authorize(Role="SuperAdmin, Admin")] in both client web app and API app.
User.IsInRole("Admin")
also I have access to claims
HttpContext.User.Claims
var q = (from p in HttpContext.User.Claims where p.Type == "role" select p.Value).ToList();
var q2 = (from p in HttpContext.User.Claims where p.Type == "sub" select p.Value).First();

I would like to provide my own answer after some rigorous research:
During the login process, the server will issue an authentication cookie with some of the claims of the user.
Then, the client will request an access token while providing the claims from the cookie, and the profile service will use the cookie claims to generate the access token claims.
Next, the client will request an id token, but this time it will use the claims from the access token.
Now the thing is, the default profile service of identity server populates the claims of the id token just by using the claims in the access token, while the default profile service of ASP.Net Identity, does look up all the user claims from the database store. This is a point of confusion.
For the identity server implementation, which claims end up in the access token? The claims associated with scopes which are API resources, as opposed to the claims in the id token, which are those associated with scopes which are identity resources.
Summary
Without ASP.NET Identity:
Login - identity server issues a cookie with some claims
Access token query - identity server adds claims from the cookie based on requested api scopes
Id token query - identity server adds claims from the access token based on requested identity scopes
With ASP.NET Identity:
Login - identity server issues a cookie with some claims
Access token query - identity server adds claims from the cookie based on requested api scopes
Id token query - identity server adds claims from the access token and the claims in the user store based on requested identity scopes

You should check context.RequestedClaimTypes and filter out claims, that were not requested.

Related

How to add claims to my accesstoken generated by IdentityServer4 using ClientCredentials grantType

I had developed a WebAPI application and secured my endpoints with OAuth 2.0 protocol using IdentityServer4
My ApiResource looks like:
Name = "BankOfDotNetApi",
Scopes =
{
new Scope("BankOfDotNetApi", "API name for Customer", new List<string>{ "Claim1"}),
new Scope("BankOfDotNetApi.Read"),
new Scope("BankOfDotNetApi.Write"),
new Scope("offline_access"),
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email
},
MyClient looks like:
Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = {new Secret("secret".Sha256())},
AllowedScopes = { "BankOfDotNetApi", "BankOfDotNetApi.Read" },
}
My API application startUp.cs looks like:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
config =>
{
});
services.AddControllers();
services.AddDbContext<BankContext>(options => options.UseInMemoryDatabase("BankingDb"));
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.RequireHttpsMetadata = false;
options.ApiName = "BankOfDotNetApi";
options.Authority = "http://localhost:5000";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I am not generating tokens manually(by creating an instance of JWTToken)and Tokens are automatically generated by IdentityServer4
I am able to access scopes in my access token but I am unable to access Claims.
If my code goes wrong, please suggest to me how and Where to add claims to my ApiResource.
How to access claims in my AccessToken
Use ICustomTokenRequestValidator interface, after token generation, control flow comes in ValidateAsync method.
namespace IdentityServer4.Validation
{
//
// Summary:
// Allows inserting custom validation logic into authorize and token requests
public interface ICustomTokenRequestValidator
{
//
// Summary:
// Custom validation logic for a token request.
//
// Parameters:
// context:
// The context.
//
// Returns:
// The validation result
Task ValidateAsync(CustomTokenRequestValidationContext context);
}
}
Use below line to add custom claim in token.
context.Result.ValidatedRequest.ClientClaims.Add(claim);
Adds the custom authorize request validator using AddCustomTokenRequestValidator in startup class.

How to create DBContext/Tenant Factory per user in Dot Net Core 2.2, JWT

I have two DBContext in my web API app. 1st one is to have all my clients with their connestionstring and 2nd one is real app DB.
Login controler use MyClientContext & other all controllers use MyContext
My startup.cs looks
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
services.AddDbContext<MyContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("MyContext")));
services.AddDbContext<MyClientContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("MyClientContext")));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
var context = serviceScope.ServiceProvider.GetRequiredService<MyContext>();
context.Database.Migrate(); // Code First Migrations for App DB
var context2 = serviceScope.ServiceProvider.GetRequiredService<MyClientContext>();
context2.Database.Migrate(); // Code First Migrations for Clients DB
}
app.UseCors("AllowSpecificOrigin");
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
}
On Login success, I issue JWT Token that looks
private string GenerateJSONWebToken(UserAuth userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserName),
new Claim(JwtRegisteredClaimNames.Email, userInfo.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(_config["Jwt:Issuer"], _config["Jwt:Issuer"], claims,
expires: DateTime.Now.AddHours(24), signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Here, I have assigned ConnectionString for real DB at startup file. I want to assign this, when user logs in. How do I achieve this?
I have done below changes in my code and its working as expected now. It may useful to some one else.
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<MyContext>((serviceProvider, options) =>
{
var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;
var connection = GetConnection(httpContext);
options.UseNpgsql(connection);
});
private string GetConnection(HttpContext httpContext)
{
var UserName = httpContext?.User?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
// Here extract ConnectionString from "MyClientContext"'s DB
}
And for Code First DB Migration, I moved Database.Migrate() to its Class
public MyContext (DbContextOptions<MyContext> options)
: base(options)
{
Database.Migrate();
}
This works fine for me.

IdentityServer4 Role Based Authorization for Web API with ASP.NET Core Identity

I am using IdentityServer4 with .Net Core 2.1 and Asp.Net Core Identity. I have two projects in my Solution.
IdentityServer
Web API
I want to Protect my Web APIs, I use postman for requesting new tokens, It works and tokens are generated successfully. When I use [Authorize] on my controllers without Roles it works perfectly but when I use [Authorize(Roles="Student")] (even with [Authorize(Policy="Student")]) it always return 403 forbidden
Whats wrong with my code
Web API startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization(options => options.AddPolicy("Student", policy => policy.RequireClaim("Role", "Student")))
.AddAuthorization(options => options.AddPolicy("Teacher", policy => policy.RequireClaim("Role", "Teacher")))
.AddAuthorization(options => options.AddPolicy("Admin", policy => policy.RequireClaim("Role", "Admin")))
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
}
Test API :
[Route("api/[controller]")]
[ApiController]
[Authorize(Roles="Student")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<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)
{
}
}
IdentityServer startup.cs
public class Startup
{
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public Startup(IConfiguration configuration, IHostingEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
string connectionString = Configuration.GetConnectionString("DefaultConnection");
string migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
IIdentityServerBuilder builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddAspNetIdentity<ApplicationUser>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
// options.TokenCleanupInterval = 15; // frequency in seconds to cleanup stale grants. 15 is useful during debugging
})
.AddProfileService<ProfileService>();
if (Environment.IsDevelopment())
{
builder.AddDeveloperSigningCredential();
}
else
{
throw new Exception("need to configure key material");
}
services.AddAuthentication();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// InitializeDatabase(app);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
//app.Run(async (context) =>
//{
// await context.Response.WriteAsync("Hello World!");
//});
}
}
}
IdentityServer4 config.cs
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API"),
new ApiResource("roles", "My Roles"),
new IdentityResource("roles", new[] { "role" })
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1","roles" }
}
};
}
}
Token Sample
eyJhbGciOiJSUzI1NiIsImtpZCI6ImU0ZjczZDU5MjQ2YjVjMmFjOWVkNDI2ZGU4YzlhNGM2IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NDYyNTk0NTYsImV4cCI6MTU0NjI2MzA1NiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkxIl0sImNsaWVudF9pZCI6InJvLmNsaWVudCIsInN1YiI6IjIiLCJhdXRoX3RpbWUiOjE1NDYyNTk0NTYsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsicm9sZXMiLCJhcGkxIl0sImFtciI6WyJwd2QiXX0.D6OvbrGx2LwrYSySne59VJ_-_kZ-WriNUbDiETiHO4pknYJzBxKr307DxvBImlvP8w35Cxj3rKxwyWDqVxyhdFhFvFFuHmxqIAv_g2r37lYj3ExcGYAn23Q1i4PuXXBWQe2AHuwFsN2cfPcG39f-N-q7pfLFhoHacXe8vSWyvKxSD0Vj3qVz15cj5VMV1R8qhodXMO-5sZfY1wNfkcJmqmXnbpPnUK_KKUY1Pi6YJkU1nYRXGRoW7YLXc7Y2SFSfa9c1ubU3DDVJV0JqVxSBpfGnvydHEpk-gBx11yQgW5nsJdu6Bi2-DVGA5AdZ_-7pz0AVI-eZPwk2lNtlivmoeA
APS.NET_USERS Table
APS.NET_USERS_Claims Table
Postman
ApiRequest
Claims While Using [Authorize]
The problem is that the claims are not added to the access token.
There are two tokens, the access token and the identity token.
When you want to add claims to the identity token, then you'll have to configure the IdentityResource. If you want to add claims to the access token, then you'll have to configure the ApiResource (or scope).
This should fix it for you:
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API"),
new ApiResource("roles", "My Roles", new[] { "role" })
};
}
Make sure the client (postman) requests the roles scope.
I did test it with the sample code from IdentityServer. In my setup I've added the role 'TestUser' to alice:
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim> { new Claim("role", "TestUser") }
},
The Postman call, please note the requested scope:
The access token including the role claim:
In your Api, somewhere before services.AddAuthentication("Bearer") add a line for JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();.
More info at this post.
EDIT:
Additionally, try to update your identity resources configuration with roles identity resource.
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("roles", new[] { "role" })
};
}
And your client AllowedScopes needs adding roles as well then:
AllowedScopes = { "api1", "roles" }
Lastly, your postman request should then ask for the roles scope to be included scope: api1 roles.
EDIT 2:
Also, update your profile to include roles in the issued claims:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.IssuedClaims.AddRange(context.Subject.Claims);
var user = await _userManager.GetUserAsync(context.Subject);
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));
}
}
The above should probably be updated to only add roles claim when it is requested.
Make sure your newly issued JWT tokens now include roles claim like the one in below:
eyJhbGciOiJSUzI1NiIsImtpZCI6ImU0ZjczZDU5MjQ2YjVjMmFjOWVkNDI2ZGU4YzlhNGM2IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NDY0Mzk0MTIsImV4cCI6MTU0NjQ0MzAxMiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkxIiwicm9sZXMiXSwiY2xpZW50X2lkIjoicm8uY2xpZW50Iiwic3ViIjoiMiIsImF1dGhfdGltZSI6MTU0NjQzOTQxMSwiaWRwIjoibG9jYWwiLCJyb2xlIjpbIkFkbWluIiwiU3R1ZGVudCJdLCJzY29wZSI6WyJvcGVuaWQiLCJhcGkxIiwicm9sZXMiXSwiYW1yIjpbInB3ZCJdfQ.irLmhkyCTQB77hm3XczL4krGMUqAH8izllG7FmQhZIQaYRqI7smLIfrqd6UBDFWTDpD9q0Xx0oefUzjBrwq2XnhGSm83vxlZXaKfb0RdLbYKtC4BlypgTEj8OC-G0ktPqoN1C0lh2_Y2PfKyQYieSRlEXkOHeK6VWfpYKURx6bl33EVDcwe_bxPO1K4axdudtORpZ_4OOkx9b_HvreYaCkuUqzUzrNhYUMl028fPFwjRjMmZTmlDJDPu3Wz-jTaSZ9CHxELG5qIzmpbujCVknh3I0QxRU8bSti2bk7Q139zaiPP2vT5RWAqwnhIeuY9xZb_PnUsjBaxyRVQZ0vTPjQ
I have solved this by adding 'role' in Type column in ApiClaims table see the image below.
ApiResourceId column name found in ApiClaims table is primary key of ApiResources table with Id column name.

How to read IdentityResource scope json data from HttpContext

I am working on an Web API application that will be secured used Identity Server 4. The user is authenticated using implicit flow using a JavaScript client.
I am having problems reading the IdentityResource scopes in my Web API client. I think I might be missing something in the configuration when I invoke app.UseIdentityServerAuthentication( ) in my Startup.cs. I am not seeing the JSON data in any of these IdentityResource scopes when I examine the User.Claims collection.
In the JavaScript client, I am able to view the scope data using the oidc-client.js library. But, I cannot read the scope data in my Web API application.
In the javascript client, I can see the scope data stored as json blob by doing this in TypeScript:
class MyService {
private _userManager: Oidc.UserManager;
private _createUserManager(authority: string, origin: string) {
// https://github.com/IdentityModel/oidc-client-js/wiki#configuration
var config : Oidc.UserManagerSettings = {
authority: authority,
client_id: "myapi",
redirect_uri: `${origin}/callback.html`,
response_type: "id_token token",
scope: "openid profile user.profile user.organization ...",
post_logout_redirect_uri: `${origin}/index.html`
};
this._userManager = new Oidc.UserManager(config);
}
// snip
user() {
this._userManager.getUser().then(user => {
const userProfile = JSON.parse(user.profile["user.profile"]);
console.log(userProfile);
const userOrganization = JSON.parse(user.profile["user.organization"]);
console.log(userOrganization);
});
}
}
In my Web API Project, I have:
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public IContainer Container { get; set; }
public Startup(IHostingEnvironment env)
{
var configurationBuilder = new ConfigurationBuilder();
if (env.IsDevelopment())
{
configurationBuilder
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
env.ConfigureNLog("nlog.development.config");
}
else
{
configurationBuilder.AddEnvironmentVariables();
env.ConfigureNLog("nlog.config");
}
Configuration = configurationBuilder.Build();
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddMvcCore()
.AddJsonFormatters()
.AddAuthorization();
services.AddCors();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRestService, RestService>();
var builder = AutoFacConfig.Create(Configuration);
builder.Populate(services);
Container = builder.Build();
return new AutofacServiceProvider(Container);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors(policy =>
{
policy
.WithOrigins(
"http://app.local:5000"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
ConfigureExceptionHandling(app, env);
ConfigureOidc(app);
app.UseMvc();
}
private void ConfigureExceptionHandling(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
}
private void ConfigureOidc(IApplicationBuilder app)
{
app.UseCors("default");
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = Configuration["IdentityServerUrl"],
AllowedScopes = { "api1", "openid", "profile", "user.profile", "user.organization" ... },
ApiName = "myapi",
RequireHttpsMetadata = false
});
}
}
The problem is when I try to access the claims to read its scopes. When the user is authenticated, I pass the token in the header of the request to the Web API endpoint. I have a BaseController in which I would like to read the scopes from, but cannot.
public class BaseController : Controller
{
private ApplicationUser _user;
public string UserId => CurrentUser?.UserInfo.Id;
public string OrganizationId => CurrentUser?.Organization.Id;
[CanBeNull]
public ApplicationUser CurrentUser
{
get
{
var claims = Request.HttpContext.User.Claims.ToList();
_user = new ApplicationUser
{
UserInfo = claims.Where(f => f.Type == "user.profile").Select(s => s.Value).FirstOrDefault(),
OrganizationInfo = claims.Where(f => f.Type == "user.organization").Select(s => s.Value).FirstOrDefault()
};
return _user;
}
}
}
If, I look at the claims collection, I see the following for Types and values:
nbf: 1499441027
exp: 1499444627
iss: https://identity-service....
aud: https://identity-service..../resources
aud: myapi
client_id: myapi
sub: the_user
auth_time: 1499441027
idp: local
scope: openid
scope: profile
scope: user.profile
scope: user.organization
...
scope: myapi
amr: pwd
How do I read scope data from C#?
For user.organization scope, I am expecting to read a data structure like:
{
"Id":"some id",
"BusinessCategory":"some category",
"Name":"some name"
}
Scope is Type and Profile is Value for one of the scopes, that is why it is like that. What value you want to see for profile?? It doesn't have any value as it is a value itself. You are asking for it:
AllowedScopes = { "api1", "openid", "profile", "user.profile", ... },
and that is what you are getting in your scope in the claims list.
If you want to fetch a particular value from the claims list, you can also use JwtRegisteredClaimNames using
User.FindFirst(JwtRegisteredClaimNames.Email).Value
In case this is useful to anyone. I have implemented a hack. I added extra code in my middleware to call the identity server's user info endpoint. The user info endpoint will return the data that I need. I then manually add claims with this data.
I have added this code to my Configure method:
app.Use(async (context, next) =>
{
if (context.User.Identity.IsAuthenticated)
{
using (var scope = Container.BeginLifetimeScope())
{
// fetch resource identity scopes from userinfo endpoint
var restService = scope.Resolve<IRestService>();
var token = context.Request.Headers["Authorization"]
.ToString().Replace("Bearer ", string.Empty);
restService.SetAuthorizationHeader("Bearer", token);
// fetch data from my custom RestService class and
// serialize into my custom ScopeData object
var data = await restService.Get<ScopeData>(
Configuration["IdentityServerUrl"], "connect/userinfo");
if (data.IsSuccessStatusCode)
{
// add identity resource scopes to claims
var claims = new List<Claim>
{
new Claim("user.organization", data.Result.Organization),
new Claim("user.profile", data.Result.UserInfo)
};
}
else
{
sLogger.Warn("Failed to retrieve identity scopes from profile service.");
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
}
}
}
await next();
});

Cache user profile with external authentication .NET Core IdentityServer4

New to the whole Identity concept but I've had a couple of Google searches and haven't found a reply I felt fitting.
I'm using .NET Core 1.0.0 with EF Core and IdentityServer 4 (ID4).
The ID4 is on a separate server and the only information I get in the client is the claims. I'd like to have access to the full (extended) user profile, preferrably from User.Identity.
So how to I set up so that the User.Identity is populated with all the properties on the ApplicationUser model without sending a DB request every time? I'd like the information to be stored in cache on authentication until the session ends.
What I don't want to do is that in each controller set up a query to get the additional information. All controllers on the client will be inheriting from a base controller, meaning I could DI some service if that's necessary.
Thanks in advance.
Client
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = Configuration.GetSection("IdentityServer").GetValue<string>("Authority"),
RequireHttpsMetadata = false,
ClientId = "RateAdminApp"
});
ID4
app.UseIdentity();
app.UseIdentityServer();
services.AddDeveloperIdentityServer()
.AddOperationalStore(builder => builder.UseSqlServer("Server=localhost;Database=Identities;MultipleActiveResultSets=true;Integrated Security=true", options => options.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)))
.AddConfigurationStore(builder => builder.UseSqlServer("Server=localhost;Database=Identities;MultipleActiveResultSets=true;Integrated Security=true", options => options.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)))
.AddAspNetIdentity<ApplicationUser>();
ApplicationUser Model
public class ApplicationUser : IdentityUser
{
[Column(TypeName = "varchar(100)")]
public string FirstName { get; set; }
[Column(TypeName = "varchar(100)")]
public string LastName { get; set; }
[Column(TypeName = "nvarchar(max)")]
public string ProfilePictureBase64 { get; set; }
}
If you want to transform claims on the identity server, for your case(you use aspnet identity) overriding UserClaimsPrincipalFactory is a solution(see Store data in cookie with asp.net core identity).
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public AppClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{
}
public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
((ClaimsIdentity)principal.Identity).AddClaims(new[] {
new Claim("FirstName", user.FirstName)
});
return principal;
}
}
// register it
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, AppClaimsPrincipalFactory>();
Also you can use events(on the client application) to add extra claims into cookie, it provides claims until the user log out.
There are two(maybe more than) options:
First using OnTicketReceived of openidconnect authentication:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = Configuration.GetSection("IdentityServer").GetValue<string>("Authority"),
RequireHttpsMetadata = false,
ClientId = "RateAdminApp",
Events = new OpenIdConnectEvents
{
OnTicketReceived = e =>
{
// get claims from user profile
// add claims into e.Ticket.Principal
e.Ticket = new AuthenticationTicket(e.Ticket.Principal, e.Ticket.Properties, e.Ticket.AuthenticationScheme);
return Task.CompletedTask;
}
}
});
Or using OnSigningIn event of cookie authentication
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
Events = new CookieAuthenticationEvents()
{
OnSigningIn = async (context) =>
{
ClaimsIdentity identity = (ClaimsIdentity)context.Principal.Identity;
// get claims from user profile
// add these claims into identity
}
}
});
See similar question for solution on the client application: Transforming Open Id Connect claims in ASP.Net Core

Categories

Resources