getting claims from IdentityServer resource scope - c#

I am having trouble getting IdentityServer to return any claims on a resource scope. I define the resource scope as:
private static Scope Roles
{
get
{
return new Scope
{
Name = "roles",
DisplayName = "Roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(SecurityContants.ClaimTypes.Role)
}
};
}
}
private static Scope Api
{
get
{
return new Scope
{
Name = "api",
DisplayName = "Api",
IncludeAllClaimsForUser = true,
//i've also tried adding the claim directly
Type = ScopeType.Resource,
Emphasize = false
};
}
}
Then they're added to a collection and handed back.
In my API Startup.cs, I configure owin like:
public void Configuration (IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost/security.tokenserver.site",
RequiredScopes = new[] { "api", "roles" },
RoleClaimType = "roles"
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
My API endpoint is simply:
public IEnumerable<string> Get()
{
var user = User as ClaimsPrincipal;
var claims = new List<string>();
foreach (var item in user.Claims)
{
claims.Add(item.Value);
}
return claims;
}
The output of this, when called from my UI after authenticating is:
[ "customer_svc", "api", "https://idsrv3/embedded", "https://idsrv3/embedded/resources", "1448048492", "1448044892" ]
So the initial login at the UI works (i see the roles claim there), retrieving the token works, and handing the token up to the API works. But I cannot see the user's roles after the token is handed back up to the API.
What am I missing?

Related

Angular oidc-client and duende IdentityServer - Problem with authorize on client-site, user unauthorized

I'm learning angular and I find a problem that I didn't see anywhere else. To the point, when I'm trying to login to my client angular app with oidc-client via identityserver, It's working great up to a point. I'm redirecting to a identityserver, logged in, goes back to the login-callback, in server-side i'm authorized, but if I want to check if I'm authorized in client-side, I got a null user. If i want to logout, I got redirect to sign out in identityserver where I can see my token:
Server-side config.cs:
public class Config
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
new IdentityResource("RedDot.API.read", new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location" })
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("RedDot.API.read", "Resource API")
{
ApiSecrets =
{
new Secret("secret".Sha256())
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email
},
Scopes = new List<string> { "RedDot.API.read" }
}
};
}
public static IEnumerable<Client> GetClients()
{
return new[]
{
new Client {
RequireConsent = false,
ClientId = "reddot_ui",
ClientName = "RedDot",
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"RedDot.API.read"
},
RedirectUris = {"https://localhost:4200/login-callback"},
PostLogoutRedirectUris = {"https://localhost:4200/"},
AllowedCorsOrigins = {"https://localhost:4200"},
AllowAccessTokensViaBrowser = true,
AccessTokenLifetime = 3600,
ClientSecrets =
{
new Secret("rwebv832hvegfh49--l-w".Sha256())
},
}
};
}
}
Program.cs:
builder.Services.AddDbContext<AppIdentityDbContext>(options => options.UseSqlServer(dataConnectionString, conf => conf.MigrationsAssembly(assembly)));
builder.Services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer().AddDeveloperSigningCredential()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(dataConnectionString, conf => conf.MigrationsAssembly(assembly));
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<AppUser>();
Client-side angular config:
private getUserManager() {
if (!this._userManager) {
const userManagerSettings: UserManagerSettings =
new UserManagerSettings();
userManagerSettings.authority = 'https://localhost:5443';
userManagerSettings.client_id = 'reddot_ui';
userManagerSettings.response_type = 'code';
userManagerSettings.scope = 'openid profile RedDot.API.read';
userManagerSettings.redirect_uri = 'https://localhost:4200/login-callback';
userManagerSettings.post_logout_redirect_uri = 'https://localhost:4200/logout-callback';
userManagerSettings.automaticSilentRenew = true;
userManagerSettings.silent_redirect_uri = 'https://localhost:4200/silent-callback';
userManagerSettings.userStore = new WebStorageStateStore({
store: window.localStorage,
}); // store information about Authentication in localStorage
this._userManager = new UserManager(userManagerSettings);
this._userManager.getUser().then((user) => {
this._user = user;
this.isUserDefined = true;
});
}
}
Rest of authorize class in angular:
import { Injectable } from "#angular/core";
import { User, UserManager, WebStorageStateStore } from "oidc-client";
import { UserManagerSettings } from "../_models/usermanager.settings";
#Injectable()
export class AuthenticationService {
isUserDefined = false;
private _user: User | null;
private _userManager: UserManager;
isLoggedIn() {
return this._user != null && !this._user.expired;
}
getName() {
return this._user?.profile.nickname;
}
getAccessToken() {
return this._user ? this._user.access_token : "";
}
getClaims() {
return this._user?.profile;
}
startAuthentication() : Promise<void> {
this.getUserManager();
return this._userManager.signinRedirect();
}
completeAuthentication() {
this.getUserManager();
return this._userManager.signinRedirectCallback().then((user) => {
this._user = user;
this.isUserDefined = true;
});
}
startLogout(): Promise<void> {
this.getUserManager();
return this._userManager.signoutRedirect();
}
completeLogout() {
this.getUserManager();
this._user = null;
return this._userManager.signoutRedirectCallback();
}
silentSignInAuthentication() {
this.getUserManager();
return this._userManager.signinSilentCallback();
}
}
Dunno where the problem can be and why I'm not authorized on client-side.
I've tried to change response type and protocol from http to https and conversely with no effect. Maybe somebody had the same problem.
The code you provided doesn't look like there is anything wrong. According to your description, you can log in and log out successfully, but you are not authorized. Does it mean that you cannot access the corresponding resources? Do you have any error messages?
I used https://demo.duendesoftware.com site to test with Angular application (here is the code of Angular), but did not reproduce your situation. You can see how the configuration items in it perform, and whether your configuration is missing some steps compared to it.
In addition, I found a sample code that is similar to your Angular program, but the Scope configuration is slightly different from yours. I am not sure if this is the reason, but you can use it as a reference

When using HttpClient with .AddClientAccessTokenHandler() extension, role claim is not included

I am using IdentityModel.AspNetCore and .AddClientAccessTokenHandler() extension to automatically supply HttpClient with access token (at least that is what I understand I can use it for) to an API. Some API endpoints are authorized based on a role. But for some reason, the access token that is added to the request does not contain the role claim. If I do not use the .AddClientAccessTokenHandler() and manually retrieve the token and set it using SetBearerToken(accessTone) then I can reach my role authorized endpoint.
My startup is:
services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("auth", new ClientCredentialsTokenRequest
{
Address = "https://localhost:44358/connect/token",
ClientId = "clientId",
ClientSecret = "clientSecret",
});
});
WebApi call:
var response = await _httpClient.GetAsync("api/WeatherForecast/SecretRole");
Identity server configuration:
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource>
{
new ApiResource("WebApi", new string[] { "role" })
{ Scopes = { "WebApi.All" }}
};
public static IEnumerable<ApiScope> GetApiScopes() =>
new List<ApiScope>
{ new ApiScope("WebApi.All") };
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "roles",
UserClaims = { "role" }
}
};
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "clientId",
ClientSecrets = { new Secret("clientSecret".ToSha256()) },
AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials
},
AllowedScopes =
{
"WebApi.All",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles"
},
RedirectUris = { "https://localhost:44305/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44305/Home/Index" },
AlwaysIncludeUserClaimsInIdToken = false,
AllowOfflineAccess = true,
}
};
For testing purposes I add users manually from Program.cs
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
AddUsers(userManager).GetAwaiter().GetResult();
}
host.Run();
}
private static async Task AddUsers(UserManager<IdentityUser> userManager)
{
var adminClaim = new Claim("role", "Admin");
var visitorClaim = new Claim("role", "Visitor");
var user = new IdentityUser("Admin");
await userManager.CreateAsync(user, user.UserName);
await userManager.AddClaimAsync(user, adminClaim);
user = new IdentityUser("Visitor");
await userManager.CreateAsync(user, user.UserName);
await userManager.AddClaimAsync(user, visitorClaim);
}
So if I use manual access token retrieval and add it myself to the HttpClient headers, then my endpoint is reached and returns expected response. If I use .AddClientAccessTokenHandler(), I get 403 - Forbidden. What am I missing?
Since you are registering the client under the name auth, you also should retrieve it as such.
This basically means I expect you to use something like this, or it's equivalent:
_httpClient = factory.CreateClient("auth");
Basically this mechanism ensures you're able to retrieve HttpClients for various API's and settings.
ps. I am on mobile; and currently not very good access to my resources.

After updating the identity server 4 4.0.0 from 3.1.4 Scope is invalid asp.net core 3 with Mongo DB

In the discovery document, the scope IdentityPortal.API is not added
{
"issuer": "https://localhost:5001",
"scopes_supported": ["profile", "openid", "email", "offline_access"],
}
However, allowed scope in the config is as below
private static string apiScope = "IdentityPortal.API";
private static ICollection<string> AllowedScopes()
{
return new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
apiScope
};
}
API Resource
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource(apiScope, "Falcon Api")
{
Scopes = new List<string>{apiScope},
UserClaims =
{
JwtClaimTypes.Profile,
JwtClaimTypes.Name,
JwtClaimTypes.Email,
}
}
};
}
From the React Application I am sending scope as below
scope: "profile openid email IdentityPortal.API offline_access",
In the identity server IdentityPortal.API is not added as supported claim.
Here is the customPersistedGrantStore.cs
public class CustomResourceStore : IResourceStore
{
protected IRepository _dbRepository;
public CustomResourceStore(IRepository repository)
{
_dbRepository = repository;
}
public Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
{
var list = _dbRepository.Where<IdentityResource>(e => scopeNames.Contains(e.Name));
return Task.FromResult(list.AsEnumerable());
}
public Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)
{
var list = _dbRepository.Where<ApiScope>(a => scopeNames.Contains(a.Name));
return Task.FromResult(list.AsEnumerable());
}
public Task<IEnumerable<ApiResource>> FindApiResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
{
var list = _dbRepository.Where<ApiResource>(a => a.Scopes.Any(s => scopeNames.Contains(s)));
return Task.FromResult(list.AsEnumerable());
}
public Task<IEnumerable<ApiResource>> FindApiResourcesByNameAsync(IEnumerable<string> apiResourceNames)
{
var list = _dbRepository.Where<ApiResource>(a => apiResourceNames.Contains(a.Name));
return Task.FromResult(list.AsEnumerable());
}
public Task<Resources> GetAllResourcesAsync()
{
var result = new Resources(GetAllIdentityResources(), GetAllApiResources(),null);
return Task.FromResult(result);
}
private IEnumerable<IdentityResource> GetAllIdentityResources()
{
return _dbRepository.All<IdentityResource>();
}
private IEnumerable<ApiResource> GetAllApiResources()
{
return _dbRepository.All<ApiResource>();
}
private IEnumerable<ApiScope> GetAllApiScopes()
{
return _dbRepository.All<ApiScope>();
}
}
Identity server setup
services.Configure<MongoDbConfigurationOptionsViewModel>(Configuration);
services.AddIdentityServer()//.AddProfileService<ProfileService>()
.AddMongoRepository()
.AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>(Configuration)
.AddClients()
.AddInMemoryApiScopes(Config.AllowedScopes())
.AddIdentityApiResources()
.AddPersistedGrants()
.AddDeveloperSigningCredential();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:5001";
// name of the API resource
options.ApiName = "IdentityPortal.API";
});
Config
public static IEnumerable<ApiScope> AllowedScopes()
{
return new List<ApiScope>
{
new ApiScope(IdentityServerConstants.StandardScopes.OpenId),
new ApiScope(IdentityServerConstants.StandardScopes.Profile),
new ApiScope(IdentityServerConstants.StandardScopes.Email),
new ApiScope(apiScope)
};
}
Issue is that you just added the api resources on IDS4 setup, you need to change your code to add API scopes too. To add api scopes you have above you can add it via AddInMemoryApiScopes. Code would like this:
services.Configure<MongoDbConfigurationOptionsViewModel>(Configuration);
services.AddIdentityServer()//.AddProfileService<ProfileService>()
.AddMongoRepository()
.AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>(Configuration)
.AddClients()
.AddInMemoryApiScopes(Config.AllowedScopes)
.AddIdentityApiResources()
.AddPersistedGrants()
.AddDeveloperSigningCredential();
Once made the code change, regenerate the token and check it on https://jwt.ms/ you should have a prop as aud = IdentityPortal.API and also scope as IdentityPortal.API
As you are using DB, you need to migrate your DB to new version first, here are scripts to help on this: https://github.com/RockSolidKnowledge/IdentityServer4.Migration.Scripts/tree/CreateScripts
After DB update make sure you have data on api resource and also api resource's scope matching the required scopes
Checkout my blog post https://github.com/nahidf-adventures/IdentityServer4-adventures/tree/ids4-4/src/IdentityServer for more detailed explanation.
Read more on official docs here

Role based authorization in asp core 2 with jwt tokens and identity

I have tried some authorization examples in asp core 2 wiht jwt tokens and asp core identity. I have followed this code https://github.com/SunilAnthony/SimpleSecureAPI and it works fine.
The problem is role based authorization. I tried something like this:
http://www.jerriepelser.com/blog/using-roles-with-the-jwt-middleware/
and the result is strange.
My controller method:
[HttpGet]
[Authorize]
public IActionResult Get()
{
IEnumerable<Claim> claims = User.Claims; // contains claim with Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" and Value = "Administrator"
bool role = User.IsInRole("Administrator"); // true
bool claim = User.HasClaim(ClaimTypes.Role, "Administrator"); // true
return Ok(claims);
}
When I call this endpoint just with attribute [Authorize] and check role / claims in code for current user it seems good (both checks are true), but when I have changed my authorization attribute to [Authorize(Roles = "Administrator")] it does not work -> when I call this endpoint with this attribute I will receive 404. I don't know where is the problem. My startup class is completely same as in the git link above and I have just added list of string role names in the payload of access_token within then "roles" array:
It is hardcoded but I have changed my login method just for test like this:
[HttpPost("login")]
public async Task<IActionResult> SignIn([FromBody] Credentials Credentials)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(Credentials.Email, Credentials.Password, false, false);
if (result.Succeeded)
{
IdentityUser user = await _userManager.FindByEmailAsync(Credentials.Email);
List<string> roles = new List<string>();
roles.Add("Administrator");
return new JsonResult(new Dictionary<string, object>
{
{ "access_token", GetAccessToken(Credentials.Email, roles) },
{ "username", user.Email },
{ "expired_on", DateTime.UtcNow.AddMinutes(_tokenLength) },
{ "id_token", GetIdToken(user) }
});
}
return new JsonResult("Unable to sign in") { StatusCode = 401 };
}
return new JsonResult("Unable to sign in") { StatusCode = 401 };
}
And the GetAccessTokenMethod:
private string GetAccessToken(string Email, List<string> roles)
{
var payload = new Dictionary<string, object>
{
{ "sub", Email },
{ "email", Email },
{ "roles", roles },
};
return GetToken(payload);
}
Where is the problem with the [Authorize(Roles = "Administrator")] attribute?
The problem is with the claim's type:
http://schemas.microsoft.com/ws/2008/06/identity/claims/role
Somehow, the [Authorize] attribute fails to work when used with Roles value.
So this [Authorize(Roles = "Administartor")] doesn't work.
You have to map the claim type to only role by applying transformation on Startup class.
If you have an Owin based project:
app.UseClaimsTransformation(incoming =>
{
// either add claims to incoming, or create new principal
var appPrincipal = new ClaimsPrincipal(incoming);
if (appPrincipal.HasClaim(x => x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"))
{
var value = appPrincipal.Claims.First(x =>
x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role").Value;
incoming.Identities.First().AddClaim(new Claim("role", value));
}
return Task.FromResult(appPrincipal);
});

IdentityServer4 : How so you include additional claims and get the value in an API controller?

How to include email in the claim and get the email value via the API Controller?
IdentityServer4 Sample: https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Quickstarts/8_EntityFrameworkStorage
API IdentityController https://github.com/IdentityServer/IdentityServer4.Samples/blob/release/Quickstarts/8_EntityFrameworkStorage/src/Api/Controllers/IdentityController.cs
TestUser https://github.com/IdentityServer/IdentityServer4.Samples/blob/release/Quickstarts/8_EntityFrameworkStorage/src/QuickstartIdentityServer/Quickstart/TestUsers.cs
When you define an api resource (have a look in Config.cs), you can do that :
new ApiResource
{
Name = "api",
DisplayName = "My API",
UserClaims =
{
JwtClaimTypes.Id,
JwtClaimTypes.Subject,
JwtClaimTypes.Email
}
}
It defines that your API will receive those claims.
EDIT :
It's better if you add the associate resource's to the GetIdentityResources function (see Config.cs)
Have a glance in the offical documentation to have a better picture
http://docs.identityserver.io/en/release/topics/resources.html .
I give you a complete example from a personal project:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
//>Declaration
var lIdentityResources = new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
//>Processing
foreach (var lAPIResource in GetApiResources())
{
lIdentityResources.Add(new IdentityResource(lAPIResource.Name,
lAPIResource.UserClaims));
}
//>Return
return lIdentityResources;
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource
{
Name = "api1",
DisplayName = "api1 API",
UserClaims =
{
JwtClaimTypes.Id,
JwtClaimTypes.Subject,
JwtClaimTypes.Email
}
}
};
}

Categories