mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
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:
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
protocol-mappers
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user