Identity Server 4 Checking for expected scope openid failed - identityserver4

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.

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

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

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

Identity Server 4 and User.Claims.Properties.Count always is 0 on client

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

Getting a Refresh Token from IdentitySever4

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.

Resources