Keycloak/OIDC : retrieve user groups attributes - resteasy

I've extracted a user's groups information from the OIDC endpoint of Keycloak, but they don't come with the group ATTRIBUTES I defined (see Attributes tab into the group form, near Settings). Is there a claim to add to my request?
I'm using a RESTeasy client to reach Keycloak's admin API (had much better results than using the provided admin client, yet):
#Path("/admin/realms/{realm}")
public interface KeycloakAdminService {
#GET
#Path("/users/{id}/groups")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
List<GroupRepresentation> getUserGroups(#PathParam("realm") String realm, #PathParam("id") String userId,
#HeaderParam(AUTHORIZATION) String accessToken);
//DEBUG the access token must always be prefixed by "Bearer "
}
So I can fetch a user's groups:
private void fetchUserGroups(UserInfoOIDC infos, String userId) {
log.info("Fetching user groups from {}...", getRealm());
try {
KeycloakAdminService proxy = kcTarget.proxy(KeycloakAdminService.class);
AccessTokenResponse response = authzClient.obtainAccessToken(getAdminUsername(), getAdminPassword());
List<GroupRepresentation> groups = proxy.getUserGroups(getRealm(), userId,
"Bearer " + response.getToken());
infos.importUserGroups(groups); //DEBUG here we go!
} catch (WebApplicationException e) {
log.error("User groups failure on {}: {}", getRealm(), e.getMessage());
}
}
But when it comes to data exploration, it turns out that no attributes are provided into the GroupRepresentation#getAttributes structure.
I've read that claims can be added to user info requests. Does it work on the admin API? How can I achieve that result with RESTeasy templates?
Thx

I was able to achieve this by adding groups/roles info in token other claims property:
For this in keycloak config, go to your client -> mappers & add a group/role mapper. E.g.
Now this info will start coming in your access token:
To access these group attribute in Java you can extract it from otherclaims property of accesstoken. E.g.:
KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext)(request.getAttribute(KeycloakSecurityContext.class.getName()));
AccesToken token = keycloakSecurityContext.getToken();
In below image you can see that otherclaims property of token is filled with groups attribute that we created on keycloak. Note that if we had named "token claim property" as groupXYZ, the otherclaims would be showing:
groupsXYZ=[Administrator]

This is how I could eventually map group attributes (inherited as user attributes, as suspected before) into user informations, into the "other claims" section :

It is possible to inherit attributes from the group by switching on Aggregate attribute values option during the creation of a new User Attribute mapper.

First of all I think the answers above are correct. I was able to achieve what I wanted to do by following recommendations from them.
But I have also broke by production keycloak integration with auth2-proxy which lead to some outage for internal users :)
So, I took time to investigate a bit and came up with creating new client scope and adding custom client role / realm role / group mappers to it.
It works, and also you don't break your working keycloak integrations with other services ;)
Here is all my terraform code which you can you to reproduce what I did:
variable "realm_name" {
type = string
description = "Name of the realm to create"
default = "master"
}
variable "keycloack_user" {
type = string
description = "Keycloak admin user"
default = "admin"
}
variable "keycloack_password" {
type = string
description = "Keycloak admin password"
default = "admin"
}
variable "keycloak_url" {
type = string
description = "Keycloak url"
default = "http://localhost:8090"
}
variable "oauth_fqdn" {
type = string
description = "FQDN of the oauth server used for valid redirects"
default = "http://localhost:3000/*"
}
terraform {
required_version = ">= 1.0.0"
required_providers {
keycloak = {
source = "mrparkers/keycloak"
version = ">= 3.7.0"
}
}
}
provider "keycloak" {
client_id = "admin-cli"
username = var.keycloack_user
password = var.keycloack_password
url = var.keycloak_url
realm = var.realm_name
# base_path = "/auth"
}
data "keycloak_realm" "realm" {
realm = var.realm_name
}
resource "keycloak_openid_client" "client" {
realm_id = data.keycloak_realm.realm.id
client_id = "my-client"
name = "my-client"
enabled = true
access_type = "CONFIDENTIAL"
valid_redirect_uris = [
var.oauth_fqdn
]
login_theme = "keycloak"
standard_flow_enabled = true
}
output "keycloak_client_id" {
value = keycloak_openid_client.client.client_id
}
output "keycloak_client_secret" {
value = keycloak_openid_client.client.client_secret
sensitive = true
}
// creating custom scope
resource "keycloak_openid_client_scope" "this" {
realm_id = data.keycloak_realm.realm.id
name = "group_and_roles"
description = "When requested, this scope will map a user's group memberships and all roles to a claim"
include_in_token_scope = true
}
// creating custom group mapper
resource "keycloak_generic_protocol_mapper" "groups" {
realm_id = data.keycloak_realm.realm.id
client_scope_id = keycloak_openid_client_scope.this.id
name = "groups mapper"
protocol = "openid-connect"
protocol_mapper = "oidc-group-membership-mapper"
config = {
"full.path" : "true",
"id.token.claim" : "true",
"access.token.claim" : "true",
"claim.name" : "groups",
"userinfo.token.claim" : "true"
}
}
// creating custom role mapper for realm level roles
resource "keycloak_generic_protocol_mapper" "realm_roles" {
realm_id = data.keycloak_realm.realm.id
client_scope_id = keycloak_openid_client_scope.this.id
name = "realm roles mapper"
protocol = "openid-connect"
protocol_mapper = "oidc-usermodel-realm-role-mapper"
config = {
"multivalued" : "true",
"userinfo.token.claim" : "true",
"id.token.claim" : "true",
"access.token.claim" : "true",
"claim.name" : "realm_roles",
"jsonType.label" : "String"
}
}
// creating custom role mapper for client level roles
resource "keycloak_generic_protocol_mapper" "client_roles" {
realm_id = data.keycloak_realm.realm.id
client_scope_id = keycloak_openid_client_scope.this.id
name = "client roles mapper"
protocol = "openid-connect"
protocol_mapper = "oidc-usermodel-client-role-mapper"
config = {
"multivalued" : "true",
"userinfo.token.claim" : "true",
"id.token.claim" : "true",
"access.token.claim" : "true",
"claim.name" : "client_roles",
"jsonType.label" : "String"
}
}
// adding custom scope to client as optional
resource "keycloak_openid_client_optional_scopes" "client_optional_scopes" {
realm_id = data.keycloak_realm.realm.id
client_id = keycloak_openid_client.client.id
optional_scopes = [
"address",
"phone",
"offline_access",
"microprofile-jwt",
keycloak_openid_client_scope.this.name
]
}
And later I setup my application level auth config as follows:
AUTH_ISSUER_URL=http://localhost:8090/realms/master
AUTH_CLIENT_ID=my-client
AUTH_CLIENT_SECRET=client-secret-get-it-from-output
AUTH_SCOPES="profile,email,openid,offline_access,group_and_roles"

Related

Terraform dynamic group creation/loop issues

I've searched and played around quite a bit and I've not come across the solution.
I am trying to manage subscription providers and preview features via the "azurerm_resource_provider_registration" resource.
i've got it working fine if I want to manage just one provider with multiple sub features using the following:
tfvars file
provider_name = "Microsoft.Network"
provider_feature_name = {
feature1 = {
feature_name = "BypassCnameCheckForCustomDomainDeletion"
registered = true
}
feature2 = {
feature_name = "AllowTcpPort25Out"
registered = true
}
}
main.tf
resource "azurerm_resource_provider_registration" "provider_registration" {
name = var.provider_name
dynamic "feature" {
for_each = var.provider_feature_name
content {
name = feature.value.feature_name
registered = feature.value.registered
}
}
}
works great if I only ever want to manage one provider and it's features.
The problem comes when/if I want to add an additional "provider_name". I've tried a separate provider_name block but I keep getting a "unexpected block here" error. if I introduce a block like so;
vars.tf
provider_name = {
provider1 = {
provider_name = "Microsoft.Network" {
feature1 = {
feature_name = "test"
registered = true
}
}
}
provider2 = {
provider_name = "Microsoft.Storage" {
feature2 = {
feature_name = "test2"
registered = true
}
}
}
}
main.tf
resource "azurerm_resource_provider_registration" "provider_registration" {
for_each = var.provider_name
name = each.value.provider_name
dynamic "feature" {
for_each = var.provider_feature_name
content {
name = feature.value.feature_name
registered = feature.value.registered
}
}
I can get it loop but cannot get it to associate only feature1 to provider 1 etc as these features are exclusive to that provider. It associates feature1 to provider 1 & 2.
If I try to introduce a for_each or dynamic group for the "name" value, it comes up with "blocks of type provider not expected here" and/or "argument name is required but no definition was found"
In short, how can I get my main to loop over each provider_name and only associate the sub block of features to that provider (with potential for multiple features per provider type). is it just not possible for this type of resource? or am I just not understanding the loop/for_each documentation correctly.
any help is appreciated
thank you.
First we need to cleanup and optimize the input structure. I have speculated on what the values should be since there are two different hypothetical structures specified in the question, but the structure itself is accurate.
providers = {
"Microsoft.Network" = {
features = { "BypassCnameCheckForCustomDomainDeletion" = true }
}
"Microsoft.Storage" = {
features = { "AllowTcpPort25Out" = true }
}
}
Now we can easily utilize this structure with a for_each meta-argument in the resource.
resource "azurerm_resource_provider_registration" "provider_registration" {
for_each = var.providers
name = each.key
dynamic "feature" {
for_each = each.value.features
content {
name = feature.key
registered = feature.value
}
}
}
and this results in two provider registrations with the corresponding feature mapped to each.

Use Azure AD Graph to update values on the `AdditionalValues` dictionary for a user

How do I use Azure AD Graph to update values on the AdditionalValues dictionary for a user? The test below returns 400 Bad Response.
Background:
The rest of my application uses MSGraph. However, since a federated user can not be updated using MSGraph I am searching for alternatives before I ditch every implementation and version of Graph and implement my own database.
This issue is similar to this one however in my case I am trying to update the AdditionalData property.
Documentation
[TestMethod]
public async Task UpdateUserUsingAzureADGraphAPI()
{
string userID = "a880b5ac-d3cc-4e7c-89a1-123b1bd3bdc5"; // A federated user
// Get the user make sure IsAdmin is false.
User user = (await graphService.FindUser(userID)).First();
Assert.IsNotNull(user);
if (user.AdditionalData == null)
{
user.AdditionalData = new Dictionary<string, object>();
}
else
{
user.AdditionalData.TryGetValue(UserAttributes.IsCorporateAdmin, out object o);
Assert.IsNotNull(o);
Assert.IsFalse(Convert.ToBoolean(o));
}
string tenant_id = "me.onmicrosoft.com";
string resource_path = "users/" + userID;
string api_version = "1.6";
string apiUrl = $"https://graph.windows.net/{tenant_id}/{resource_path}?{api_version}";
// Set the field on the extended attribute
user.AdditionalData.TryAdd(UserAttributes.IsCorporateAdmin, true);
// Serialize the dictionary and put it in the content of the request
string content = JsonConvert.SerializeObject(user.AdditionalData);
string additionalData = "{\"AdditionalData\"" + ":" + $"[{content}]" + "}";
//additionalData: {"AdditionalData":[{"extension_myID_IsCorporateAdmin":true}]}
HttpClient httpClient = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage
{
Method = HttpMethod.Patch,
RequestUri = new Uri(apiUrl),
Content = new StringContent(additionalData, Encoding.UTF8, "application/json")
};
var response = await httpClient.SendAsync(request); // 400 Bad Request
}
Make sure that the Request URL looks like: https://graph.windows.net/{tenant}/users/{user_id}?api-version=1.6. You need to change the api_version to "api-version=1.6".
You cannot directly add extensions in AdditionalData and it will return the error(400).
Follow the steps to register an extension then write an extension value to user.
Register an extension:
POST https://graph.windows.net/{tenant}/applications/<applicationObjectId>/extensionProperties?api-version=1.6
{
"name": "<extensionPropertyName like 'extension_myID_IsCorporateAdmin>'",
"dataType": "<String or Binary>",
"targetObjects": [
"User"
]
}
Write an extension value:
PATCH https://graph.windows.net/{tenant}/users/{user-id}?api-version=1.6
{
"<extensionPropertyName>": <value>
}

IdentityServer4 Discovery Client Error - Issuer Name Does Not Match Authority

I am getting the 'Issuer name does not match authority' error because I have an ssl-terminating load balancer in front of my is4 service (i.e. issuer is https://myurl and authority is http://myurl).
What should I do in this situation? The dns names are identical, it is the s in https which is causing the validation failure!
It is possible for your Issuer and Authority to be different, but it requires changes to configuration of the server and the discovery request.
On your Identity Server's startup method, you can set the issuer:
var identityServerBuilder = services.AddIdentityServer(options =>
{
if (Environment.IsDevelopment())
{
options.IssuerUri = $"http://myurl:5000";
}
else
{
options.IssuerUri = $"https://myurl";
}
})
And then in your discovery document request:
DiscoveryDocumentRequest discoveryDocument = null;
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == EnvironmentName.Development)
{
discoveryDocument = new DiscoveryDocumentRequest()
{
Address = "http://myurl:5000",
Policy = {
RequireHttps = false,
Authority = "http://myurl:5000",
ValidateEndpoints = false
},
};
}
else
{
discoveryDocument = new DiscoveryDocumentRequest()
{
Address = "http://myurl:5000",
Policy = {
RequireHttps = false,
Authority = "https://myurl",
ValidateEndpoints = false
},
};
}
var disco = await httpClient.GetDiscoveryDocumentAsync(discoveryDocument);
Your issuer url is https however authority url is http. Both urls should be exactly same.
Else you can try setting ValidateIssuerName property to false. This property decides if issuer name has to be identical to authority or not. By default it is true -
var discoRequest = new DiscoveryDocumentRequest
{
Address = "authority",
Policy = new DiscoveryPolicy
{
ValidateIssuerName = false,
},
};
answer from mackie1001 on identityserver4 gitter
your load balancer should forward on the original protocol (X-Forwarded-Proto) and you can use that to set the current request scheme to match the incoming request
you'd just need to create a middleware function to do it
Have a read of this: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.0
for reference this is the code i added to startup:-
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto
});
many thanks mackie1001!
if using nginx as the load balancer you will probably need this in service configuration...
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// Only loopback proxies are allowed by default.
// Clear that restriction because forwarders are enabled by explicit
// configuration.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
and then add this middleware before the identity server middleware
app.UseForwardedHeaders();

IdentitySever4 user claims and ASP.NET User.Identity

I've written a small IdentityServer demo server, following the examples in the documentation. I have the following TestUser:
new TestUser
{
SubjectId = "1",
Username = "Username",
Password = "password",
Claims = new List<Claim>()
{
new Claim(System.Security.Claims.ClaimTypes.Name, "Username"),
new Claim(System.Security.Claims.ClaimTypes.Email, "username#domain.com")
}
}
I get an access token using ResourceOwnerPassword flow. And I am authorized to access my API.
The problem is that when in my protected API I'm trying to get the user identity, the name property is returned as null, and I don't see the email claim. No matter what I do I always see the same 12 claims. The sub claim is the only one passed with the information I put in the Client object.
How can I populate the HttpContext.User.Identity.Name property and send additional claims/data about the user?
The reason probably is that you are not requesting the proper resources/scopes for your client.
You need to define an API resource with the claims you need in the access token.
e.g in Resources.cs you can add the claims to be included in all api2 scopes
new ApiResource
{
Name = "api2",
ApiSecrets =
{
new Secret("secret".Sha256())
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email
},
Scopes =
{
new Scope()
{
Name = "api2.full_access",
DisplayName = "Full access to API 2",
},
new Scope
{
Name = "api2.read_only",
DisplayName = "Read only access to API 2"
}
}
}
Then you allow your resource owner client the access to those API resources.
e.g in client.cs
new Client
{
ClientId = "roclient",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
"custom.profile",
"api1", "api2.read_only"
}
},
You can then request the scope in your roclient
client.RequestResourceOwnerPasswordAsync("bob", "bob", "api2.read_only", optional).Result
Post the access token to the API and you will get the claims you added to your API resource.
In the call to UseOpenIdConnectAuthentication, or wherever you're trying to use the token, make sure you set the TokenValidationParameters for the Name property to ClaimTypes.Name.
By default, the Name claim type is set to name (JwtClaimType.Name).

When is ProfileDataRequestContext.RequestedClaimTypes not empty?

I'm trying out IdentityServer4 demo project and I'm adding user claims to ProfileDataRequestContext.IssuedClaims in IProfileService implementation. One thing I've noticed is that there is a context.RequestedClaimTypes collection, which is always empty in any resource/identity/scope configuration variations I've tried. Under what condition does this collection has data?
If in the definition of your ApiResources you define UserClaims, these will then be populated in the context.RequestClaimTypes.
For example:
new ApiResource
{
Name = "TestAPI",
ApiSecrets = { new Secret("secret".Sha256()) },
UserClaims = {
JwtClaimTypes.Email,
JwtClaimTypes.EmailVerified,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.PhoneNumberVerified,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
JwtClaimTypes.PreferredUserName
},
Description = "Test API",
DisplayName = "Test API",
Enabled = true,
Scopes = { new Scope("testApiScore) }
}
Then your ProfileDataRequestContext.RequestClaimTypes will contain these request claims, for your Identity Server to fulfil how you see fit.
I've found out that it if you set client.GetClaimsFromUserInfoEndpoint = true and additional roundtrip is made to /connect/userinfo endpoint and the request has requested value "sub".
Answer: https://github.com/IdentityServer/IdentityServer4/issues/1067
Whenever you request a scope that has associated claims.

Resources