Audience is empty in generated jwt token - asp.net-core

I am trying to use OpenIddict as an authorization server and I managed to produce a token, however I have no idea how to put the "aud" claim in the token.
I could of course manually add it to the created ClaimsPrincipal, but I'm wondering if there is a way for that like in IdentityServer. In IdentityServer you could declare api resources, and for those resources, you could declare scopes. If a scope of an API resource was requested, the name of the api resource was included as an audience.
Is there a way to tell OpenIddict that if a scope is requested, include a special audience, or it has to be implemented manually?

You can attach a list of resources to a scope when creating it via the scope manager:
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
DisplayName = "Demo API access",
DisplayNames =
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Accès à l'API de démo"
},
Name = "demo_api",
Resources =
{
"resource_server"
}
});
Then, use ListResourcesAsync(scopes) to list all the resources associated with the specified scopes. E.g:
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());

Related

ASP.NET core app authenticating with AAD: it is possible to retrieve additional user properties/attributes as claims?

I have created an ASP.NET Core application that successfully authenticates users with Azure AD and have seen that for an authenticated user an instance of type System.Security.Claims.ClaimsPrincipal is returned containing a single identity of type System.Security.Claims.ClaimsIdentity with a Type property valued "preferred_username" that carries the user's UPN. Among the claims that are returned there is for example one with its Type property valued "name" which seems to contain the user's display name. I was wondering if there is a way to have other users' attributes/properties also returned in the form of claims.
I was wondering if there is a way to have other user's
attributes/properties also returned in the form of claims.
Obviously you can retrieve optional claims within your token using azure active directory. To achieve that you would need few settings in Token configuration under azure active directory blade.
What Additional Claim can be added:
Currently, you can include these optional claims in your both idToken, accessToken and saml2Token
Settings For Optional Claims:
If you would like to include additional claims within your token you ought to configure as following:
Select Token configuration in your app:
Select Add optional claim and Include your claims:
You should have profile access permission:
Check your Manifest if the claims added accordingly:
Now You can Call Optional Claims which are available ClaimTypes Class:
Note: You can check the available claims fields witin ClaimTypes Class here.
Code Snippet:
You can implement within your application as following:
var username = HttpContext.User.Identity.Name;
var givenName = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var email = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
var country = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Country)?.Value;
Note: If you would like to know more details on optional claims you could check our official document here.

Adding and accessing claims in asp net core 3.0 using built in Identity server

I'm currently failing at wrapping my head around claims. I have a ASP.Net Core 3 project with the angular template and users stored in app.
I want to add claims to my users, reading up on I thought it would be easy, just add something along the lines of
await _UserManager.AddClaimAsync(user, new Claim(AccountStatic.ClaimTypes._Claim_Id, user.Id));
When you create the user, and then get it back using the below line once they are logged in again:
User.FindFirst(AccountStatic.ClaimTypes._Claim_Id)?.Value;
This does however not work. I can see the claims being written to AspNetUserClaims table in my database but it's not there in the users claims when they log in. There are a few other claims there, but not the ones I have added.
Do I need to define somewhere which of the users claims get included when they log in?
Edit.
I found a post stating that I need to add claims using a DI AddClaimsPrincipalFactory. So I added this class.
public class UserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
public UserClaimsPrincipalFactory(UserManager<ApplicationUser> userManager,IOptions<IdentityOptions> optionsAccessor): base(userManager, optionsAccessor)
{}
//https://levelup.gitconnected.com/add-extra-user-claims-in-asp-net-core-web-applications-1f28c98c9ec6
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var identity = await base.GenerateClaimsAsync(user);
identity.AddClaim(new Claim(AccountStatic.ClaimTypes.Claim_Id, user.Id ?? "[no id]"));
return identity;
}
}
And if I step through the code I can see the claims being added here. But in the Controller my custom claims are not present.
internal string GetUserId()
{
if (User.Identity.IsAuthenticated == false)
return null;
return User.FindFirst(AccountStatic.ClaimTypes.Claim_Id)?.Value;
}
Update. Ok I find this very strange. I have been trying to do what others claim work but for me nothing gets me the users name or id. inspecting the User I get the following. Nothing here contains any reference to the logged in user.
Update 2:
Just noticed that there is actually an Id in there: {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: ed107a11-6c62-496b-901e-ed9e6497662a} Seems to be the users id from the database. Not sure how to access it yet though.
These return null.
User.FindFirst(JwtRegisteredClaimNames.NameId)?.Value;
User.FindFirst("nameidentifier")?.Value;
User.FindFirst("NameIdentifier")?.Value;
Another update
I'm using a UserClaimsPrincipalFactory and breakingpointing it and looking at the Claims I can see that all of the ones I want are there. But again, these are not available in my API controllers as seen in the first picture.
I finally understood the problem, in large parts thanks to Ruard van Elburgs comments, and the answer he made in the linked question IdentityServer4 Role Based Authorization.
The problem is that the claims are not added to the access token.
There are two tokens, the access token and the identity token.
- Ruard van Elburg
They key to understanding what was going on was finding out that there are two tokens, and that they contain different claims and have different purposes.
You can force claims from one token to also be included in the other if you deem it necessary.
The solution to my problem was to add this in Startup.ConfigureServices
services
.AddIdentityServer(options => {})
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
foreach (var c in options.ApiResources)
{
// the string name of the token I want to include
c.UserClaims.Add(AccountStatic.ClaimTypes.Claim_Id);
}
});
I still have not figured out how to get the Identity token, but as I'm now including the user Id in the access token my problems are solved for the moment.

Retrieving client name in IdentityServer4 and ASP.NET Core

The dbo.Clients table in the IdentityServer database contains both ClientId and ClientName, however requesting a client credentials token doesn't include the client name in the token.
Is there a way to either retrieve client information from IdentityServer given the client id, or request that the client name be added to the token?
You can add them dynamically using a custom token request validator :
public class ClaimClientsUpdated : ICustomTokenRequestValidator
{
public Task ValidateAsync(CustomTokenRequestValidationContext context)
{
context.Result.ValidatedRequest.Client.AlwaysSendClientClaims = true;
context.Result.ValidatedRequest.ClientClaims.Add(new Claim("name", context.Result.ValidatedRequest.Client.ClientName));
return Task.FromResult(0);
}
}
Register in DI :
services.AddTransient<ICustomTokenRequestValidator, ClaimClientsUpdated>();
It will add prefix "client_" to custom claims , so claim will be "client_name": "value" in access token .
You can add information about the client in general by adding claims to the ClientClaims table. E.g. Type = Name, Value = MyCustomName
Which is added as claim (assuming prefix client_):
"client_Name": "MyCustomName"
This will allow you to add information about the client without having to add or change code.
The drawback for the client name is that you'll have to add a claim with redundant information, as Clients.ClientName is not the source. The advantage is that it's configuration only.

IdentityServer4.AddScopes creates invalid connect requesturl

We have successfully implemented IdentityServer4 which is working well for the two simple websites. The current project however, has a very complex set of policies, each with multiple requirements. And therefore we have many [Authorize(policy: policyname)] attributes on various controller actions.
The issue I am facing is I have added each of these policies as scopes to reduce the returned claims, but now the initial connect request fails with HTTP Error 404.15 - Not Found because the request url is too long.
e.g. reduced length for simplicity: https://localhost:44364/connect/authorize?client_id=Jbssa.HRX.Web&redirect_uri=https%3A%2F%2Flocalhost%3A44352%2Fsignin-oidc&response_type=code%20id_token&scope=openid%20profile%20offline_access%20Jbssa.HRX.BulkEventDocumentAttachs%20Jbssa.HRX.BulkEventDocumentDeletes%20Jbssa.HRX.BulkEvents%20Jbssa.HRX.Categories%20Jbssa.HRX.CheckerboardOutputEmployeeHistory....M&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
Now I could reduce the length of the claim type names but this would only be a stop gap.
This is an ASP.NET Core 2.2 website and I setup OpenIdConnect as follows within ConfigureServices:
services.AddOpenIdConnect(KnownAuthenticationScheme.JbsMainOidc, options =>
{
options.SignInScheme = KnownAuthenticationScheme.JbsMainCookie;
options.Authority = settings.Authority;
options.ResponseType = settings.ResponseType;
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientSecret;
options.RequireHttpsMetadata = settings.RequireHttpsMetadata;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapAll();
// add all scopes possible for the website
AddScopes(options.Scope);
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
})
and the AddScopes call looks like this:
static partial void AddScopes(ICollection<string> scopes)
{
scopes.Add(IdentityServerConstants.StandardScopes.OpenId);
scopes.Add(IdentityServerConstants.StandardScopes.Profile);
scopes.Add("offline_access");
foreach (var item in SupportedPolicies.GetList())
scopes.Add(item);
}
Because I don't have access to the Request URL during configuration I can't reduce the number of scopes to just the policies defined for the calling action.
For example:
[Authorize(SupportedPolicies.Categories)]
public partial class CategoriesController : Core.Mvc.ControllerBase
should only be using the "Categories" policy requirements therefore the request to IdSrv4 should be something like, should it not?
https://localhost:44364/connect/authorize?client_id=Jbssa.HRX.Web&redirect_uri=https%3A%2F%2Flocalhost%3A44352%2Fsignin-oidc&response_type=code%20id_token&scope=openid%20profile%20offline_access%20Jbssa.HRX.Categories&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
Or am I missing/not understanding something?
Update 16 aug
I think I understand what you are saying Ruard, and I understand the concept of authentication vs authorization. We have our own "PolicyServer" and it defines the roles/permissions for our applications. The issue I am having is with IdentityServer 4, when it makes it's initial connect request (read, not our code) it combines all the AllowedScopes into that connect request to determine the claims to return. These claims can be against multiple things, Identity or Api Resources, even the Client and Roles can have attached claims. But all these are filtered by the scope of the request. We have over 60 possible scopes that secures various resources and these I thought needed to be defined when we register OpenId via the AddScopes method (seen above).
To be more specific, when we don't include all the scopes, this SQL is being generated by IdentityServer4 within the connect request:
SELECT [api].[Id], [api].[Description], [api].[DisplayName], [api].[Enabled], [api].[Name]
FROM [dbo].[ApiResource] AS [api]
WHERE EXISTS (
SELECT 1
FROM [dbo].[ApiScope] AS [x]
WHERE [x].[Name] IN (N'openid', N'profile', N'Jbssa.HRX') AND ([api].[Id] = [x].[ApiResourceId]))
ORDER BY [api].[Id]
And this determines the resources to check for claims. If the scope does not exist, no resource is located and the STS returns an 'Invalid Scope' error. Hence why I added all the scopes and it now returns the Url Too long error.
We changed the way our clients requested the scopes and instead reduced the number of scopes to just a few and application the appropriate claims to these. It increased the access token size and therefore the response time, but it was not significant enough to warrant looking for other solutions.

Google OpenIDConnect: Why am I not getting an 'openid_id' value along with 'sub'?

I've read all the documentation I can find on migrating from Google OpenID 2 to OAuth 2/OpenIDConnect, and am currently using a nice class from phpclasses.org . This class seems to work quite well with both Google and Facebook (haven't yet tried other providers), but I'm having a problem with just one aspect of Google's migration path that is quite critical to me: obtaining the google user's old OpenID identifier in addition to the new OpenIDConnect 'sub' value for that user. I've got users registered in my database only through their old OpenID identifiers.
According to Step 3 in Google's Migration Guide it looks like all I should need to do is add a parameter "openid.realm=http://www.example.com" to the authentication request sent to https://accounts.google.com/o/oauth2/auth.
I looked up in my old code what the realm was that I used for its OpenID registration process (it was 'http://' . $_SERVER['HTTP_HOST'];), and then I made sure that the redirect urls in my application were compatible with that realm.
I added that value (url-encoded) as the value of an openid.realm parameter passed on the authentication request made within the class. But when the class exchanged the token for an access token, it got back the correct email, name, sub, etc, but there was no openid_id parameter present. BTW, my scope parameter is 'openid email profile'
Does anyone have a suggestion for what else I should try, or what I can do to determine what the problem is? Does anyone have successful experience getting the openid_id parameter value in php code? I'd really rather not go the client-side route with their "Sign-in with Google" button, and according to the docs that really shouldn't be necessary (plus there's no particular reason to believe it would solve my problem if I did it).
Just discovered it's in the id_token returned along with the access_token when you exchange the authorization_code for the access_token.
In the Migration Document, Step 3 first two paragraphs:
When you send an OpenID Connect authentication request URI to Google
as described in Step 1, you include an openid.realm parameter. The
response that is sent to your redirect_uri includes an authorization
code that your application can use to retrieve an access token and an
ID token. (You can also retrieve an ID token directly from the OpenID
Connect authentication request by adding id_token to the response_type
parameter, potentially saving a back-end call to the token endpoint.)
The response from that token request includes the usual fields
(access_token, etc.), plus an openid_id field and the standard OpenID
Connect sub field. The fields you need in this context are openid_id
and sub:
This is confusing and misleading/wrong. What token request? The authentication request returns an authorization code that you can exchange for an access_token and an id_token. The parenthetical remark about adding id_token to the response_type doesn't help much, as the various ways I tried to do that resulted in an error. But in any event, the
"usual fields (access_token, etc.), plus an openid_id field..."
is wrong. The access_token never appears in the same list at the openid_id field. The access_token appears in a list with the id_token, and the openid_id field is encoded within the id_token!
For testing purposes, you can decode an id_token using https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=<string>
In this documentation I couldn't find a useful description for how to decode an id_token, only caveats about their being sensitive, and how to validate them (though validation is not needed if obtained directly from a google endpoint as is the case here). I downloaded google's php client, and extracted code from it (src/Google/Auth/OAuth2.php and src/Google/Utils.php). And from that it's easy enough to figure out how to decode the id_token string: explode on ., base64_decode element 1, and json_decode that.
Update 2015-05-21: In reply to #Arthur's "answer", which would have been more appropriate as a comment on this answer. I would have commented on that answer myself, but comments aren't allowed to be very long and don't allow image uploads, plus I thought this extra info improves my answer...
Below is a screenshot from netbeans/xdebug, showing the array elements I get when decoding the id_token I get. Interesting that the intersection of the fields listed here with the fields listed by #Arthur is the null set. So I suspect that whatever #Arthur is decoding, it is not an id_token of the kind described here. I'm not familiar enough with this stuff even to guess what it is that's being decoded in that answer.
I'm afraid I don't have the time to dig through the library I use to extract the exact code path that produces the id_token I decoded to get this array using the simple algorithm I described. But I can tell you that the library I use is this: http://www.phpclasses.org/package/7700-PHP-Authorize-and-access-APIs-using-OAuth.html
Using it just as documented does not give you the id_token you need for this for two reasons:
The pre-configured server for Google with Oauth 2 doesn't handle the openid.realm parameter. To handle that, I added the following server definition to the oauth_configuration.json file:
"Google-OpenIdConnect":
{
"oauth_version": "2.0",
"dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&openid.realm={REALM}",
"offline_dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&access_type=offline&approval_prompt=force",
"access_token_url": "https://accounts.google.com/o/oauth2/token"
},
Just after the call to Initialize(), you need to add
$client->store_access_token_response = true;
Without that, the actual access_token response is not accessible (at least not the way I'm using the class). With those two changes in place, my exact code to get the openid_id using this class is as follows:
protected function jwt_decode($jwt) {
$segments = explode(".", $jwt);
if (count($segments) != 3) {
throw new Exception("Wrong number of segments in token: $jwt");
}
// Parse envelope.
$envelope = json_decode($this->urlSafeB64Decode($segments[0]), true);
if (!$envelope) {
throw new Exception("Can't parse token envelope: " . $segments[0]);
}
// Parse token
$json_body = $this->urlSafeB64Decode($segments[1]);
$payload = json_decode($json_body, true);
return $payload;
}
protected function getOpenid_id() {
require_once 'Phpclasses/Http/Class.php';
require_once 'Phpclasses/OauthClient/Class.php';
require 'Phpclasses/Google/private/keys.php';
$client = new oauth_client_class;
$client->configuration_file = $phpclasses_oauth_dir . '/oauth_configuration.json';
$client->server = 'Google-OpenIdConnect';
$client->redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
$client->client_id = $GOOGLE_APPID;
$client->client_secret = $GOOGLE_APPSECRET;
$client->scope = 'openid email';
$client->realm = $this->getRequest()->getScheme() . '://' . $this->getRequest()->getHttpHost();
$me = null;
if (($success = $client->Initialize())) {
// set *after* the call to Initialize
$client->store_access_token_response = true;
if (($success = $client->Process())) {
if (strlen($client->authorization_error)) {
$client->error = $client->authorization_error;
$success = false;
}
elseif (strlen($client->access_token)) {
$success = $client->CallAPI('https://www.googleapis.com/oauth2/v1/userinfo', 'GET', array(), array('FailOnAccessError' => true), $user);
$me = (array) $user;
if (!array_key_exists('id_token', $client->access_token_response)) {
throw new Exception('No id_token in \$client->access_token_response');
}
$openid_id = $this->jwt_decode($client->access_token_response['id_token']);
$me['openid_id'] = $openid_id;
}
}
$success = $client->Finalize($success);
}
if ($client->exit)
exit;
$client->ResetAccessToken();
if ($success) {
return $me;
}
// Code to handle failure...
}
Despite sootsnoot's (own) answer I still can't find the openid_id field anywhere. When decoding the id_token there are only "issuer", "issued_to", "audience", "user_id" , "expires_in" , "issued_at", "email" and "nonce" fields.
No "openid_id" field in sight..
Any ideas?
In response to sootsnoot's response :) And I apologize for not having enough reputation to comment, otherwise would have done so.
Am using an OpenID Connect library that takes endpoints from auto-config: https://accounts.google.com/.well-known/openid-configuration
So assume the endpoints are not the problem. Indeed it seems I was checking the wrong id_token. However, even when checking the correct one I still don't see the "openid_id" field. I now see everything you have, except that I have a "nonce" field instead of the "openid_id" field:
stdClass::__set_state(array( 'iss' => 'https://accounts.google.com', 'sub' => ****, 'azp' => ****, 'email' => ****, 'nonce' => ****, 'at_hash' => ****, 'email_verified' => true, 'aud' => ****, 'iat' => ****, 'exp' => 1432300788, ))
Must be doing something wrong, but what...
Final update:
Found the issue: was passing realm parameter as openid_realm=... instead of openid.realm=...
Oh do I feel stupid... :)