Wildfly 38 - aggregate realm for OIDC and LDAP

38 views
Skip to first unread message

Dj Apal

unread,
Dec 30, 2025, 4:23:43 AM (10 days ago) 12/30/25
to WildFly
Hello all

We want to have the following setup
Users login via OIDC, entra ID and their roles are taken from an internal ldap server.
I have a sample app with the following oidc.json

{
"client-id" : "XXX",
"provider-url" : "https://login.microsoftonline.com/XXX/v2.0",
"ssl-required" : "EXTERNAL",
"credentials" : {
"secret" : "XXX"
},
"principal-attribute": "email"
}

In my test jsp page i get the
Principal user = req.getUserPrincipal();
user.getName() 
but i want to load the roles of the user via ldap as a second step.
AI told me to create an aggregate realm with token realm for authentication and the ldap realm as authorization


/subsystem=elytron/regex-principal-transformer=email-to-uid:add(pattern="([^@]+)@.*", replacement="$1")

/subsystem=elytron/token-realm=oidc-token-realm:add(principal-claim=email)

/subsystem=elytron/dir-context=idmDC:add(url="ldap://MYLDAPSERVER",principal="uid=ldapuser,cn=users,cn=accounts,dc=internal,dc=net",credential-reference={clear-text="XXX"})

/subsystem=elytron/ldap-realm=idmLR:add(dir-context=idmDC, \
direct-verification=true, \
identity-mapping={search-base-dn="cn=users,cn=accounts,dc=internal,dc=net", \
rdn-identifier="uid", \
attribute-mapping=[{from="cn", to="Roles", filter="(member=uid={0},cn=users,cn=accounts,dc=internal,dc=net)", filter-base-dn="cn=groups,cn=accounts,dc=internal,dc=net"}, {from="gecos", to="fullName"}, {from="krbPasswordExpiration", to="passwordExpirationDate"}]})

So far so good
but when i try to add the aggregate realm

/subsystem=elytron/aggregate-realm=oidc-ldap-realm:add(authentication-realm=oidc-token-realm, authorization-realm=idmLR, principal-transformer=email-to-uid)
i get

{
    "outcome" => "failed",
    "failure-description" => {
        "WFLYCTL0412: Required services that are not installed:" => ["org.wildfly.security.security-realm.oidc-token-realm"],
        "WFLYCTL0180: Services with missing/unavailable dependencies" => ["service org.wildfly.security.security-realm.oidc-ldap-realm is missing [o
rg.wildfly.security.security-realm.oidc-token-realm]"]
    },
    "rolled-back" => true
}

although the token realm is there.
Any thoughts? Why do i get error although the realm is there even after reload?
How can i get the roles from the ldap realm and continue to my app?

Dj Apal

unread,
Dec 30, 2025, 7:24:13 AM (10 days ago) 12/30/25
to WildFly

I tried various senarios and this seems to be partially working

/subsystem=elytron/regex-principal-transformer=email-to-uid:add(pattern="([^@]+)@.*", replacement="$1")

/subsystem=elytron/dir-context=idmDC:add(url="MYLDAPSERVER",principal="uid=ldapuser,cn=users,cn=accounts,dc=internal,dc=net",credential-reference={clear-text="XXX"})


/subsystem=elytron/ldap-realm=idmLR:add(dir-context=idmDC, \
direct-verification=true, \
identity-mapping={search-base-dn="cn=users,cn=accounts,dc=internal,dc=net", \
rdn-identifier="uid", \
attribute-mapping=[{from="cn", to="Roles", filter="(member=uid={0},cn=users,cn=accounts,dc=internal,dc=net)", filter-base-dn="cn=groups,cn=accounts,dc=internal,dc=net"}, {from="gecos", to="fullName"}, {from="krbPasswordExpiration", to="passwordExpirationDate"}]})

/subsystem=elytron/token-realm=azureTokenRealm:add(jwt={issuer=["https://login.microsoftonline.com/XXX/v2.0"], audience=["XXX"], \
jwks-url="https://login.microsoftonline.com/XXX/discovery/v2.0/keys"}, principal-claim="preferred_username")

/subsystem=elytron/aggregate-realm=oidc-ldap-realm:add(authentication-realm=azureTokenRealm, authorization-realm=idmLR, principal-transformer=email-to-uid)

/subsystem=elytron/security-domain=nikiSecurityDomain:add(realms=[{realm=oidc-ldap-realm,role-decoder=groups-to-roles}],default-realm=oidc-ldap-realm)

/subsystem=elytron/http-authentication-factory=nikiHttpAuth:add(security-domain=nikiSecurityDomain, http-server-mechanism-factory=global, mechanism-configurations=[{mechanism-name=OIDC, mechanism-realm-configurations=[{realm-name=oidc-ldap-realm}]}])

/subsystem=undertow/application-security-domain=nikiApplicationSecurityDomain:add(http-authentication-factory=nikiHttpAuth)

Along with oidc.json in my project and <security-domain>nikiApplicationSecurityDomain</security-domain>
IN jboss-web.xml, i get the user email and successfully login, but it seems that authorization realm is never called.
I don't see with TRACE level any request to my ldap in order to take the groups/roles.
Is this setup valid?
What do i do wrong here?
Thank you


Gabriel Padilha

unread,
Dec 30, 2025, 12:39:58 PM (10 days ago) 12/30/25
to WildFly
Hey,

I think you should be able to reach what you are looking for by using a custom realm like this:

/subsystem=elytron/dir-context=idmDC:add(url="MYLDAPSERVER",principal="uid=ldapuser,cn=users,cn=accounts,dc=internal,dc=net",credential-reference={clear-text="XXX"})

/subsystem=elytron/ldap-realm=idmLR:add(dir-context=idmDC, \
direct-verification=true, \
identity-mapping={search-base-dn="cn=users,cn=accounts,dc=internal,dc=net", \
rdn-identifier="uid", \
attribute-mapping=[{from="cn", to="Roles", filter="(member=uid={0},cn=users,cn=accounts,dc=internal,dc=net)", filter-base-dn="cn=groups,cn=accounts,dc=internal,dc=net"}, {from="gecos", to="fullName"}, {from="krbPasswordExpiration", to="passwordExpirationDate"}]})

/subsystem=elytron/custom-realm=myOidcRealm:add(module=org.wildfly.security.elytron-http-oidc, class-name=org.wildfly.security.http.oidc.OidcSecurityRealm)
/subsystem=elytron/aggregate-realm=oidc-ldap-realm:add(authentication-realm=myOidcRealm, authorization-realm=idmLR, principal-transformer=email-to-uid)
/subsystem=elytron/security-domain=nikiSecurityDomain:add(realms=[{realm=oidc-ldap-realm,role-decoder=groups-to-roles}],default-realm=oidc-ldap-realm)
/subsystem=elytron/service-loader-http-server-mechanism-factory=myFactory:add(module=org.wildfly.security.elytron-http-oidc) /subsystem=elytron/http-authentication-factory=myMechanism:add(http-server-mechanism-factory=myFactory,security-domain=nikiSecurityDomain,mechanism-configurations=[ {mechanism-name=OIDC}]) /subsystem=undertow/application-security-domain=myDomain:add(http-authentication-factory=myMechanism,override-deployment-config=true)

After configuring this, you should be able to use the "myOidcRealm" for authentication and "idmLR" for authorization via an aggregate realm as you have done.

The trick here is to modify the application to "propagate" the OIDC user to the "nikiSecurityDomain" .

Under WEB-INF directory in the application where you have the "oidc.json", web.xml, jboss-web.xml, remove the login-config and add the listener class in the "web.xml". E.g:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0"> ... <!-- <login-config> <auth-method>OIDC</auth-method> </login-config> --> <listener> <listener-class>org.wildfly.security.http.oidc.OidcConfigurationServletListener</listener-class> </listener> </web-app>

After that, in the jboss-web.xml, point the security domain. E.g:
<?xml version="1.0" encoding="UTF-8"?> <jboss-web> <context-root>/hello1_formauth</context-root> <security-domain>myDomain</security-domain> </jboss-web>

As a last step, add the jboss-deployment-structure.xml to use the elytron-http-oidc module:
<jboss-deployment-structure> <deployment> <dependencies> <module name="org.wildfly.security.elytron-http-oidc"/> </dependencies> </deployment> </jboss-deployment-structure>

With that, you should be able to reach what you are looking for.

Dj Apal

unread,
Dec 30, 2025, 2:29:34 PM (10 days ago) 12/30/25
to WildFly
Hi Gabriel
Thank you for your response!
It seems that this partially works but it's better than my previous setup!
Problem is that principal transformer doesnt get called/doesnt work

ldap expects aalexiadis and not aalex...@mydomain.com, so login fails

21:21:18,688 DEBUG [org.wildfly.security] (default task-2) Obtaining lock for identity [aalex...@mydomain.com]...
21:21:18,688 DEBUG [org.wildfly.security] (default task-2) Obtained lock for identity [aalex...@mydomain.com].
21:21:18,688 DEBUG [org.wildfly.security] (default task-2) Creating [class javax.naming.directory.InitialDirContext] with environment:
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [java.naming.security.credentials] with value [******]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [java.naming.security.authentication] with value [simple]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [java.naming.provider.url] with value [ldap://myldapserver:389]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [com.sun.jndi.ldap.read.timeout] with value [60000]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [com.sun.jndi.ldap.connect.pool] with value [false]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [com.sun.jndi.ldap.connect.timeout] with value [10000]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [java.naming.security.principal] with value [uid=ldapuser,cn=users,cn=accounts,dc=internal,dc=net]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [java.naming.referral] with value [ignore]
21:21:18,688 DEBUG [org.wildfly.security] (default task-2)     Property [java.naming.factory.initial] with value [com.sun.jndi.ldap.LdapCtxFactory]
21:21:18,967 DEBUG [org.wildfly.security] (default task-2) [javax.naming.ldap.InitialLdapContext@355091ec] successfully created. Connection established to LDAP server.
21:21:18,967 DEBUG [org.wildfly.security] (default task-2) Trying to create identity for principal [aalex...@mydomain.com].
21:21:18,967 DEBUG [org.wildfly.security] (default task-2) Executing search [(uid={0})] in context [cn=users,cn=accounts,dc=internal,dc=net] with arguments [aalex...@mydomain.com]. Returning attributes are [GECOS, KRBPASSWORDEXPIRATION]. Binary attributes are [null].
21:21:19,016 DEBUG [org.wildfly.security] (default task-2) Identity for principal [aalex...@mydomain.com] not found.

Any ideas?
Thanks once again!

Dj Apal

unread,
Dec 31, 2025, 4:37:09 AM (9 days ago) 12/31/25
to WildFly
After a lot of searching, the solution was to create a custom realm

import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityRealm;
import org.wildfly.security.authz.Attributes;
import org.wildfly.security.authz.AuthorizationIdentity;
import org.wildfly.security.authz.MapAttributes;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.evidence.Evidence;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.security.Principal;
import java.security.spec.AlgorithmParameterSpec;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

public class OidcLdapRealm implements SecurityRealm {

private final String ldapUrl = "ldap://XXX/";
private final String bindDn = "uid=ldapuser,cn=users,cn=accounts,dc=internal,dc=net";
private final String bindPassword = "XXX";
private final String usersBaseDn = "cn=users,cn=accounts,dc=internal,dc=net";
private final String groupsBaseDn = "cn=groups,cn=accounts,dc=internal,dc=net";

@Override
public RealmIdentity getRealmIdentity(Principal principal) {
final String email = principal.getName();

return new RealmIdentity() {
private String userDn;
private Set<String> groups = new HashSet<>();

@Override
public Principal getRealmIdentityPrincipal() {
return () -> email;
}

@Override
public boolean verifyEvidence(Evidence evidence) {
return false;
}

@Override
public boolean exists() throws RealmUnavailableException {
return true;
}

@Override
public Attributes getAttributes() {
MapAttributes attrs = new MapAttributes();
int i = 0;

for (String group : groups) {
attrs.add("groups", i++, group);
}

return attrs;
}

@Override
public AuthorizationIdentity getAuthorizationIdentity() {
Properties env = new Properties();
env.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
env.setProperty(Context.PROVIDER_URL, ldapUrl);
env.put(Context.SECURITY_PRINCIPAL, bindDn);
env.put(Context.SECURITY_CREDENTIALS, bindPassword);

try {
DirContext ctx = new InitialDirContext(env);
String filter = "(mail=" + email + ")";
SearchControls userSc = new SearchControls();
userSc.setSearchScope(SearchControls.SUBTREE_SCOPE);
userSc.setReturningAttributes(new String[] { "dn" });
NamingEnumeration<SearchResult> results = ctx.search(usersBaseDn, filter, userSc);

System.out.println("SEARCHING FOR MAIL " + email);

if (results.hasMore()) {
SearchResult sr = results.next();
userDn = sr.getNameInNamespace();

System.out.println("=== RESULT IS " + userDn + " ===");

Set<String> roles = lookupGroups(userDn, ctx); // however you retrieve roles
SimpleAttributesImpl attributes = new SimpleAttributesImpl();

for (String role : roles) {
attributes.add("groups", role);
}

System.out.println("=== TOTAL GROUPS: " + roles.size() + " ===");

ctx.close();

return AuthorizationIdentity.basicIdentity(attributes);
}
} catch (Exception e) {
throw new RuntimeException(e);
}

return null;
}

@Override
public <C extends Credential> C getCredential(Class<C> credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) {
return null;
}

@Override
public <C extends Credential> C getCredential(Class<C> credentialType) {
return null;
}

@Override
public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) {
return SupportLevel.UNSUPPORTED;
}

@Override
public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType, String algorithmName) {
return SupportLevel.UNSUPPORTED;
}

@Override
public void dispose() {
// No cleanup required
}

// helper method to fetch roles via LDAP
private Set<String> lookupGroups(String userDn, DirContext ctx) throws Exception {
Set<String> result = new HashSet<>();
String filter = "(member=" + userDn + ")";
SearchControls sctl = new SearchControls();
sctl.setSearchScope(SearchControls.SUBTREE_SCOPE);
sctl.setReturningAttributes(new String[]{ "cn" });

NamingEnumeration<SearchResult> answer = ctx.search(groupsBaseDn, filter, sctl);

while (answer.hasMore()) {
SearchResult sr = answer.next();
Attribute cn = sr.getAttributes().get("cn");

System.out.println(" - " + cn.get());

if (cn != null) {
result.add(cn.get(0).toString());
}
}

return result;
}
};
}

@Override
public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> aClass, String s, AlgorithmParameterSpec algorithmParameterSpec) throws RealmUnavailableException {
return SupportLevel.UNSUPPORTED;
}

@Override
public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType, String algorithmName) {
return SupportLevel.UNSUPPORTED;
}
}

Although this works, i would prefer a simpler solution TBH!


Reply all
Reply to author
Forward
0 new messages