Added Custom Claim, showing in ID token missing in Access token - identityserver4

I have a .NET Core Identity Provider (which also uses IdentityServer4) which authenticates SPA applications with Azure AD. I am adding an "oid" claim with the object identifier value received from Azure. The problem is that from the SPA application I can see the "oid" claim in the ID token but cannot see it in the access token. I need the oid in the access token as well. Here is the relevant code:
Startup.cs
services.AddAuthentication()
.AddCookie("Cookies", options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
})
.AddOpenIdConnect(ActiveDirectoryTenants.TenantA, ActiveDirectoryTenants.TenantA, options => Configuration.Bind("TenantAAzureAd", options))
.AddOpenIdConnect(ActiveDirectoryTenants.TenantB, ActiveDirectoryTenants.TenantB, options => Configuration.Bind("TenantBAzureAd", options));
AddActiveDirectoryOpenIdConnectOptions(services, ActiveDirectoryTenants.TenantA);
AddActiveDirectoryOpenIdConnectOptions(services, ActiveDirectoryTenants.TenantB);
I have a common function to add other options to these configurations. I tried to add the oid claim in OnTokenValidated but didn't receive the oid claim in the access token.
protected virtual void AddActiveDirectoryOpenIdConnectOptions(IServiceCollection services, string tenant)
{
services.Configure<OpenIdConnectOptions>(tenant, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = ctx =>
{
ctx.ProtocolMessage.LoginHint = ctx.Properties.GetString("username");
return Task.CompletedTask;
},
OnTokenValidated = ctx =>
{
//Maybe need to add oid here???
}
};
});
}
The oid claim is being added after successfully logging in to Azure AD.
AccountController.cs
public async Task<IActionResult> ExternalLoginCallback(string returnUrl, string remoteError = null, string openIdScheme = null)
{
var authResult = await HttpContext.AuthenticateAsync(openIdScheme ?? ActiveDirectoryTenants.TenantA);
var externalUser = authResult.Principal;
var claims = externalUser.Claims.ToList();
var applicationUser = //gets the user based on the email found in claims, omitted for brevity
await userManager.AddClaimAsync(applicationUser, new Claim("oid", claims.First(x => x.Type == http://schemas.microsoft.com/identity/claims/objectidentifier).Value));
await signInManager.SignInAsync(applicationUser, false, "AzureAD");
return Redirect("~/");
}
The ID token received in the SPA application (note the oid claim):
{
"nbf": xxx,
"exp": xxx,
"iss": "https://localhost:3000",
"aud": "xxx-spa-test",
"iat": xxx,
"at_hash": "",
"s_hash": "",
"sid": "",
"sub": "guid",
"auth_time": 1620026953,
"idp": "AzureAD",
"display_name": "Test User",
"oid": "guid",
"role": [
"Staff",
],
"name": "test#azureaddomain",
"amr": [
"external"
]
}
The access token received in the SPA application (note the missing oid claim):
{
"nbf": xxx,
"exp": xxx,
"iss": "https://localhost:3000",
"aud": [
"https://localhost:3000/resources",
"xxx-api-test-scope"
],
"client_id": "xxx-spa-test",
"sub": "guid",
"auth_time": 1620026953,
"idp": "AzureAD",
"role": [
"Staff",
],
"name": "test#azureaddomain",
"scope": [
"openid",
"profile",
"xxx-api-test-scope"
],
"amr": [
"external"
]
}

For the claim to end up in the access token, you need to add a ApiScope and add the Userclaim name to it. Alternatively, add an ApiScope and an ApiResource that contains the UserClaim.
Like
var apiresource1 = new ApiResource()
{
Name = "apiresource1", //This is the name of the API
ApiSecrets = new List<Secret>
{
new Secret("myapisecret".Sha256())
},
Description = "This is the order Api-resource description",
Enabled = true,
DisplayName = "Orders API Service",
Scopes = new List<string> { "apiscope1"},
UserClaims = new List<string>
{
//Custom user claims that should be provided when requesting access to this API.
//These claims will be added to the access token, not the ID-token!
"apiresource1-userclaim3",
}
};
See my answer here for more details
To complement this answer, I write a blog post that goes into more detail about this topic:
IdentityServer – IdentityResource vs. ApiResource vs. ApiScope

Related

Identity server 4 Obtain role from token in webApi app

I have correctly configured identity server 4 which authorizes a web api for method access. However, I cannot use the roles in the web api, the role is in the token but when it arrives on the web api it does not give me authorization to enter the api.
IDS4 Configuration
new Client
{
ClientId = "spaclient",
ClientName = "SPA Client",
RequireConsent = false,
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
RequirePkce = true,
RequireClientSecret = false,
AllowAccessTokensViaBrowser = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"role"
}
}
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("spaclient", "SPA")
};
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("spaclient", "SPA")
};
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("role","User Role", new List<string>() { "role" })
};
CLIENT CONFIG
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:9002"; // --> IdentityServer Project
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
NameClaimType = "role",
RoleClaimType = "role"
};
});
CONTROLLER PART
[HttpGet]
[Authorize(Roles ="Administrator")] // <-- with role not work
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
[HttpGet]
[Authorize]<-- without role work fine
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
In your access token, there is no role claim. You need to configure your existing ApiScope or ApiResource to include the necessar role claim.
What you have done is to only include it in your ID-token.
see my answer here about the relationship between the various resource types in IdentityServer
To add a userclaim to your APIScope, like this:
new ApiScope(name: "spaclient",
displayName:"SPA",
userClaims: new List<string>{ "role" }),
Also, you must request the spaclient and openid scopes as well.
To control the token lifetimes:
var client2 = new Client
{
ClientId = "authcodeflowclient",
IdentityTokenLifetime = 300, //5 minutes
AccessTokenLifetime = 3600, //1 hour
AuthorizationCodeLifetime = 300, //5 minutes
AbsoluteRefreshTokenLifetime = 2592000, //30 days
SlidingRefreshTokenLifetime = 1296000, //15 days
...
To complement this answer, I write a blog post that goes into more detail about this topic:
IdentityServer – IdentityResource vs. ApiResource vs. ApiScope

How to validate Identityserver4 token validation in webapi?

Have you tried to ID4 connect with web api (.net framework 4.6) I follow below the tutorial but APIResource with secret key is not working. it also not giving any error if I give wrong API resource name and secret.
https://nahidfa.com/posts/identityserver4-and-asp-.net-web-api/
Source code
var IDSBearerOption = new IdentityServerBearerTokenAuthenticationOptions
{
AuthenticationType = "Bearer",
Authority = "https://localhost:5000",
ValidationMode = ValidationMode.Local,
RequiredScopes = new[] { "api1" },
PreserveAccessToken = true,
RoleClaimType = "role",
ValidAudiences = new[] { "TestAPI1" } ,
ClientId = "TestAPI1", //api resource name
ClientSecret = "secret1" //api resource secret
};
app.UseIdentityServerBearerTokenAuthentication(IDSBearerOption);
Is it possible to validate the token in webapi .net framework4.6?
You'll have to share cookies and set your bearer token auth options inside OWIN in startup:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
CookieHttpOnly = false,
CookieSecure = CookieSecureOption.Always,
CookieName = "MySharedCookieName"
});
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = authorityUrl,
RequiredScopes = new[] {"MyAPIName"},
ClientId = "MyAPIName",
ClientSecret = "secret",
AuthenticationType = "Bearer",
NameClaimType = "name",
RoleClaimType = "role",
SigningCertificate = x509Cert
});
Just make sure your cookie names match in IDS4 and all other apps

Identity Server 4 Checking for expected scope openid failed

I'm trying to set up an Identity Server for the first time in ASP.NET Core. I've set up everything to use a database and have created a script to create a test client, test user and resources. I can request a client token and request a user token, those work fine, but when calling the connect/userinfo endpoint, I'm getting a Forbidden response and the following error;
IdentityServer4.Validation.TokenValidator[0]
Checking for expected scope openid failed
{
"ValidateLifetime": true,
"AccessTokenType": "Jwt",
"ExpectedScope": "openid",
"Claims": {
"nbf": 1556641697,
"exp": 1556645297,
"iss": "https://localhost:5001",
"aud": [
"https://localhost:5001/resources",
"customAPI"
],
"client_id": "newClient",
"sub": "75f86dd0-512e-4c9d-b298-1afa120c7d47",
"auth_time": 1556641697,
"idp": "local",
"role": "admin",
"scope": "customAPI.read",
"amr": "pwd"
}
}
I'm not sure what is causing the issue. Here is the script I used to setup the test entities;
private static void InitializeDbTestData(IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
scope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>().Database.Migrate();
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
var context = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
// API Client
Client client = new Client
{
ClientId = "newClient",
ClientName = "Example Client Credentials Client Application",
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
ClientSecrets = new List<Secret>
{
new Secret("123456789".Sha256())
},
AllowedScopes = new List<string> {"customAPI.read"}
};
context.Clients.Add(client.ToEntity());
context.SaveChanges();
// Identity Resources
IList<IdentityResource> identityResources = new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
foreach (IdentityResource identityResource in identityResources)
{
context.IdentityResources.Add(identityResource.ToEntity());
}
// API Resource
ApiResource resource = new ApiResource
{
Name = "customAPI",
DisplayName = "Custom API",
Description = "Custom API Access",
UserClaims = new List<string> {"role"},
ApiSecrets = new List<Secret> {new Secret("scopeSecret".Sha256())},
Scopes = new List<Scope>
{
new Scope("customAPI.read"),
new Scope("customAPI.write")
}
};
context.ApiResources.Add(resource.ToEntity());
context.SaveChanges();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
// User
IdentityUser user = new IdentityUser
{
UserName = "JohnDoe",
Email = "john#doe.co.uk",
};
IList<Claim> claims = new List<Claim>
{
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.Role, "admin")
};
userManager.CreateAsync(user, "112222224344").Wait();
userManager.AddClaimsAsync(user, claims).Wait();
}
}
I'm sure I've set up something wrong when I set up the client/user, can anyone pinpoint what it is?
Can't see your client side code, but the error says you did not requested openid scope when applied for the token. The token valid for Useinfo endpoint must contain openid scope.

How to correctly protect API in STS Asp.Net 2.2 project + Api Asp.Net 2.2 project + angular7 client

Have been struggling with this for a while, no problem having angular7 client access id4 Asp.Net core 2.2 project for logging in and getting jwt back, struggling with angular7 client accessing Asp.Net core 2.2 api project protected api which is under id4 protection.
jwt.io decode (values x'd out):
HEADER:ALGORITHM & TOKEN TYPE
{
"alg": "RS256",
"kid": "c672fc19f3ff652c5c8816cfac31bfcc",
"typ": "JWT"
}
PAYLOAD:DATA
{
"nbf": 1550161736,
"exp": 1550164736,
"iss": "https://localhost:44340",
"aud": "angularclient",
"nonce": "N0.88924643059608991550161727071",
"iat": 1550161736,
"at_hash": "A3fYyAynZIUQN5Z3ugvpvw",
"sid": "90c459301964e9f136a38b9b19d9b1e0",
"sub": "71765055-647D-432E-AFB6-0F84218D0247",
"auth_time": 1550161731,
"idp": "local",
"preferred_username": "xxxxxxxx",
"name": "xxxxxxxxxx",
"regid": "xxxxxxxxx",
"jseg": "xxxxx",
"jobid": "xxxxx",
"role": "xxxx",
"given_name": "xxxx",
"family_name": "Grexxxxenwald",
"email": "xxxxx",
"amr": [
"pwd"
]
}
ID4 config:
private static readonly string[] customClaimTypes = { "role", "jseg", "jobid", "regid", "api1" };
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("api1scope", customClaimTypes),
};
}
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[]
{
new ApiResource()
{
Name = "api1",
Description = "tsicApis",
ApiSecrets =
{
new Secret(Startup.Configuration.GetSection("StsConfig:STSTSICApisSecuredSecret").Value.Sha256())
},
Scopes =
{
new Scope()
{
Name = "api1",
DisplayName = "Scope for the api1 ApiResource",
},
},
UserClaims = customClaimTypes
}
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
var trustedClientSecrets = Startup.Configuration.GetSection("StsConfig:TrustedClientSecrets").Value;
var angularClientUrl = Startup.Configuration.GetSection("StsConfig:AngularClientUrl").Value;
var angularRedirectUris = Startup.Configuration.GetSection("StsConfig:AngularRedirectUris").Value;
var angularPostLogoutRedirectUris = Startup.Configuration.GetSection("StsConfig:AngularPostLogoutRedirectUris").Value;
var angularAllowedCorsOrigins = Startup.Configuration.GetSection("StsConfig:AngularAllowedCorsOrigins").Value;
var angularClientSecret = Startup.Configuration.GetSection("StsConfig:STSTSICApisSecuredSecret").Value;
var mvcClientSecrets = Startup.Configuration.GetSection("StsConfig:MVCClientSecrets").Value;
var mvcRedirectUris = Startup.Configuration.GetSection("StsConfig:MVCRedirectUris").Value;
var mvcFrontChannelLogoutUri = Startup.Configuration.GetSection("StsConfig:MVCFrontChannelLogoutUri").Value;
var mvcPostLogoutRedirectUris = Startup.Configuration.GetSection("StsConfig:MVCPostLogoutRedirectUris").Value;
// client credentials client
return new List<Client>
{
new Client
{
ClientName = "angularclient",
ClientId = "angularclient",
RequireClientSecret = true,
ClientSecrets = { new Secret(angularClientSecret) },
RequireConsent = true,
AllowRememberConsent = false,
AccessTokenType = AccessTokenType.Jwt,
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenLifetime = 33000,// 330 seconds, default 60 minutes
IdentityTokenLifetime = 3000,
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Implicit,
AllowedCorsOrigins = angularAllowedCorsOrigins.Split(','),
AllowedScopes =
{
"openid",
"profile",
"email",
"role",
"jseg",
"jobid",
"regid",
"api1",
"api1scope",
},
RedirectUris = angularRedirectUris.Split(','),
PostLogoutRedirectUris = angularPostLogoutRedirectUris.Split(',')
},
new Client
{
ClientId = "mvcclient",
ClientName = "mvcclient",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = { new Secret(mvcClientSecrets.Sha256()) },
RequireConsent = true,
AllowRememberConsent = false,
RedirectUris = mvcRedirectUris.Split(','),
FrontChannelLogoutUri = mvcFrontChannelLogoutUri,
PostLogoutRedirectUris = mvcPostLogoutRedirectUris.Split(','),
AllowOfflineAccess = true,
AllowedScopes = new List<string>
{
"openid",
"profile",
"api1"
}
},
};
}
Angular app.module.ts:
export class AppModule {
constructor(
private oidcSecurityService: OidcSecurityService
) {
const openIDImplicitFlowConfiguration = new OpenIDImplicitFlowConfiguration();
openIDImplicitFlowConfiguration.storage = sessionStorage;
openIDImplicitFlowConfiguration.stsServer = environment.oidc.stsServer;
openIDImplicitFlowConfiguration.redirect_url = environment.oidc.redirect_url;
// The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer
// identified by the iss (issuer) Claim as an audience.
// The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
// or if it contains additional audiences not trusted by the Client.
openIDImplicitFlowConfiguration.client_id = 'angularclient';
openIDImplicitFlowConfiguration.response_type = 'id_token token';
openIDImplicitFlowConfiguration.scope = 'openid profile email api1scope';
openIDImplicitFlowConfiguration.post_logout_redirect_uri = environment.oidc.post_logout_redirect_uri;
// openIDImplicitFlowConfiguration.start_checksession = this.oidcConfigService.clientConfiguration.start_checksession;
openIDImplicitFlowConfiguration.silent_renew = true;
openIDImplicitFlowConfiguration.silent_renew_url = environment.oidc.silent_renew_url;
openIDImplicitFlowConfiguration.post_login_route = environment.oidc.post_login_route;
// HTTP 403
openIDImplicitFlowConfiguration.forbidden_route = '/forbidden';
// HTTP 401
openIDImplicitFlowConfiguration.unauthorized_route = '/unauthorized';
openIDImplicitFlowConfiguration.log_console_warning_active = environment.oidc.log_console_warning_active;
openIDImplicitFlowConfiguration.log_console_debug_active = environment.oidc.log_console_debug_active;
// id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time,
// limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific.
openIDImplicitFlowConfiguration.max_id_token_iat_offset_allowed_in_seconds = environment.oidc.max_id_token_iat_offset_allowed_in_seconds;
// openIDImplicitFlowConfiguration.iss_validation_off = false;
// configuration.FileServer = this.oidcConfigService.clientConfiguration.apiFileServer;
// configuration.Server = this.oidcConfigService.clientConfiguration.apiServer;
const authWellKnownEndpoints = new AuthWellKnownEndpoints();
authWellKnownEndpoints.issuer = environment.oidc.stsServer;
authWellKnownEndpoints.jwks_uri = `${environment.oidc.stsServer}/.well-known/openid-configuration/jwks`;
authWellKnownEndpoints.authorization_endpoint = `${environment.oidc.stsServer}/connect/authorize`;
authWellKnownEndpoints.token_endpoint = `${environment.oidc.stsServer}/connect/token`;
authWellKnownEndpoints.userinfo_endpoint = `${environment.oidc.stsServer}/connect/userinfo`;
authWellKnownEndpoints.end_session_endpoint = `${environment.oidc.stsServer}/connect/endsession`;
authWellKnownEndpoints.check_session_iframe = `${environment.oidc.stsServer}/connect/checksession`;
authWellKnownEndpoints.revocation_endpoint = `${environment.oidc.stsServer}/connect/revocation`;
authWellKnownEndpoints.introspection_endpoint = `${environment.oidc.stsServer}/connect/introspect`;
this.oidcSecurityService.setupModule(
openIDImplicitFlowConfiguration,
authWellKnownEndpoints
);
}
}
Asp.Net core 2.2 api project start.cs:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServer4Strings:Authority");
options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer4Strings:RequireHttpsMetadata");
options.ApiName = Configuration.GetValue<string>("IdentityServer4Strings:ApiName");
options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt;
options.ApiSecret = Configuration.GetValue<string>("IdentityServer4Strings:STSTSICApisSecuredSecret");
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10); // that's the default
});
Asp.Net core 2.2 sts project start.cs:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var identityServer = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddProfileService<IdentityWithAdditionalClaimsProfileService>()
//.AddTestUsers(TestUsers.Users)
// this adds the config data from DB (clients, resources, CORS)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString);
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString);
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
})
.AddProfileService<IdentityWithAdditionalClaimsProfileService>()
.AddAspNetIdentity<ApplicationUser>();
services.AddTransient<IProfileService, IdentityWithAdditionalClaimsProfileService>();
Api authorize protection decorator (have tried both):
`
//[Authorize]
[Authorize(AuthenticationSchemes = "Bearer")]
`
Asp.Net core 2.2 sts start.cs:
`
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var identityServer = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddProfileService<IdentityWithAdditionalClaimsProfileService>()
//.AddTestUsers(TestUsers.Users)
// this adds the config data from DB (clients, resources, CORS)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString);
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString);
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
})
.AddProfileService<IdentityWithAdditionalClaimsProfileService>()
.AddAspNetIdentity<ApplicationUser>();
services.AddTransient<IProfileService, IdentityWithAdditionalClaimsProfileService>();
Asp.Net core 2.2 sts project IdentityWithAdditionalClaims handler:
public IdentityWithAdditionalClaimsProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, SqlDbContext Sql)
{
_userManager = userManager;
_claimsFactory = claimsFactory;
_context = Sql;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
var tsicCustomClaims = await GetTSICCustomClaims(claims);
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
claims.Add(new Claim(JwtClaimTypes.Scope, "api1"));
claims.Add(new Claim("regid", tsicCustomClaims.RegId.ToString()));
claims.Add(new Claim("jseg", tsicCustomClaims.JobPath));
claims.Add(new Claim("jobid", tsicCustomClaims.JobId.ToString()));
claims.Add(new Claim(JwtClaimTypes.Role, tsicCustomClaims.RoleName));
claims.Add(new Claim(JwtClaimTypes.GivenName, tsicCustomClaims.FirstName));
claims.Add(new Claim(JwtClaimTypes.FamilyName, tsicCustomClaims.LastName));
claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, tsicCustomClaims.EMail));
context.IssuedClaims = claims;
}
Errors:
When accessing the protected api I get from Asp.Net core 2.2 api project:
[09:29:03 Information] Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler
Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
at Microsoft.IdentityModel.Tokens.Validators.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateAudience(IEnumerable`1 audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
[09:29:03 Information] Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler
BearerIdentityServerAuthenticationJwt was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
[09:29:03 Information] IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
Bearer was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
On startup of the Asp.Net Core 2.2 sts project I get:
Seeding database...
Clients already populated
IdentityResources already populated
ApiResources already populated
Done seeding database.
[09:28:09 Information] IdentityServer4.Startup
Starting IdentityServer4 version 2.3.2.0
[09:28:09 Information] IdentityServer4.Startup
Using the default authentication scheme Identity.Application for IdentityServer
[09:28:09 Debug] IdentityServer4.Startup
Using Identity.Application as default ASP.NET Core scheme for authentication
[09:28:09 Debug] IdentityServer4.Startup
Using Identity.External as default ASP.NET Core scheme for sign-in
[09:28:09 Debug] IdentityServer4.Startup
Using Identity.External as default ASP.NET Core scheme for sign-out
[09:28:09 Debug] IdentityServer4.Startup
Using Identity.Application as default ASP.NET Core scheme for challenge
[09:28:09 Debug] IdentityServer4.Startup
Using Identity.Application as default ASP.NET Core scheme for forbid
[09:28:10 Debug] IdentityServer4.EntityFramework.TokenCleanup
Starting grant removal
Hosting environment: Development
Content root path: E:\Projects-STS\TSIC\TSIC.STS
Now listening on: https://localhost:44340
Application started. Press Ctrl+C to shut down.
[09:28:13 Debug] IdentityServer4.Startup
Login Url: /Account/Login
[09:28:13 Debug] IdentityServer4.Startup
Login Return Url Parameter: ReturnUrl
[09:28:13 Debug] IdentityServer4.Startup
Logout Url: /Account/Logout
[09:28:13 Debug] IdentityServer4.Startup
ConsentUrl Url: /consent
[09:28:13 Debug] IdentityServer4.Startup
Consent Return Url Parameter: returnUrl
[09:28:13 Debug] IdentityServer4.Startup
Error Url: /home/error
[09:28:13 Debug] IdentityServer4.Startup
Error Id Parameter: errorId
[09:28:25 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /connect/authorize matched to endpoint type Authorize
[09:28:25 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Authorize, successfully created handler: IdentityServer4.Endpoints.AuthorizeEndpoint
[09:28:25 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.AuthorizeEndpoint for /connect/authorize
[09:28:25 Debug] IdentityServer4.Endpoints.AuthorizeEndpoint
Start authorize request
[09:28:25 Debug] IdentityServer4.Endpoints.AuthorizeEndpoint
No user present in authorize request
[09:28:25 Debug] IdentityServer4.Validation.AuthorizeRequestValidator
Start authorize request protocol validation
[09:28:26 Debug] IdentityServer4.EntityFramework.Stores.ClientStore
angularclient found in database: True
[09:28:26 Debug] IdentityServer4.Stores.ValidatingClientStore
client configuration validation for client angularclient succeeded.
[09:28:27 Debug] IdentityServer4.EntityFramework.Stores.ResourceStore
Found ["openid", "profile", "email", "api1scope"] identity scopes in database
[09:28:27 Debug] IdentityServer4.EntityFramework.Stores.ResourceStore
Found [] API scopes in database
[09:28:27 Debug] IdentityServer4.EntityFramework.Stores.ResourceStore
Found ["openid", "profile", "email", "api1scope"] identity scopes in database
[09:28:27 Debug] IdentityServer4.EntityFramework.Stores.ResourceStore
Found [] API scopes in database
I think I'm close here and just need a push in the right direction.
I'm currently focused on the line:
[09:28:27 Debug] IdentityServer4.EntityFramework.Stores.ResourceStore
Found [] API scopes in database
Thinking this is related to the api project error:
IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
This bugs me because the database DOES have an entry in dbo.ApiScopes, from SQL Server:
Id Name DisplayName Description Required Emphasize ShowInDiscoveryDocument ApiResourceId
9 api1 Scope for the api1 ApiResource NULL 0 0 1 12
I'm grateful for any assistance
In your angular config you have:
openIDImplicitFlowConfiguration.scope = 'openid profile email api1scope';
However, it should match one of your valid scopes for the api1:
openIDImplicitFlowConfiguration.scope = 'openid profile email api1';
Identity Server 4 only adds Api resource as a valid token audience if you request at least one of the scopes that belong to a given api and if your client is allowed that scope.
Vidmantas, thanks for your response, I tried that and same error occurred. I then got lucky and solved (was a problem with the different audiences indicated in the id token vs the access token, and how the id token aud: could be set by the IdentityServer4 configuration in the Api project startup.cs):
Got it, hope this helps others:
The angular client after logging in is returned:
id token:
{
"nbf": 1550240640,
"exp": 1550273640,
"iss": "https://localhost:44340",
"aud": "https://localhost:44340/resources",
"client_id": "angularclient",
"sub": "71765055-647D-432E-AFB6-0F84218D0247",
"auth_time": 1550240638,
"idp": "local",
"regid": "xxxx",
"jseg": "xxxxx",
"jobid": "b0984a87-172a-436e-a382-e95de3e1059f",
"role": "xxxx",
"given_name": "xxxxx",
"family_name": "xxxx",
"email": "xxxx",
"scope": [
"openid",
"profile",
"email"
],
"amr": [
"pwd"
]
}
and access token:
{
"nbf": 1550240640,
"exp": 1550243640,
"iss": "https://localhost:44340",
"aud": "angularclient",
"nonce": "N0.55036966062308791550240634889",
"iat": 1550240640,
"at_hash": "yNVxDVHkmEmUvurl7XlzuA",
"sid": "f54dee03793e7cc202b57f1d6de7622e",
"sub": "71765055-647D-432E-AFB6-0F84218D0247",
"auth_time": 1550240638,
"idp": "local",
"preferred_username": "TSICSuperUser",
"name": "xxxx",
"email": "xxxxx",
"email_verified": true,
"regid": "xxxxx",
"jseg": "xxxxx",
"jobid": "xxxxxxf",
"role": "xxxxx",
"given_name": "xxxx",
"family_name": "xxxxx",
"amr": [
"pwd"
]
}
NOTE THE DIFFERENT AUDIENCES (aud:)
The Asp.Net Core 2.2 Api Project startup.cs configured IdentityServer4:
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServer4Strings:Authority");
options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer4Strings:RequireHttpsMetadata");
options.ApiName = "api1";
options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt;
options.ApiSecret = Configuration.GetValue<string>("IdentityServer4Strings:STSTSICApisSecuredSecret");
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10); // that's the default
});
and the Bearer authentication error was:
2019-02-14 18:03:15.188 -07:00 [DBG] AuthenticationScheme: Bearer was not authenticated.
2019-02-14 18:04:04.360 -07:00 [INF] Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
at Microsoft.IdentityModel.Tokens.Validators.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateAudience(IEnumerable`1 audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
2019-02-14 18:04:04.430 -07:00 [INF] BearerIdentityServerAuthenticationJwt was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
2019-02-14 18:04:04.433 -07:00 [INF] Bearer was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'https://localhost:44340/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
The "api1" in error:
Did not match: validationParameters.ValidAudience: 'api1'
references startup.cs
.AddIdentityServerAuthentication
options.ApiName = "api1";
Changing the Asp.Net Core 2.2 Api project startup.cs to:
// critical for bearer authentication, the audience of the id token (set by Options.ApiName) is equal to this value
var idTokenAudience = $"{Configuration.GetValue<string>("IdentityServer4Strings:Authority")}/resources";
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServer4Strings:Authority");
options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer4Strings:RequireHttpsMetadata");
options.ApiName = idTokenAudience;
options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt;
options.ApiSecret = Configuration.GetValue<string>("IdentityServer4Strings:STSTSICApisSecuredSecret");
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10); // that's the default
});
resolved the issue.
2019-02-15 07:45:12.414 -07:00 [INF] Successfully validated the token.
2019-02-15 07:45:12.414 -07:00 [DBG] AuthenticationScheme: Bearer was successfully authenticated.

How to get user group information from Azure AD B2C

I am using Microsoft Graph for fetching user information, namely "List users" API.
Following is the code for accessing the user information :
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(resourceId, clientCred);
string token = authenticationResult.AccessToken;
Debug.WriteLine("token=" + token);
var responseString = String.Empty;
string[] scopes = { "User.Read" };
using (var client = new HttpClient())
{
string requestUrl = "https://graph.microsoft.com/v1.0/users?$select=id,givenName,surname";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
Debug.WriteLine(request.ToString());
HttpResponseMessage response = client.SendAsync(request).Result;
responseString = response.Content.ReadAsStringAsync().Result;
Debug.WriteLine(responseString);
}
Output :
{
"#odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(id,givenName,surname)",
"value": [
{
"id": "000000000-0000-0000-000-000000000",
"givenName": "XXX",
"surname": "XXX"
}, {
"id": "000000000-0000-0000-000-000000000",
"givenName": "XXX",
"surname": "XXX"
}
]
}
How to the get user group ?
You can call the user: getMemberGroups API action for a user to get their groups:
You need to make a request like so:
POST https://graph.microsoft.com/v1.0/users/user-id-here/getMemberGroups
Content-type: application/json
Content-length: 33
{
"securityEnabledOnly": true
}
The securityEnabledOnly parameter defines if it should only return security groups. Setting it to false will also return the user's Office 365 group memberships for example.
An alternative is to use the memberOf navigation property:
GET https://graph.microsoft.com/v1.0/users/user-id-here/memberOf
This returns the groups and directory roles the user is a direct member of.

Resources