CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-keycloak--keycloak-server-spi

Service Provider Interface (SPI) contracts and abstractions for the Keycloak identity and access management server enabling extensibility through custom providers

Pending
Overview
Eval results
Files

vault-integration.mddocs/

Vault Integration

The vault integration framework provides secure secrets management by integrating with external vault systems. It enables Keycloak to retrieve sensitive configuration values like passwords, API keys, and certificates from secure storage.

Core Vault Interfaces

VaultProvider

Provider interface for vault implementations.

public interface VaultProvider extends Provider {
    /**
     * Obtains a raw secret from the vault.
     * 
     * @param vaultSecretId the vault secret identifier
     * @return vault raw secret or empty optional
     */
    VaultRawSecret obtainSecret(String vaultSecretId);
}

VaultTranscriber

Transcribes vault references in configuration values to actual secret values.

public interface VaultTranscriber {
    /**
     * Transcribes a value, replacing vault references with actual secret values.
     * 
     * @param value the value potentially containing vault references
     * @return transcribed value with secrets resolved
     */
    String transcribe(String value);
}

Vault Secret Types

VaultRawSecret

Base interface for raw vault secrets.

public interface VaultRawSecret extends AutoCloseable {
    /**
     * Gets the raw secret as byte array.
     * 
     * @return optional raw secret bytes
     */
    Optional<byte[]> get();

    /**
     * Gets the raw secret as byte array or default value.
     * 
     * @param defaultValue default value if secret not found
     * @return raw secret bytes or default
     */
    default byte[] getOrDefault(byte[] defaultValue) {
        return get().orElse(defaultValue);
    }

    @Override
    void close();
}

VaultCharSecret

Interface for character-based vault secrets.

public interface VaultCharSecret extends VaultRawSecret {
    /**
     * Gets the secret as character array.
     * 
     * @return optional character array
     */
    Optional<char[]> getAsArray();

    /**
     * Gets the secret as character array or default value.
     * 
     * @param defaultValue default value if secret not found
     * @return character array or default
     */
    default char[] getAsArray(char[] defaultValue) {
        return getAsArray().orElse(defaultValue);
    }

    @Override
    default Optional<byte[]> get() {
        return getAsArray().map(chars -> {
            byte[] bytes = new byte[chars.length];
            for (int i = 0; i < chars.length; i++) {
                bytes[i] = (byte) chars[i];
            }
            return bytes;
        });
    }
}

VaultStringSecret

Interface for string-based vault secrets.

public interface VaultStringSecret extends VaultCharSecret {
    /**
     * Gets the secret as string.
     * 
     * @return optional string value
     */
    Optional<String> get();

    /**
     * Gets the secret as string or default value.
     * 
     * @param defaultValue default value if secret not found
     * @return string value or default
     */
    default String getOrDefault(String defaultValue) {
        return get().orElse(defaultValue);
    }

    @Override
    default Optional<char[]> getAsArray() {
        return get().map(String::toCharArray);
    }
}

Vault Key Resolution

VaultKeyResolver

Resolves vault keys from configuration values.

public interface VaultKeyResolver {
    /**
     * Checks if a value contains a vault key reference.
     * 
     * @param value the value to check
     * @return true if value contains vault reference
     */
    boolean isVaultKey(String value);

    /**
     * Extracts the vault key from a vault reference.
     * 
     * @param value the vault reference value
     * @return vault key or null if not a vault reference
     */
    String getVaultKey(String value);

    /**
     * Creates a vault reference from a key.
     * 
     * @param key the vault key
     * @return vault reference string
     */
    String createVaultReference(String key);
}

Vault SPI

VaultSpi

SPI definition for vault providers.

public class VaultSpi implements Spi {
    @Override
    public boolean isInternal() {
        return true;
    }

    @Override
    public String getName() {
        return "vault";
    }

    @Override
    public Class<? extends Provider> getProviderClass() {
        return VaultProvider.class;
    }

    @Override
    public Class<? extends ProviderFactory> getProviderFactoryClass() {
        return VaultProviderFactory.class;
    }
}

VaultProviderFactory

Factory for creating vault provider instances.

public interface VaultProviderFactory extends ProviderFactory<VaultProvider> {
    /**
     * Creates a vault provider instance.
     * 
     * @param session Keycloak session
     * @return vault provider
     */
    @Override
    VaultProvider create(KeycloakSession session);

    /**
     * Gets the provider ID.
     * 
     * @return provider ID
     */
    @Override
    String getId();
}

Usage Examples

Creating a Custom Vault Provider

public class HashiCorpVaultProvider implements VaultProvider {
    private final VaultClient vaultClient;
    private final String mountPath;

    public HashiCorpVaultProvider(VaultClient vaultClient, String mountPath) {
        this.vaultClient = vaultClient;
        this.mountPath = mountPath;
    }

    @Override
    public VaultRawSecret obtainSecret(String vaultSecretId) {
        try {
            // Parse vault secret ID (e.g., "secret/myapp/database")
            String[] parts = vaultSecretId.split("/");
            if (parts.length < 2) {
                return new EmptyVaultSecret();
            }

            String path = vaultSecretId;
            if (!path.startsWith(mountPath)) {
                path = mountPath + "/" + path;
            }

            // Retrieve secret from HashiCorp Vault
            Map<String, Object> secretData = vaultClient.read(path);
            if (secretData == null || secretData.isEmpty()) {
                return new EmptyVaultSecret();
            }

            // Return the secret value
            Object secretValue = secretData.get("value");
            if (secretValue == null) {
                // Try default field names
                secretValue = secretData.get("password");
                if (secretValue == null) {
                    secretValue = secretData.get("secret");
                }
            }

            if (secretValue != null) {
                return new StringVaultSecret(secretValue.toString());
            }

            return new EmptyVaultSecret();

        } catch (Exception e) {
            logger.error("Failed to retrieve secret: " + vaultSecretId, e);
            return new EmptyVaultSecret();
        }
    }

    @Override
    public void close() {
        if (vaultClient != null) {
            vaultClient.close();
        }
    }

    // Inner class for string secrets
    private static class StringVaultSecret implements VaultStringSecret {
        private final String secret;
        private volatile boolean closed = false;

        public StringVaultSecret(String secret) {
            this.secret = secret;
        }

        @Override
        public Optional<String> get() {
            if (closed) {
                throw new IllegalStateException("Secret has been closed");
            }
            return Optional.ofNullable(secret);
        }

        @Override
        public void close() {
            closed = true;
            // Clear the secret from memory (not possible with String, but shown for demonstration)
        }
    }

    // Inner class for empty secrets
    private static class EmptyVaultSecret implements VaultStringSecret {
        @Override
        public Optional<String> get() {
            return Optional.empty();
        }

        @Override
        public void close() {
            // Nothing to close
        }
    }
}

Creating a Vault Provider Factory

public class HashiCorpVaultProviderFactory implements VaultProviderFactory {
    public static final String PROVIDER_ID = "hashicorp-vault";

    private volatile VaultClient vaultClient;

    @Override
    public VaultProvider create(KeycloakSession session) {
        if (vaultClient == null) {
            synchronized (this) {
                if (vaultClient == null) {
                    vaultClient = initializeVaultClient();
                }
            }
        }
        return new HashiCorpVaultProvider(vaultClient, getMountPath());
    }

    @Override
    public void init(Config.Scope config) {
        // Initialize configuration
    }

    @Override
    public void postInit(KeycloakSessionFactory factory) {
        // Post-initialization
    }

    @Override
    public void close() {
        if (vaultClient != null) {
            vaultClient.close();
        }
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public List<ProviderConfigProperty> getConfigMetadata() {
        return Arrays.asList(
            new ProviderConfigProperty("vault.url", "Vault URL", "HashiCorp Vault server URL", 
                                       ProviderConfigProperty.STRING_TYPE, "https://vault.example.com:8200"),
            new ProviderConfigProperty("vault.token", "Vault Token", "Authentication token for Vault", 
                                       ProviderConfigProperty.PASSWORD, null),
            new ProviderConfigProperty("vault.mount-path", "Mount Path", "Vault mount path for secrets", 
                                       ProviderConfigProperty.STRING_TYPE, "secret"),
            new ProviderConfigProperty("vault.namespace", "Namespace", "Vault namespace (Enterprise feature)", 
                                       ProviderConfigProperty.STRING_TYPE, null),
            new ProviderConfigProperty("vault.timeout", "Timeout", "Connection timeout in seconds", 
                                       ProviderConfigProperty.STRING_TYPE, "30")
        );
    }

    private VaultClient initializeVaultClient() {
        String vaultUrl = getConfig().get("vault.url", "https://vault.example.com:8200");
        String vaultToken = getConfig().get("vault.token");
        String namespace = getConfig().get("vault.namespace");
        int timeout = getConfig().getInt("vault.timeout", 30);

        VaultConfig vaultConfig = new VaultConfig()
            .address(vaultUrl)
            .token(vaultToken)
            .openTimeout(timeout)
            .readTimeout(timeout);

        if (namespace != null && !namespace.isEmpty()) {
            vaultConfig.nameSpace(namespace);
        }

        return new Vault(vaultConfig).logical();
    }

    private String getMountPath() {
        return getConfig().get("vault.mount-path", "secret");
    }

    private Config.Scope getConfig() {
        // Return configuration scope
        return Config.scope("vault", PROVIDER_ID);
    }
}

Using Vault Transcriber

// Using vault transcriber to resolve secrets in configuration
try (KeycloakSession session = sessionFactory.create()) {
    VaultTranscriber transcriber = session.vault();
    
    // Configuration values with vault references
    String dbPassword = "${vault.secret/myapp/database}";
    String apiKey = "${vault.api-keys/external-service}";
    String certificateKeystore = "${vault.certificates/ssl-keystore}";
    
    // Transcribe vault references to actual secret values
    String actualDbPassword = transcriber.transcribe(dbPassword);
    String actualApiKey = transcriber.transcribe(apiKey);
    String actualKeystorePassword = transcriber.transcribe(certificateKeystore);
    
    // Use the resolved secrets
    DataSource dataSource = createDataSource("jdbc:postgresql://localhost/mydb", 
                                            "dbuser", actualDbPassword);
    
    HttpClient httpClient = createHttpClient(actualApiKey);
    
    KeyStore keyStore = loadKeyStore("keystore.p12", actualKeystorePassword);
}

Direct Vault Provider Usage

// Direct usage of vault provider
try (KeycloakSession session = sessionFactory.create()) {
    VaultProvider vaultProvider = session.getProvider(VaultProvider.class);
    
    // Retrieve database password
    try (VaultStringSecret dbSecret = (VaultStringSecret) vaultProvider.obtainSecret("secret/myapp/database")) {
        String dbPassword = dbSecret.getOrDefault("defaultPassword");
        
        // Use the password
        Connection connection = DriverManager.getConnection(
            "jdbc:postgresql://localhost/mydb", "dbuser", dbPassword);
    }
    
    // Retrieve API key
    try (VaultStringSecret apiSecret = (VaultStringSecret) vaultProvider.obtainSecret("api-keys/payment-gateway")) {
        String apiKey = apiSecret.get().orElse(null);
        if (apiKey != null) {
            PaymentGatewayClient client = new PaymentGatewayClient(apiKey);
        }
    }
    
    // Retrieve certificate data
    try (VaultRawSecret certSecret = vaultProvider.obtainSecret("certificates/client-cert")) {
        byte[] certData = certSecret.getOrDefault(new byte[0]);
        if (certData.length > 0) {
            X509Certificate certificate = loadCertificate(certData);
        }
    }
}

Custom Vault Key Resolver

public class CustomVaultKeyResolver implements VaultKeyResolver {
    private static final Pattern VAULT_PATTERN = Pattern.compile("\\$\\{vault\\.([^}]+)\\}");
    private static final String VAULT_PREFIX = "${vault.";
    private static final String VAULT_SUFFIX = "}";

    @Override
    public boolean isVaultKey(String value) {
        return value != null && value.startsWith(VAULT_PREFIX) && value.endsWith(VAULT_SUFFIX);
    }

    @Override
    public String getVaultKey(String value) {
        if (!isVaultKey(value)) {
            return null;
        }
        
        Matcher matcher = VAULT_PATTERN.matcher(value);
        if (matcher.matches()) {
            return matcher.group(1);
        }
        
        return null;
    }

    @Override
    public String createVaultReference(String key) {
        if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("Vault key cannot be null or empty");
        }
        
        return VAULT_PREFIX + key + VAULT_SUFFIX;
    }
}

Environment-Specific Vault Configuration

// Configure vault provider based on environment
public class EnvironmentVaultProviderFactory implements VaultProviderFactory {
    public static final String PROVIDER_ID = "environment-vault";

    @Override
    public VaultProvider create(KeycloakSession session) {
        String environment = System.getProperty("keycloak.environment", "development");
        
        switch (environment.toLowerCase()) {
            case "production":
                return createProductionVaultProvider();
            case "staging":
                return createStagingVaultProvider();
            case "development":
            default:
                return createDevelopmentVaultProvider();
        }
    }

    private VaultProvider createProductionVaultProvider() {
        // Use HashiCorp Vault for production
        return new HashiCorpVaultProvider(
            createVaultClient("https://vault.prod.example.com:8200"),
            "prod-secrets"
        );
    }

    private VaultProvider createStagingVaultProvider() {
        // Use HashiCorp Vault for staging
        return new HashiCorpVaultProvider(
            createVaultClient("https://vault.staging.example.com:8200"),
            "staging-secrets"
        );
    }

    private VaultProvider createDevelopmentVaultProvider() {
        // Use file-based vault for development
        return new FileBasedVaultProvider("/etc/keycloak/dev-secrets");
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }
}

// Simple file-based vault for development
public class FileBasedVaultProvider implements VaultProvider {
    private final Path secretsDirectory;

    public FileBasedVaultProvider(String secretsPath) {
        this.secretsDirectory = Paths.get(secretsPath);
    }

    @Override
    public VaultRawSecret obtainSecret(String vaultSecretId) {
        try {
            Path secretFile = secretsDirectory.resolve(vaultSecretId + ".txt");
            if (!Files.exists(secretFile)) {
                return new EmptyVaultSecret();
            }

            String content = Files.readString(secretFile, StandardCharsets.UTF_8).trim();
            return new StringVaultSecret(content);
            
        } catch (IOException e) {
            logger.error("Failed to read secret file: " + vaultSecretId, e);
            return new EmptyVaultSecret();
        }
    }

    @Override
    public void close() {
        // Nothing to close for file-based implementation
    }
}

Vault Secret Caching

// Vault provider with caching for better performance
public class CachedVaultProvider implements VaultProvider {
    private final VaultProvider delegate;
    private final Cache<String, VaultRawSecret> secretCache;
    private final Duration cacheExpiration;

    public CachedVaultProvider(VaultProvider delegate, Duration cacheExpiration) {
        this.delegate = delegate;
        this.cacheExpiration = cacheExpiration;
        this.secretCache = Caffeine.newBuilder()
            .expireAfterWrite(cacheExpiration)
            .maximumSize(1000)
            .removalListener((key, value, cause) -> {
                if (value instanceof VaultRawSecret) {
                    ((VaultRawSecret) value).close();
                }
            })
            .build();
    }

    @Override
    public VaultRawSecret obtainSecret(String vaultSecretId) {
        return secretCache.get(vaultSecretId, key -> {
            VaultRawSecret secret = delegate.obtainSecret(key);
            
            // Convert to cacheable secret
            if (secret instanceof VaultStringSecret) {
                VaultStringSecret stringSecret = (VaultStringSecret) secret;
                String value = stringSecret.getOrDefault(null);
                secret.close(); // Close original
                
                return value != null ? new CacheableStringSecret(value) : new EmptyVaultSecret();
            }
            
            return secret;
        });
    }

    @Override
    public void close() {
        secretCache.invalidateAll();
        delegate.close();
    }

    private static class CacheableStringSecret implements VaultStringSecret {
        private final String value;

        public CacheableStringSecret(String value) {
            this.value = value;
        }

        @Override
        public Optional<String> get() {
            return Optional.ofNullable(value);
        }

        @Override
        public void close() {
            // Nothing to close for cached values
        }
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-org-keycloak--keycloak-server-spi

docs

authentication-sessions.md

component-framework.md

core-models.md

credential-management.md

index.md

organization-management.md

provider-framework.md

session-management.md

user-storage.md

validation-framework.md

vault-integration.md

tile.json