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
Related
I need to fill in the "Properties" in the client's claim.
I am writing down a claimon the IS4 server in the ProfileService class:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// ...
Claim claim = new Claim("userData", "personalRights");
string valuePersonalRights = JsonConvert.SerializeObject(userRights);
claim.Properties.Add(GetKeyValuePair("rights", valuePersonalRights));
claims.Add(claim);
context.IssuedClaims.AddRange(claims);
}
private KeyValuePair<string, string> GetKeyValuePair(string key, string value)
{
KeyValuePair<string, string> keyValuePair = new KeyValuePair<string, string>(key, value);
return keyValuePair;
}
In this claim on the server there are records "Properties":
https://postgres-russia.ru/wp-content/files/is4_img/on_server.jpg
However, on the client, the properties of this claim are missing:
https://postgres-russia.ru/wp-content/files/is4_img/on_client.jpg
Client Configuration:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("domainGroups");
options.Scope.Add("geolocation");
options.Scope.Add("fullname");
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
};
});
How to get claims properties on the client?
When you are defining your client, you can assign it's claims too, which will be included in access token.
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "spa",
ClientName = "SPA Client",
ClientUri = "",
AllowedGrantTypes = {GrantType.ResourceOwnerPassword,GrantType.ClientCredentials},
RedirectUris =
{
},
RequireClientSecret = false,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
PostLogoutRedirectUris = { "" },
AllowedCorsOrigins = { "","" },
AllowedScopes = { "openid", "profile","roles", IdentityServerConstants.LocalApi.ScopeName },
Claims = new Claim[]//look at this property
{
new Claim("prop1","value1")
}
}
};
This is a common problem when using Net Core. For some reason, the Firefox browser was simply silent, without showing errors, and the Chrome browser pointed to 431 Request Header Fields Too Large. The size of the statements in the cookie was over 4096 byte. Solved by using the ITicketStore and storing the claims as claims in the user session. More details: stackoverrun.com/ru/q/11186809
More: IIS Deployed ASP.NET Core application giving intermittent 431 Request headers too long error
I have a Blazor web app that connects to a different Identity Server 4 server. I can get the login to work correctly and pass the access token back the Blazor. However, when the token expires I don't know how to go out and get a new access token? Should I be getting a refresh token and then an access token? I am confused on how this all works.
Blazor Code
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = AzureADDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(AzureADDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:44382";
options.RequireHttpsMetadata = true;
options.ClientId = "client";
options.ClientSecret = "secret";
options.ResponseType = "code id_token token";
options.SaveTokens = true;
options.Scope.Add("IdentityServerApi");
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
});
IdentityServer4 Setup
...
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Hybrid,
AllowAccessTokensViaBrowser = true,
RequireClientSecret = true,
RequireConsent = false,
RedirectUris = { "https://localhost:44370/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44370/signout-callback-oidc" },
AllowedScopes = { "openid", "profile", "email", "roles", "offline_access",
IdentityServerConstants.LocalApi.ScopeName
},
AllowedCorsOrigins = { "https://localhost:44370" },
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowOfflineAccess = true,
AccessTokenLifetime = 1,//testing
UpdateAccessTokenClaimsOnRefresh = true
},
...
UPDATE:
I have updated my code to offline_access for the client and server (thanks for the update below). My next question is how do I inject the request for the refresh token in Blazor once I get rejected because the access token is expired?
I have the Blazor app making calls back to the API (which validates the access token).
public class APIClient : IAPIClient
{
private readonly HttpClient _httpClient;
//add the bearer token to the APIClient when the client is used
public APIClient(IHttpContextAccessor httpAccessor, HttpClient client, IConfiguration configuration)
{
var accessToken = httpAccessor.HttpContext.GetTokenAsync("access_token").Result;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestVersion = new Version(2, 0);
client.BaseAddress = new Uri(configuration["Api_Location"]);
_httpClient = client;
_logger = logger;
}
What do I need to add to my API calls to validate?
Yes, you should obtain a refresh token as well to keep getting new access tokens. To get a refresh token from IdentityServer you need to add the 'offline_access' scope in the 'AllowedScopes' property of your client. You also need to set the 'AllowOfflineAccess' property on your client to true.
After that you need to include 'offline_access' to the scopes sent by the client and you should receive a refresh token in the response.
To use the refresh token, send a request to the token endpoint with everything you sent for the code exchange except replace the 'code' param with 'refresh_token' and change the value for 'grant_type' from 'code' to 'refresh_token'. The IdentityServer4 response to this request should contain an id_token, an access_token, and a new refresh_token.
I think I have found an answer (given the push from Randy). I did something familiar to this post, where I created a generic method in my APIClient.
public async Task<T> SendAsync<T>(HttpRequestMessage requestMessage)
{
var response = await _httpClient.SendAsync(requestMessage);
//test for 403 and actual bearer token in initial request
if (response.StatusCode == HttpStatusCode.Unauthorized &&
requestMessage.Headers.Where(c => c.Key == "Authorization")
.Select(c => c.Value)
.Any(c => c.Any(p => p.StartsWith("Bearer"))))
{
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", _httpAccessor.HttpContext.GetTokenAsync("refresh_token").Result),
new KeyValuePair<string, string>("client_id", "someclient"),
new KeyValuePair<string, string>("client_secret", "*****")
};
//retry do to token request
using (var refreshResponse = await _httpClient.SendAsync(
new HttpRequestMessage(HttpMethod.Post, new Uri(_authLocation + "connect/token"))
{
Content = new FormUrlEncodedContent(pairs)})
)
{
var rawResponse = await refreshResponse.Content.ReadAsStringAsync();
var x = Newtonsoft.Json.JsonConvert.DeserializeObject<Data.Models.Token>(rawResponse);
var info = await _httpAccessor.HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", x.Refresh_Token);
info.Properties.UpdateTokenValue("access_token", x.Access_Token);
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", x.Access_Token);
//retry actual request with new tokens
response = await _httpClient.SendAsync(new HttpRequestMessage(requestMessage.Method, requestMessage.RequestUri));
}
}
if (typeof(T).Equals(typeof(HttpResponseMessage)))
return (T)Convert.ChangeType(response, typeof(T));
else
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync());
}
I don't like that I have to call AuthenticateAsync. Yet, that seems to be the way I have found to get access to the UpdateTokenValue method to delete and then re-add the new access token.
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.
We have an existing ID server configuration and I am trying to work on the UI without having to set up the rest of the connections. I followed the example to set up an MVC client:
In Startup.cs:
Console.WriteLine("Adding Test Users!!!");
serviceBulder.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
New client added:
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
Users are configured as the following:
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password"
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
},
};
}
MVC is configured to use the services:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
});
}
I have a couple of issues:
When I use AllowedGrantTypes = GrantTypes.Implicit per tutorial I get "Sorry, there was an error : unauthorized_client"
When I use the AllowedGrantTypes = GrantTypes.Hybrid I do get the login screen but my in memory users (alice and bob with pw = password) can't log in.
Questions:
- Does AddTestUsers not work when there are additional data stores present already?
- Why does my grant type differ from the walkthrough?
I'd check on the AllowedGrantTypes at the identity server level. We have roughly the same setup and we use AllowedGrantTypes = GrantTypes.HybridAndClientCredentials This allows the MVC application to talk with identity server behind the scenes.
The test users should be present.
I have a problem with windows auth and custom claims.
I have an identityServer with windows auth and User.Identity.Name show my AD name.
But I cannot understand, how should I add some properties from my storage to this user. I have now something like this:
var claims = new List<Claim> {
new Claim("devhomepage", "www.devsite.com", ClaimValueTypes.String)};
var userIdentity = new ClaimsIdentity(claims, "siteinfo");
User.AddIdentity(userIdentity);
await HttpContext.SignInAsync(User);
return RedirectToLocal(returnUrl);
and it doesn't work :-) my client will be not authorized.
here is a config for the server
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = {"http://localhost:60640/signin-oidc"},// where to redirect to after login
PostLogoutRedirectUris = {"http://localhost:60640/signout-callback-oidc"},// where to redirect to after logout
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
AllowOfflineAccess = true,
RequireConsent = false
and is't a client
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:49245/";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
});
For SignIn I must use Microsoft.AspNetCore.Identity.SignInManager class with method SignInAsync(/*I should take hire a TUser object, that I stored in my database, and make mapping with my AD account */)
To use custom claims (will be used for all samples with custom claims in IS4):
public class ProfileService : IProfileService
{
protected UserManager<IdentityUser> _userManager;
public ProfileService(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//>Processing
var user = await _userManager.GetUserAsync(context.Subject);
//my custom claims
var claims = new List<Claim>
{
new Claim("devhomepage", "www"),
new Claim("reporting","reps")
};
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
//>Processing
var user = await _userManager.GetUserAsync(context.Subject);
context.IsActive = (user != null);
}
}
But this service I must register only after my IdentityService
services.AddIdentityServer()
...
services.AddTransient<IProfileService, ProfileService>();
}
of ConfigureServices