Document provided ProtocolMapper implementations (#47331)

Closes #47330

Signed-off-by: Ryan Emerson <remerson@ibm.com>
Co-authored-by: Stian Thorgersen <stianst@gmail.com>
This commit is contained in:
Ryan Emerson
2026-05-11 11:49:10 +01:00
committed by GitHub
parent e8a690f6d4
commit e977267092
7 changed files with 259 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
= Keycloak Admin API Guide
include::../attributes.adoc[]
<#list ctx.guides as guide>
:links_admin-api_${guide.id}_name: ${guide.title}
:links_admin-api_${guide.id}_url: #${guide.id}
</#list>
<#list ctx.guides as guide>
include::${guide.template}[leveloffset=+${guide.levelOffset}]
</#list>
+1
View File
@@ -0,0 +1 @@
protocol-mappers
+120
View File
@@ -0,0 +1,120 @@
<#import "/templates/guide.adoc" as tmpl>
<@tmpl.guide
title="Protocol Mappers"
summary="Discover all built-in protocol mappers and how to use these to define token claims and assertion attributes.">
Protocol mappers provide a flexible way to define claims used in OAuth 2.0 tokens and endpoints, and attributes in SAML 2.0 assertions. For example adding user attributes or role mappings.
This page includes a list of all built-in protocol mappers, but {project_name} also supports defining custom protocol mappers through the `ProtocolMapper` SPI.
Protocol mappers can be created and managed via the {project_name} REST API using the
link:https://www.keycloak.org/docs-api/latest/rest-api/index.html#_post_adminrealmsrealmclient_scopesclient_scope_idprotocol_mappersmodels[create protocol mapper] endpoint.
When creating a `ProtocolMapperRepresentation`, the `config` field is a key-value map whose available entries
depend on the specific mapper type. This page serves as a reference for the expected configuration options
available in `ProtocolMapperRepresentation.config` for each `ProtocolMapper` implementation.
For example, to create a protocol mapper that maps a user attribute into an OIDC token claim, you would send a `ProtocolMapperRepresentation` using the following JSON:
[source,json]
----
{
"name": "my-user-attribute-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "phone_number",
"claim.name": "phone",
"jsonType.label": "String"
}
}
----
The `protocolMapper` field corresponds to the "ID" listed in the tables below, and the `config` entries are described in each mapper's configuration table.
The same mapper can be created programmatically using the {project_name} Java admin client:
[source,java]
----
ProtocolMapperRepresentation mapper = new ProtocolMapperRepresentation();
mapper.setName("my-user-attribute-mapper");
mapper.setProtocol("openid-connect");
mapper.setProtocolMapper("oidc-usermodel-attribute-mapper");
mapper.setConfig(Map.of(
"user.attribute", "phone_number",
"claim.name", "phone",
"jsonType.label", "String"
));
Keycloak.getInstance(...)
.realm("my-realm")
.clientScopes()
.get("my-client-scope-id")
.getProtocolMappers()
.createMapper(mapper);
----
[discrete]
== Overview
The below table is an index of all available `ProtocolMapper` implementations provided by {project_name},
grouped by the associated protocol.
[cols="1,2",options="header"]
|===
|ID |Description
<#list ctx.protocolMappers.getMappersByProtocol(["openid-connect", "oid4vc", "saml", "docker-v2"]) as protocol, categoryMap>
2+a|**${protocol}**
<#list categoryMap as category, mapperList>
<#list mapperList as mapper>
|<<${mapper.id()},${mapper.id()}>>
|${(mapper.helpText())!"_No description available._"}
</#list>
</#list>
</#list>
|===
<#assign mappers = ctx.protocolMappers.getMappersByProtocol(["openid-connect", "oid4vc", "saml", "docker-v2"]) />
<#list mappers as protocol, categoryMap>
== ${protocol}
The following section contains all `ProtocolMapper` implementations associated with the ${protocol} protocol. For each
implementation we provide the "ID" of the `ProtocolMapper` and a table describing the supported configuration properties.
<#list categoryMap as category, mapperList>
<#list mapperList as mapper>
[#${mapper.id()}]
=== ${mapper.displayType()}
${(mapper.helpText())!"_No description available._"}
*ID*: `${mapper.id()}`
<#if mapper.configProperties()?has_content>
[cols="1,1,1,1,2",options="header"]
|===
|Name |Property |Type |Default |Description
<#list mapper.configProperties() as prop>
<#if prop.getName()??>
|${ctx.protocolMappers.resolveLabel(prop.getLabel()!prop.getName())}
|`${prop.getName()}`
|`${prop.getType()!"String"}`
|<#if prop.getDefaultValue()??>`${prop.getDefaultValue()?string}`<#else>_None_</#if>
|${ctx.protocolMappers.resolveTooltip((prop.getHelpText())!"")}
</#if>
</#list>
|===
</#if>
</#list>
</#list>
</#list>
</@tmpl.guide>
+7
View File
@@ -54,5 +54,12 @@
<include>pinned-guides</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/admin-api</directory>
<outputDirectory>generated-guides/admin-api/</outputDirectory>
<includes>
<include>pinned-guides</include>
</includes>
</fileSet>
</fileSets>
</assembly>
+12
View File
@@ -226,6 +226,18 @@
<preserveDirectories>true</preserveDirectories>
</configuration>
</execution>
<execution>
<id>admin-api-asciidoc-to-html</id>
<phase>generate-resources</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<sourceDirectory>${basedir}/target/generated-guides/admin-api</sourceDirectory>
<outputDirectory>${project.build.directory}/generated-docs/admin-api</outputDirectory>
<preserveDirectories>true</preserveDirectories>
</configuration>
</execution>
<execution>
<id>ui-customization-asciidoc-to-html</id>
<phase>generate-resources</phase>
@@ -14,11 +14,13 @@ public class Context {
private final Options options;
private final Features features;
private final ProtocolMappers protocolMappers;
private final List<Guide> guides;
public Context(Path srcPath) throws IOException {
this.options = new Options();
this.features = new Features();
this.protocolMappers = new ProtocolMappers(srcPath.getParent().getParent().getParent());
this.guides = new LinkedList<>();
Path partials = srcPath.resolve("partials");
@@ -81,6 +83,10 @@ public class Context {
return features;
}
public ProtocolMappers getProtocolMappers() {
return protocolMappers;
}
public List<Guide> getGuides() {
return guides;
}
@@ -0,0 +1,101 @@
package org.keycloak.guides.maven;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderManager;
import org.keycloak.quarkus.runtime.Providers;
public class ProtocolMappers {
private static final String MESSAGES_RELATIVE_PATH = "js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties";
private final Map<String, Map<String, List<ProtocolMapperInfo>>> mappers;
private final Properties messages;
public ProtocolMappers(Path projectRootDir) {
messages = loadMessages(projectRootDir);
ProviderManager providerManager = Providers.getProviderManager(Thread.currentThread().getContextClassLoader());
mappers = providerManager.loadSpis().stream()
.filter(spi -> spi.getName().equals("protocol-mapper"))
.findFirst()
.<Map<String, Map<String, List<ProtocolMapperInfo>>>>map(spi -> providerManager.load(spi).stream()
.map(ProtocolMapper.class::cast)
.sorted(Comparator.comparing(ProtocolMapper::getDisplayType))
.map(mapper -> new ProtocolMapperInfo(
mapper.getId(),
mapper.getClass().getName(),
mapper.getProtocol(),
mapper.getDisplayType(),
mapper.getDisplayCategory(),
mapper.getHelpText(),
mapper.getPriority(),
mapper.getConfigProperties()
))
.collect(Collectors.groupingBy(
ProtocolMapperInfo::protocol,
LinkedHashMap::new,
Collectors.groupingBy(
ProtocolMapperInfo::category,
LinkedHashMap::new,
Collectors.toList()
)
)))
.orElse(Map.of());
}
public Map<String, Map<String, List<ProtocolMapperInfo>>> getMappersByProtocol(List<String> protocols) {
Map<String, Map<String, List<ProtocolMapperInfo>>> ordered = new LinkedHashMap<>();
for (String protocol : protocols) {
Map<String, List<ProtocolMapperInfo>> categoryMap = mappers.get(protocol);
if (categoryMap != null) {
ordered.put(protocol, categoryMap);
}
}
return ordered;
}
public String resolveLabel(String label) {
if (label != null && label.endsWith(".label")) {
return messages.getProperty(label, label);
}
return label;
}
public String resolveTooltip(String tooltip) {
if (tooltip != null && tooltip.endsWith(".tooltip")) {
return messages.getProperty(tooltip, tooltip);
}
return tooltip;
}
private static Properties loadMessages(Path projectRootDir) {
Properties props = new Properties();
Path messagesFile = projectRootDir.resolve(MESSAGES_RELATIVE_PATH);
if (Files.exists(messagesFile)) {
try (Reader reader = Files.newBufferedReader(messagesFile)) {
props.load(reader);
} catch (IOException e) {
throw new RuntimeException("Failed to load admin messages properties from " + messagesFile, e);
}
}
return props;
}
public record ProtocolMapperInfo(String id, String implementationClass, String protocol, String displayType,
String category, String helpText, int priority,
List<ProviderConfigProperty> configProperties) {
}
}