Fine Tuning Keycloak Dynamic Attributes: Exclusion Lists
In my previous post, I built an SPI that automatically inject all user attributes into OIDC tokens or UserInfo. While powerful, I realized there are scenarios where I might want to omit certain sensitive or irrelevant attributes. In this follow-up, I’ll extend the SPI to support a configurable exclusion list, giving me granular control over which attributes get mapped.
SPI configuration changes
First, I add a new configuration option in my mapper that lets me list out attributes to skip.
public static final String EXCLUDE_ATTRIBUTES = "exclude.attributes";
private static final List<ProviderConfigProperty> configProperties;
static {
List<ProviderConfigProperty> props = new ArrayList<>( );
OIDCAttributeMapperHelper.addIncludeInTokensConfig( props , AttributesMapper.class );
ProviderConfigProperty excludeProperty = new ProviderConfigProperty( );
excludeProperty.setName( EXCLUDE_ATTRIBUTES );
excludeProperty.setLabel( "Attributes to exclude" );
excludeProperty.setHelpText( "Comma-separated list of attribute name to exclude" );
excludeProperty.setType( ProviderConfigProperty.STRING_TYPE );
props.add( excludeProperty );
configProperties = Collections.unmodifiableList( props );
}
This adds a new “Attribute to exclude” field in the mapper settings, where I can enter something like duns, lastLogin
.
Mapping logic with exclusions
Next, I update the core mapping logic to parse that list and skip any matching attribute keys:
@Override
protected void setClaim(IDToken token , ProtocolMapperModel mappingModel , UserSessionModel userSession ,
KeycloakSession keycloakSession , ClientSessionContext clientSessionCtx) {
var user = userSession.getUser( );
var attributes = user.getAttributes( );
if ( attributes == null ) return;
String excludeList = mappingModel.getConfig( ).get( EXCLUDE_ATTRIBUTES );
Set<String> excludeSet = new HashSet<>( );
if ( excludeList != null && !excludeList.isEmpty( ) ) {
for (String attribute : excludeList.split( "," )) {
excludeSet.add( attribute.trim( ) );
}
}
var dynamicMapping = new ProtocolMapperModel( );
dynamicMapping.setId( mappingModel.getId( ) );
dynamicMapping.setName( mappingModel.getName( ) );
dynamicMapping.setProtocol( mappingModel.getProtocol( ) );
dynamicMapping.setProtocolMapper( mappingModel.getProtocolMapper( ) );
Map<String, String> config = mappingModel.getConfig( ) != null
? new HashMap<>( mappingModel.getConfig( ) )
: new HashMap<>( );
for (var entry : attributes.entrySet( )) {
var key = entry.getKey( );
if ( excludeSet.contains( key ) ) continue;
var value = entry.getValue( );
if ( value == null || value.isEmpty( ) ) continue;
var claimValue = (value.size( ) == 1) ? value.get( 0 ) : value;
config.put( "claim.name" , key );
config.put( "aggregate.attrs" , "true" );
if ( value.size( ) > 1 ) {
config.put( "multivalued" , "true" );
}
dynamicMapping.setConfig( config );
OIDCAttributeMapperHelper.mapClaim( token , dynamicMapping , claimValue );
}
The complete mapper class
For reference, here’s the full updated AttributesMapper with exclusion support:
package com.tmh.attributes;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.mappers.*;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;
import java.util.*;
public class AttributesMapper extends AbstractOIDCProtocolMapper
implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
public static final String PROVIDER_ID = "oidc-tmh-attributes";
public static final String EXCLUDE_ATTRIBUTES = "exclude.attributes";
private static final List<ProviderConfigProperty> configProperties;
static {
List<ProviderConfigProperty> props = new ArrayList<>( );
OIDCAttributeMapperHelper.addIncludeInTokensConfig( props , AttributesMapper.class );
ProviderConfigProperty excludeProperty = new ProviderConfigProperty( );
excludeProperty.setName( EXCLUDE_ATTRIBUTES );
excludeProperty.setLabel( "Attributes to exclude" );
excludeProperty.setHelpText( "Comma-separated list of attribute name to exclude" );
excludeProperty.setType( ProviderConfigProperty.STRING_TYPE );
props.add( excludeProperty );
configProperties = Collections.unmodifiableList( props );
}
@Override public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}
@Override public String getDisplayType() {
return "Dynamic User Attributes";
}
@Override public String getHelpText() {
return "Dynamically add all user attributes to userinfo";
}
@Override public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override public String getId() {
return PROVIDER_ID;
}
@Override
protected void setClaim(IDToken token , ProtocolMapperModel mappingModel , UserSessionModel userSession ,
KeycloakSession keycloakSession , ClientSessionContext clientSessionCtx) {
var user = userSession.getUser( );
var attributes = user.getAttributes( );
if ( attributes == null ) return;
String excludeList = mappingModel.getConfig( ).get( EXCLUDE_ATTRIBUTES );
Set<String> excludeSet = new HashSet<>( );
if ( excludeList != null && !excludeList.isEmpty( ) ) {
for (String attribute : excludeList.split( "," )) {
excludeSet.add( attribute.trim( ) );
}
}
var dynamicMapping = new ProtocolMapperModel( );
dynamicMapping.setId( mappingModel.getId( ) );
dynamicMapping.setName( mappingModel.getName( ) );
dynamicMapping.setProtocol( mappingModel.getProtocol( ) );
dynamicMapping.setProtocolMapper( mappingModel.getProtocolMapper( ) );
Map<String, String> config = mappingModel.getConfig( ) != null
? new HashMap<>( mappingModel.getConfig( ) )
: new HashMap<>( );
for (var entry : attributes.entrySet( )) {
var key = entry.getKey( );
if ( excludeSet.contains( key ) ) continue;
var value = entry.getValue( );
if ( value == null || value.isEmpty( ) ) continue;
var claimValue = (value.size( ) == 1) ? value.get( 0 ) : value;
config.put( "claim.name" , key );
config.put( "aggregate.attrs" , "true" );
if ( value.size( ) > 1 ) {
config.put( "multivalued" , "true" );
}
dynamicMapping.setConfig( config );
OIDCAttributeMapperHelper.mapClaim( token , dynamicMapping , claimValue );
}
}
}