IdentityServer4.AddScopes creates invalid connect requesturl - asp.net-core

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.

Related

Configuring JWT Token Validations Parameter cannot be set in ASP Net Core

I have a scenario in which I need to set the clock skew for JWT bearer tokens. but whatever I do, the code ignores my settings.
I have tried configuring it in multiple different sections of the Dependency Injection file in the Infrastructure Layer, but it ignores all of them.
I have this at the moment:
services.AddIdentityServer(options =>
{
options.IssuerUri = configuration.GetSection("MyCurrentDomainName")?.Value;
options.Authentication.CookieLifetime = TimeSpan.FromDays(999);
options.Authentication.CookieSlidingExpiration = true;
}).AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.Configure<JwtBearerOptions>(configuration =>
{
configuration.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(9875664);
});
services.TryAddEnumerable(ServiceDescriptor
.Singleton<IConfigureOptions<JwtBearerOptions>, ConfigureBearerOptions>());
Services.AddTransient<IProfileService, ProfileService>();
services.TryAddEnumerable(ServiceDescriptor
.Singleton<IPostConfigureOptions<JwtBearerOptions>, ConfigureJwtBearerOptions>());
The code ignores my configurations and sets the clock skew to its default 5 minutes.
In the last line of the above code, I have a PostConfigurationOption, and I have set the clock skew there as well, it runs the code, but when the WebUI layer calls for the authentication, the clock skew will default to 5 minutes.
What am I doing wrong here?
services.AddIdentityServer(options =>
{
options.Authentication.CookieLifetime = TimeSpan.FromDays(999);
});
If you're using Jwt token, why setting cookie lifetime ?
services.Configure<JwtBearerOptions>(configuration =>
{
configuration.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(9875664);
});
JwtBearerOptions got registered in the app service but internally. Identity server doens't make use of this.
Identity server internally make use of table Clients on the server to skew the clock for each client that we register. The time specified by corresponding columns IdentityTokenLifetime, AccessTokenLifetime, AuthorizationCodeLifetime, ConsentLifetime, AbsoluteRefreshTokenLifetime, SlidingRefreshTokenLifetime, modify them as you need.
UPDATE
The template still make use of IdentityServer4, it doesn't make it own magic. And that's what happen in the template.
In Startup, service called AddInfrastructure. which let the app to use In-memory database (as default config on appSettings), furthur more, IdentityServer took the IdentityServerSPA option on appSettings as it's profile. which will get config at .AddApiAuthorization<ApplicationUser, ApplicationDbContext>().
Since it's a pre-config profile, you cannot mess with it.
For the most clearly instruction, use localdb sql server. I think the way to do that is clear enougn in DependencyInjection file on Infrastructure layer. Then we'll see the table magically appear on sql.
And if it's just a simple demo, try this:
// Modify this block code in DependencyInjection file, Infrastructure layer
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(cfg =>
{
var defaultClient = cfg.Clients["CleanArchitecture.WebUI"];
defaultClient.AccessTokenLifetime = 3600; // 3600s
});
And other lifetime for each kind of token per client can be configure as well.

Audience is empty in generated jwt token

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());

AddOpenIdConnect error - SecurityTokenException: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: ''."

I am getting this exception in my ASP.NET Core 3.1 app.
Those are parameters used:
options.ClientId = config["Connection:ClientId"];
options.ClientSecret = config["Connection:ClientSecret"];
options.Authority = config["Connection:Uri"];
options.CallbackPath = new PathString("/signin");
options.ResponseType = "code";
options.ResponseMode = "query";
options.RequireHttpsMetadata = false;
I get both authorization code and access token, but after that I get error message as shown:
Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: ''."
Why is this validation for "id_token" occurring even tough I am using "code" as a response type?
Thank you for your help.
The last time I used them, the Microsoft server side web app libraries only supported the hybrid flow and you had to set:
response_type=code id_token
This forces id tokens to be validated as part of the authorization response, which adds to security in the area of substitution attacks.
A newer option for UIs is to use the following flow, in which case additional PKCE parameters are also needed, which I'm pretty sure the MS libraries do not support.
response_type=code
In the aecond case the id token is received and processed in the following step that swaps the authorization code for tokens.

Cookie Authentication: How to access a returning user

I have studied a couple examples of ASP.Net Core 2.0 Cookie Authentication.
However, I still don't see the code that checks for a returning user via a cookie.
Please note that I'm not a web developer. As a result, I realize that my question may appear foolish.
As I understand, the browser sends the cookie to the server per client request. However, I just don't see the server logic for this in the examples that I've studied. Hence, I only see logic for logging in the user the very first time based a username and password that's explicitly passed in.
Expectation:
I expected the server to explicitly check if a cookie exists when requesting the index page of the website. If a cookie does exist, I would expect to see some logic to address a user that's already logged in.
Giraffe example
Tutorial example
Here's my code:
let loginHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
Tasks.Task.Run(fun _ -> StackOverflow.CachedTags.Instance() |> ignore) |> ignore
task {
let! data = ctx.BindJsonAsync<LogInRequest>()
let email = data.Email.ToLower()
if authenticate email data.Password
then match login email with
| Some provider ->
let claims = [ Claim(ClaimTypes.Name, email) ]
let identity = ClaimsIdentity(claims, authScheme)
let user = ClaimsPrincipal(identity)
do! ctx.SignInAsync(authScheme, user)
return! json provider next ctx
| None -> return! (setStatusCode 400 >=> json "Invalid login") next ctx
else return! (setStatusCode 400 >=> json "Invalid login") next ctx
}
I'm looking at the giraffe example.
The statements services.AddAuthentication(authScheme) and services.AddCookie(cookieAuth) will add various services to the services collection, making them available for injection.
The statement app.UseAuthentication() adds services to the middleware pipeline. A middleware service is something that runs on every request.
The service that gets added by UseAuthentication is called AuthenticationMiddleware. As you can see this middleware relies on an injected IAuthenticationSchemeProvider (provided by your call to AddAuthentication) which in turn (skipping a few steps here) relies on a service called CookieAuthenticationhandler (provided by your call to AddCookie) which actually does the work of authenticating via cookie. When it is done it sets the logged in user onto the HttpContext which you can reference from your controllers if you need to.

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... :)