I am new to Identity Server 4, and I am struggling to obtain the users identities. At the moment, I am displaying the Claims via an API that Identity Server is protecting as so:
namespace API01.Controllers
{
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
// GET identity
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
}
The problem with this is that email resource is just showing up as the value email when I decode the jwt.
"scope": [
"email",
"openid",
"api1"
],
I have been experimenting with User.Identities but so far I cannot get the information I need from my AllowedScopes {"email", "openid", "api1"}.
Basically, I want to obtain the value which in my case is test#test.com. I am not worried about returning a JsonResult, just a string would suffice for now, if its going to be difficult.
If you want to include the email claim in your id token , you can add the IdentityResources.Email() in IdentityResource of IDS4 :
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
Also set AlwaysIncludeUserClaimsInIdToken to true in client config :
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
....
....
AlwaysIncludeUserClaimsInIdToken = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1",
IdentityServerConstants.StandardScopes.Email,
},
AllowOfflineAccess = true
},
You can start from Identity Server4 code samples .
If you want to find the scopes in jwt token , id token won't include the scopes claim , but access token includes since api should validate that .
Scope array indicates what is allowed to access.
You probably want to see email claim in the token.
For that you need to implement IProfilrService and add all the claims from Subject to IssuedClaims.
Related
Ok, I am failing to get IS4 to issue a refresh token during authorization_code flow, even though I (believe) turned it on, and the client is requesting it by including offline_access in the scope. I add the IS4 using:
services.AddIdentityServer()
.AddInMemoryClients(Config.Clients)
.AddPersistedGrantStore<PersistedGrantStore>()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddSigningCredential(loadedX509pfx);
// identity resources from another file:
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
Client is defined as (I included everything except URLs for brevity):
ClientId = "MyClientBlahBlah",
ClientSecrets = new List<Secret> { new("secretXX".Sha256()) },
RequireClientSecret = false,
RequireConsent = false,
AllowedGrantTypes = GrantTypes.Code,
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = (int)TimeSpan.FromDays(7).TotalSeconds,
IdentityTokenLifetime = (int)TimeSpan.FromDays(7).TotalSeconds,
AbsoluteRefreshTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
SlidingRefreshTokenLifetime = (int)TimeSpan.FromDays(15).TotalSeconds,
UpdateAccessTokenClaimsOnRefresh = true,
AllowOfflineAccess = true,
RefreshTokenExpiration = TokenExpiration.Sliding,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true,
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess
},
RequirePkce = false,
...
However, when the request comes via this request:
GET /connect/authorize?client_id=MyClientBlahBlah&response_type=code&scope=openid%20profile%20offline_access&redirect_uri=<targeturl>
and the user logs in, the redirect URL is called only with authorization code:
There is no mention of refresh token anywhere. When I check the grant persistence store, the record was created (though its set to expire in 5 mins), however, again, the Data JSON in the record of store does not contain any mention of the refresh and there is no additional record being created for the subject.
{
"CreationTime" : "2022-03-28T15:56:26Z",
"Lifetime" : 300,
"ClientId" : "MyClientBlahBlah",
"Subject" : {...} <full json of claims, which is ok>
"IsOpenId" : true,
"RequestedScopes" : [
"0" : "opened",
"1" : "profile",
"2" : "offline_access"
],
"RedirectUri" : "targetURL",
"Nonce" : null,
"StateHash" : "d8EeV5-CyXT_-vbd8gRcRA",
"WasConsentShown" :false,
"SessionId" : "8DBD74DF791AD6C04FCCB78A3CF57C6E",
"CodeChallenge" : "",
"CodeChallengeMethod" : null,
"Description" : null,
"Properties" : { }
}
What am I doing wrong or what have I forgotten to include? I can't seem to find any help with documentation, most people have this problem when they forget to include offline_access in scope, which is not the case here.
The tokens (ID/Access/Refresh) are not meant be included in this request:
GET /connect/authorize?client_id=MyClientBlahBlah&response_type=code&scope=openid%20profile%20offline_access&redirect_uri=<targeturl>
Instead the OpenIDConnect handler in ASP.NET Core will after receiving the authorization code, ask for the tokens in a separate request between ASP.NET Core and IdentityServer. A request not visible to the user/browser.
Using the code below you can check what tokens that you actually have received:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");
string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
string tokenType = await HttpContext.GetTokenAsync("token_type");
string accessTokenExpire = await HttpContext.GetTokenAsync("expires_at");
I'm using code flow for a vuejs client with Identityserver4.
i added RequirePkce and i can get the access token and id token from oidc-client.
but access token aud claim is pointing back to Identityserver4 base address not my api resource.
can something be wrong ?
Client:
new Client
{
ClientId = "js.admin",
ClientName = "admin dashboard vuejs client.",
RequirePkce = true,
RequireClientSecret = false,
RequireConsent = false,
AllowedGrantTypes = GrantTypes.Code,
AllowAccessTokensViaBrowser = true,
RedirectUris = new List<string>
{
"http://localhost:8080",
"http://localhost:8080/logincb.html",
"http://localhost:8080/silent-renew.html"
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:8080/",
"http://localhost:8080"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:8080"
},
AllowedScopes = new List<string>
{
"openid",
"role",
"profile",
"api1.rw",
"email",
"phone"
}
}
oidc client setting:
const clientSettings = {
userStore: new WebStorageStateStore({ store: window.localStorage }),
authority: STS_DOMAIN,
client_id: "js.admin",
redirect_uri: "http://localhost:8080/logincb.html",
automaticSilentRenew: true,
silent_redirect_uri: "http://localhost:8080/silent-renew.html",
response_type: "code",
scope: "openid profile api1.rw role email phone",
post_logout_redirect_uri: "http://localhost:8080/",
filterProtocolClaims: true
};
Access token decoded:
"iss": "http://localhost:5001",
"aud": "http://localhost:5001/resources",
as you can see the both issuer and audience claims are the same with is wrong.
but even scopes are correct.
I really appreciate any help.
Bearer was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'http://localhost:5001/resources'. Did not match: validationParameters.ValidAudience: 'api1' or validationParameters.ValidAudiences: 'null'.
its is the last error i got.
The http://localhost:5001/resources is a generic resource that is added when you have not defined or associated any ApiResources with the requested ApiScope.
From the documentation here, it says:
When using the scope-only model, no aud (audience) claim will be added
to the token since this concept does not apply. If you need an aud
claim, you can enable the EmitStaticAudienceClaim setting on the
options. This will emit an aud claim in the issuer_name/resources
format. If you need more control of the aud claim, use API resources.
To get api1.rw as your audience, you need to add a ApiResource to your IdentityServer configuration. You can name the ApiResource and ApiScope api1.rw
To complement this answer, I write a blog post that goes into more detail about this topic:
IdentityServer – IdentityResource vs. ApiResource vs. ApiScope
I have a client on IdentityServer ,which allows openid,profile and email scopes :
return new[] {
new Client
{
ClientId = "TestWebApp",
ClientSecrets = new [] { new Secret("TestSecret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new List<string>{ StandardScopes.OpenId, StandardScopes.Profile,StandardScopes.Email },
}
};
I have defined following Identity resources as well,
public static IEnumerable<IdentityResource> IdentityResources()
{
return new IdentityResource[] {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
In-case the claim is missing , I am adding email to user claims explicitly while creation:
await _userManager.AddClaimAsync(testUser, new Claim("email", user.Username));
Now from my login controller using ResourceOwnerPasswordAndClientCredentials I am sending authentication request :
var client = new OAuth2Client(new Uri("http://localhost:44322/connect/token"), "TestWebApp", "TestSecret");
var requestResponse = client.RequestAccessTokenUserName(model.Email, model.Password, "openid profile email");
This works fine and I am getting the scopes back, but all of them are blank.
If you want to include the user claims in the Id token you can set AlwaysIncludeUserClaimsInIdToken to true on your client config.
return new[] {
new Client
{
ClientId = "TestWebApp",
ClientSecrets = new [] { new Secret("TestSecret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new List<string>{ StandardScopes.OpenId,
StandardScopes.Profile,StandardScopes.Email },
AlwaysIncludeUserClaimsInIdToken = true
}
};
You can include user claims in accesstoken when you specify those claims on Scopes. For instance for Swagger we needed to include the name claim if availible, below I dumped out the contents of what the ApiResource class should contain.
{
"ApiSecrets": [],
"Scopes": [
{
"Name": "SwaggerApi",
"DisplayName": "SwaggerApi",
"Description": null,
"Required": true,
"Emphasize": false,
"ShowInDiscoveryDocument": true,
"UserClaims": ["name","email"]
}
],
"Enabled": true,
"Name": "SwaggerApi",
"DisplayName": "SwaggerApi",
"Description": null,
"UserClaims": ["name","email"]
}
Add this scope to the allowed scopes of your client registration.
Request an access token.
If the User has a name claim or email claim -> it should get added to the access token.
Result contents access token
"idp": "oidc",
"name": "MyUserName",
"scope": [
"openid",
"profile",
"SwaggerApi"
],
When you use the resource owner password flow you’re requesting an access token, not an id token. Because of this, the claims associated with the scopes defined as identity resources are not passed in to your registered profile service implementation when the access token is created. If you really want to include the email in the access token then I’d advise you to make an api resource scope with “email” defined as a claim type.
That being said, if the email is being used for authentication purposes I’d suggest using another login flow that allows identity tokens if possible or using the user info endpoint.
The scenario is my client's possibility to grab data about my users. There is the code for config server:
Startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryIdentityResources(ApiConfiguration.GetIdentityResources()) .AddInMemoryApiResources(ApiConfiguration.GetAllResources())
.AddInMemoryClients(ApiConfiguration.GetAllClients())
.AddTestUsers(ApiConfiguration.GetUsers())
ApiConfiguration
public static IEnumerable<ApiResource> GetAllResources()
{
yield return new ApiResource
{
Name = "customAPI",
Description = "Custom API Access",
UserClaims = new List<string> { "role" },
ApiSecrets = new List<Secret> { new Secret("scopeSecret".Sha256()) },
Scopes = new List<Scope> {
new Scope("customAPI"),
}
};
}
public static IEnumerable<Client> GetAllClients()
{
yield return new Client
{
ClientId = "oauthClient",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> {
new Secret("Password".Sha256())},
AllowedScopes = new List<string> { "customAPI" }
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "psw",
Claims = new List<Claim> {
new Claim(ClaimTypes.Email, "1#1.com"),
new Claim(ClaimTypes.Role, "admin")
}
}
};
}
With this request:
POST /connect/token
Headers:
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=client_credentials&scope=customAPI&client_id=oauthClient&client_secret=Password
the client got the access token. So my question is how can I use the token? What I need http-request to grab data about bob (test user)?
And there is another related question: how to config api to my client could access token for specific user only?
Looking at the configuration above, the client you have setup is using the ClientCredentials grant, this is an OAuth grant type. If you wish to add identity you should look to use an OpenID Connect grant type, this will provide you with identity and tokens specific to the user you authenticate as.
More information on using OpenID Connect with Identity Server can be found in the docs at http://docs.identityserver.io/en/release/quickstarts/3_interactive_login.html
If you work through the following quickstart tutorials, you will see it come together.
Setup and Overview
Protecting an API using Client Credentials
Protecting an API using Passwords
Adding User Authentication with OpenID Connect
Switching to Hybrid Flow and adding API Access back
(You can skip the one on External Authentication.)
https://identityserver4.readthedocs.io/en/release/quickstarts/0_overview.html
Short: My client retrieves an access token from IdentityServer sample server, and then passes it to my WebApi. In my controller, this.HttpContext.User.GetUserId() returns null (User has other claims though). I suspect access token does not have nameidentity claim in it. How do I make IdentityServer include it?
What I've tried so far:
switched from hybrid to implicit flow (random attempt)
in IdSvrHost scope definition I've added
Claims = { new ScopeClaim(ClaimTypes.NameIdentifier, alwaysInclude: true) }
in IdSvrHost client definition I've added
Claims = { new Claim(ClaimTypes.NameIdentifier, "42") }
(also a random attempt)
I've also tried other scopes in scope definition, and neither of them appeared. It seems, that nameidentity is usually included in identity token, but for most public APIs I am aware of, you don't provide identity token to the server.
More details:
IdSrvHost and Api are on different hosts.
Controller has [Authorize]. In fact, I can see other claims coming.
Api is configured with
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseIdentityServerAuthentication(options => {
options.Authority = "http://localhost:22530/";
// TODO: how to use multiple optional scopes?
options.ScopeName = "borrow.slave";
options.AdditionalScopes = new[] { "borrow.receiver", "borrow.manager" };
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
});
Scope:
public static Scope Slave { get; } = new Scope {
Name = "borrow.slave",
DisplayName = "List assigned tasks",
Type = ScopeType.Resource,
Claims = {
new ScopeClaim(ClaimTypes.NameIdentifier, alwaysInclude: true),
},
};
And client:
new Client {
ClientId = "borrow_node",
ClientName = "Borrow Node",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"borrow_node:redirect-target",
},
Claims = { new Claim(ClaimTypes.NameIdentifier, "42") },
AllowedScopes = {
StandardScopes.OpenId.Name,
//StandardScopes.OfflineAccess.Name,
BorrowScopes.Slave.Name,
},
}
Auth URI:
request.CreateAuthorizeUrl(
clientId: "borrow_node",
responseType: "token",
scope: "borrow.slave",
redirectUri: "borrow_node:redirect-target",
state: state,
nonce: nonce);
and I also tried
request.CreateAuthorizeUrl(
clientId: "borrow_node",
responseType: "id_token token",
scope: "openid borrow.slave",
redirectUri: "borrow_node:redirect-target",
state: state,
nonce: nonce);
Hooray, I found an answer, when I stumbled upon this page: https://github.com/IdentityServer/IdentityServer3.Samples/issues/173
Apparently, user identity is passed in "sub" claim in the access token. Because I blindly copied API sample, its configuration included
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
which essentially prevented my API from mapping "sub" claim to nameidentifier. After removing this line, HttpContext.User.GetUserId() of authenticated controller returns user ID correctly.