dotnet / dotnet/aspnetcore

BlazorWebAppOidc AddOpenIdConnect with GetClaimsFromUserInfoEndpoint = true doesn't propogate role claims to client

Open
#58,826 6 comments 1 reaction 0 assignees View on GitHub
area-auth Docs
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

I am using the Blazor 8 BlazorWebAppOidc sample to authenticate and authorize using OpenIdConnect with keycloak. I am seeing an issue where role claims from the userinfo endpoint do not propogate to the client. My setup is as follows:

```csharp
builder.Services.AddAuthentication(MS_OIDC_SCHEME)
.AddOpenIdConnect(MS_OIDC_SCHEME, oidcOptions =>
{
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

oidcOptions.Authority = "https://host.docker.internal/keycloak/realms/Autostore/";

oidcOptions.ClientId = "WMSServiceCalendar";

oidcOptions.ResponseType = OpenIdConnectResponseType.Code;

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtRegisteredClaimNames.Name,
RoleClaimType = "role"
};

oidcOptions.UsePkce = true;
oidcOptions.GetClaimsFromUserInfoEndpoint = true;

oidcOptions.Events.OnUserInformationReceived = ctx =>
{
Console.WriteLine();
Console.WriteLine("Claims from the ID token");
foreach (var claim in ctx.Principal.Claims)
{
Console.WriteLine($"{claim.Type} - {claim.Value}");
}
Console.WriteLine();
Console.WriteLine("Claims from the UserInfo endpoint");
foreach (var property in ctx.User.RootElement.EnumerateObject())
{
Console.WriteLine($"{property.Name} - {property.Value}");
}
return Task.CompletedTask;
};
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Events.OnSigningIn = ctx =>
{
Console.WriteLine();
Console.WriteLine("Claims received by the Cookie handler");
foreach (var claim in ctx.Principal.Claims)
{
Console.WriteLine($"{claim.Type} - {claim.Value}");
}
Console.WriteLine();

return Task.CompletedTask;
};
});
```

With the OnUserInformationReceived logging I can see the claims coming from Keycloak:

```
Claims from the ID token
exp - 1730968528
iat - 1730968228
auth_time - 1730968228
jti - 1801bcab-dfd5-463f-9bf6-7cf84ded884a
iss - https://host.docker.internal/keycloak/realms/Autostore
aud - WMSServiceCalendar
sub - c5f3046c-b1b7-497d-b12e-a3f29afc5d11
typ - ID
azp - WMSServiceCalendar
nonce - 638665650239925001.M2YxOGZlNDMtMzkxYi00MDhkLWJmZTEtYzM1Y2QwNjI3NjAxODBkODhmZDAtMjE3OC00ZDYyLWEwZjktZGM0MjI5MzZkNjA1
sid - 654b19df-4dcf-405b-9d34-27bffd126968
at_hash - iCCj0tkd2HpoJZRclzph-w
acr - 1
email_verified - true
name - manager name
preferred_username - manager
given_name - manager
family_name - name

Claims from the UserInfo endpoint
sub - c5f3046c-b1b7-497d-b12e-a3f29afc5d11
resource_access - {"CommonClient":{"roles":["CommonClient:GrafanaViewer","CommonClient:MessagingAdmin","CommonClient:GrafanaEditor","CommonClient:GrafanaAdmin","CommonClient:UserManagement","CommonClient:Users"]}}
email_verified - True
role - ["WMSServiceCalendar:VehicleTypeDelete","WMSServiceCalendar:VehicleDelete","WMSServiceCalendar:VehicleExport","WMSServiceCalendar:VehicleTypeView","WMSServiceCalendar:VehicleView","WMSServiceCalendar:Users"]
name - manager name
preferred_username - manager
given_name - manager
family_name - name

Claims received by the Cookie handler
auth_time - 1730968228
jti - 1801bcab-dfd5-463f-9bf6-7cf84ded884a
sub - c5f3046c-b1b7-497d-b12e-a3f29afc5d11
typ - ID
sid - 654b19df-4dcf-405b-9d34-27bffd126968
email_verified - true
name - manager name
preferred_username - manager
given_name - manager
family_name - name
role - WMSServiceCalendar:VehicleTypeDelete
role - WMSServiceCalendar:VehicleDelete
role - WMSServiceCalendar:VehicleExport
role - WMSServiceCalendar:VehicleTypeView
role - WMSServiceCalendar:VehicleView
role - WMSServiceCalendar:Users
```

I have amended UserInfo as follows:

```csharp
public sealed class UserInfo
{
public required string UserId { get; init; }
public required string Name { get; init; }
public required string[] Roles { get; init; }

public const string UserIdClaimType = "sub";
public const string NameClaimType = "name";
public const string RoleClaimType = "role";

public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
new()
{
UserId = GetRequiredClaim(principal, UserIdClaimType),
Name = GetRequiredClaim(principal, NameClaimType),
Roles = principal.FindAll(RoleClaimType).Select(c => c.Value).ToArray(),
};

public ClaimsPrincipal ToClaimsPrincipal() =>
new(new ClaimsIdentity(
Roles.Select(role => new Claim(RoleClaimType, role))
.Concat([
new Claim(UserIdClaimType, UserId),
new Claim(NameClaimType, Name),
]),
authenticationType: nameof(UserInfo),
nameType: NameClaimType,
roleType: RoleClaimType));

private static string GetRequiredClaim(ClaimsPrincipal principal, string claimType) =>
principal.FindFirst(claimType)?.Value ?? throw new InvalidOperationException($"Could not find required '{claimType}' claim.");
}
```

In my client I then have the following in my page:

`@attribute [Authorize(Roles = "WMSServiceCalendar:Users")]`

As a result of the issue I see the following error:

`info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (WMSServiceCalendar:Users)`

Note: If I add the claims to the id token in keycloak then all works but that feels like it defeats the point of using GetClaimsFromUserInfoEndpoint?

### Expected Behavior

Using GetClaimsFromUserInfoEndpoint = true should flow the claims from server to client side and allow the roles to be used during authorization

### Steps To Reproduce

I am using the above setup with no further modifications to the sample application.

### Exceptions (if any)

_No response_

### .NET Version

8.0.403

### Anything else?

ID Token:
```
{
"exp": 1730967440,
"iat": 1730967140,
"jti": "aab08d01-1d37-4d87-9c73-7f9f95aa103c",
"iss": "https://host.docker.internal/keycloak/realms/Autostore",
"aud": "WMSServiceCalendar",
"sub": "c5f3046c-b1b7-497d-b12e-a3f29afc5d11",
"typ": "ID",
"azp": "WMSServiceCalendar",
"sid": "0bf27112-0271-4ce1-9d97-9e1d18cc9214",
"acr": "1",
"email_verified": true,
"name": "manager name",
"preferred_username": "manager",
"given_name": "manager",
"family_name": "name"
}
```

User Info:
```
{
"sub": "c5f3046c-b1b7-497d-b12e-a3f29afc5d11",
"email_verified": true,
"role": [
"WMSServiceCalendar:VehicleTypeDelete",
"WMSServiceCalendar:VehicleDelete",
"WMSServiceCalendar:VehicleExport",
"WMSServiceCalendar:VehicleTypeView",
"WMSServiceCalendar:VehicleView",
"WMSServiceCalendar:Users"
],
"name": "manager name",
"preferred_username": "manager",
"given_name": "manager",
"family_name": "name"
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.