0
# Component Framework
1
2
The component framework provides a configuration-driven approach to managing provider instances in Keycloak. It enables dynamic configuration of providers with validation, lifecycle management, and hierarchical organization.
3
4
## Core Component Interfaces
5
6
### ComponentFactory
7
8
Factory interface for component-based providers with configuration support.
9
10
```java { .api }
11
public interface ComponentFactory<T extends Provider> extends ProviderFactory<T> {
12
/**
13
* Creates a provider instance with the given component configuration.
14
*
15
* @param session the Keycloak session
16
* @param model the component model with configuration
17
* @return provider instance
18
*/
19
T create(KeycloakSession session, ComponentModel model);
20
21
/**
22
* Validates the component configuration.
23
*
24
* @param session the Keycloak session
25
* @param realm the realm
26
* @param model the component model to validate
27
* @throws ComponentValidationException if validation fails
28
*/
29
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
30
throws ComponentValidationException;
31
32
/**
33
* Called when a component instance is created.
34
*
35
* @param session the Keycloak session
36
* @param realm the realm
37
* @param model the component model
38
*/
39
void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model);
40
41
/**
42
* Called when a component configuration is updated.
43
*
44
* @param session the Keycloak session
45
* @param realm the realm
46
* @param oldModel the previous component model
47
* @param newModel the updated component model
48
*/
49
void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel);
50
51
/**
52
* Gets configuration properties for this component type.
53
*
54
* @return list of configuration properties
55
*/
56
@Override
57
default List<ProviderConfigProperty> getConfigMetadata() {
58
return Collections.emptyList();
59
}
60
61
/**
62
* Gets configuration properties specific to the component context.
63
*
64
* @param session the Keycloak session
65
* @param realm the realm
66
* @return list of configuration properties
67
*/
68
default List<ProviderConfigProperty> getConfigProperties(KeycloakSession session, RealmModel realm) {
69
return getConfigMetadata();
70
}
71
}
72
```
73
74
### SubComponentFactory
75
76
Factory for components that can have sub-components.
77
78
```java { .api }
79
public interface SubComponentFactory<T extends Provider, S extends Provider> extends ComponentFactory<T> {
80
/**
81
* Gets the provider class for sub-components.
82
*
83
* @return sub-component provider class
84
*/
85
Class<S> getSubComponentClass();
86
87
/**
88
* Gets supported sub-component types.
89
*
90
* @param session the Keycloak session
91
* @param realm the realm
92
* @param model the parent component model
93
* @return list of supported sub-component types
94
*/
95
List<String> getSubComponentTypes(KeycloakSession session, RealmModel realm, ComponentModel model);
96
97
/**
98
* Gets configuration properties for a sub-component type.
99
*
100
* @param session the Keycloak session
101
* @param realm the realm
102
* @param parent the parent component model
103
* @param subType the sub-component type
104
* @return list of configuration properties
105
*/
106
List<ProviderConfigProperty> getSubComponentConfigProperties(KeycloakSession session, RealmModel realm,
107
ComponentModel parent, String subType);
108
}
109
```
110
111
## Component Model
112
113
### ComponentModel
114
115
Model representing a component configuration.
116
117
```java { .api }
118
public class ComponentModel {
119
private String id;
120
private String name;
121
private String providerId;
122
private String providerType;
123
private String parentId;
124
private String subType;
125
private MultivaluedHashMap<String, String> config;
126
127
// Constructors
128
public ComponentModel() {
129
this.config = new MultivaluedHashMap<>();
130
}
131
132
public ComponentModel(ComponentModel copy) {
133
this.id = copy.id;
134
this.name = copy.name;
135
this.providerId = copy.providerId;
136
this.providerType = copy.providerType;
137
this.parentId = copy.parentId;
138
this.subType = copy.subType;
139
this.config = new MultivaluedHashMap<>(copy.config);
140
}
141
142
public ComponentModel(String id, String name, String parentId, String providerId, String providerType) {
143
this();
144
this.id = id;
145
this.name = name;
146
this.parentId = parentId;
147
this.providerId = providerId;
148
this.providerType = providerType;
149
}
150
151
// Getters and setters
152
public String getId() { return id; }
153
public void setId(String id) { this.id = id; }
154
155
public String getName() { return name; }
156
public void setName(String name) { this.name = name; }
157
158
public String getProviderId() { return providerId; }
159
public void setProviderId(String providerId) { this.providerId = providerId; }
160
161
public String getProviderType() { return providerType; }
162
public void setProviderType(String providerType) { this.providerType = providerType; }
163
164
public String getParentId() { return parentId; }
165
public void setParentId(String parentId) { this.parentId = parentId; }
166
167
public String getSubType() { return subType; }
168
public void setSubType(String subType) { this.subType = subType; }
169
170
public MultivaluedHashMap<String, String> getConfig() { return config; }
171
public void setConfig(MultivaluedHashMap<String, String> config) { this.config = config; }
172
173
// Configuration helper methods
174
public String get(String key) {
175
List<String> values = config.get(key);
176
return values != null && !values.isEmpty() ? values.get(0) : null;
177
}
178
179
public String get(String key, String defaultValue) {
180
String value = get(key);
181
return value != null ? value : defaultValue;
182
}
183
184
public List<String> getList(String key) {
185
List<String> values = config.get(key);
186
return values != null ? values : Collections.emptyList();
187
}
188
189
public int get(String key, int defaultValue) {
190
String value = get(key);
191
if (value == null) return defaultValue;
192
try {
193
return Integer.parseInt(value);
194
} catch (NumberFormatException e) {
195
return defaultValue;
196
}
197
}
198
199
public long get(String key, long defaultValue) {
200
String value = get(key);
201
if (value == null) return defaultValue;
202
try {
203
return Long.parseLong(value);
204
} catch (NumberFormatException e) {
205
return defaultValue;
206
}
207
}
208
209
public boolean get(String key, boolean defaultValue) {
210
String value = get(key);
211
return value != null ? Boolean.parseBoolean(value) : defaultValue;
212
}
213
214
public void put(String key, String value) {
215
if (value == null) {
216
config.remove(key);
217
} else {
218
config.putSingle(key, value);
219
}
220
}
221
222
public void put(String key, List<String> values) {
223
if (values == null || values.isEmpty()) {
224
config.remove(key);
225
} else {
226
config.put(key, values);
227
}
228
}
229
230
public void put(String key, int value) {
231
put(key, String.valueOf(value));
232
}
233
234
public void put(String key, long value) {
235
put(key, String.valueOf(value));
236
}
237
238
public void put(String key, boolean value) {
239
put(key, String.valueOf(value));
240
}
241
242
public boolean contains(String key) {
243
return config.containsKey(key);
244
}
245
246
@Override
247
public boolean equals(Object obj) {
248
if (this == obj) return true;
249
if (obj == null || getClass() != obj.getClass()) return false;
250
ComponentModel that = (ComponentModel) obj;
251
return Objects.equals(id, that.id);
252
}
253
254
@Override
255
public int hashCode() {
256
return Objects.hash(id);
257
}
258
259
@Override
260
public String toString() {
261
return String.format("ComponentModel{id='%s', name='%s', providerId='%s', providerType='%s'}",
262
id, name, providerId, providerType);
263
}
264
}
265
```
266
267
### ConfiguredComponent
268
269
Interface for components that require configuration.
270
271
```java { .api }
272
public interface ConfiguredComponent {
273
/**
274
* Gets the component model configuration.
275
*
276
* @return component model
277
*/
278
ComponentModel getModel();
279
}
280
```
281
282
### PrioritizedComponentModel
283
284
Component model with priority ordering.
285
286
```java { .api }
287
public class PrioritizedComponentModel extends ComponentModel {
288
private int priority;
289
290
public PrioritizedComponentModel() {
291
super();
292
}
293
294
public PrioritizedComponentModel(ComponentModel copy) {
295
super(copy);
296
if (copy instanceof PrioritizedComponentModel) {
297
this.priority = ((PrioritizedComponentModel) copy).priority;
298
}
299
}
300
301
public PrioritizedComponentModel(ComponentModel copy, int priority) {
302
super(copy);
303
this.priority = priority;
304
}
305
306
public int getPriority() { return priority; }
307
public void setPriority(int priority) { this.priority = priority; }
308
309
@Override
310
public String toString() {
311
return String.format("PrioritizedComponentModel{id='%s', name='%s', providerId='%s', priority=%d}",
312
getId(), getName(), getProviderId(), priority);
313
}
314
}
315
```
316
317
### JsonConfigComponentModel
318
319
Component model that stores configuration as JSON.
320
321
```java { .api }
322
public class JsonConfigComponentModel extends ComponentModel {
323
private Map<String, Object> jsonConfig;
324
325
public JsonConfigComponentModel() {
326
super();
327
this.jsonConfig = new HashMap<>();
328
}
329
330
public JsonConfigComponentModel(ComponentModel copy) {
331
super(copy);
332
this.jsonConfig = new HashMap<>();
333
}
334
335
public Map<String, Object> getJsonConfig() { return jsonConfig; }
336
public void setJsonConfig(Map<String, Object> jsonConfig) { this.jsonConfig = jsonConfig; }
337
338
public <T> T getJsonConfig(String key, Class<T> type) {
339
Object value = jsonConfig.get(key);
340
return type.isInstance(value) ? type.cast(value) : null;
341
}
342
343
public void putJsonConfig(String key, Object value) {
344
if (value == null) {
345
jsonConfig.remove(key);
346
} else {
347
jsonConfig.put(key, value);
348
}
349
}
350
}
351
```
352
353
## Exception Handling
354
355
### ComponentValidationException
356
357
Exception thrown during component validation.
358
359
```java { .api }
360
public class ComponentValidationException extends Exception {
361
private final Object[] parameters;
362
363
public ComponentValidationException(String message) {
364
super(message);
365
this.parameters = null;
366
}
367
368
public ComponentValidationException(String message, Object... parameters) {
369
super(message);
370
this.parameters = parameters;
371
}
372
373
public ComponentValidationException(String message, Throwable cause) {
374
super(message, cause);
375
this.parameters = null;
376
}
377
378
public ComponentValidationException(String message, Throwable cause, Object... parameters) {
379
super(message, cause);
380
this.parameters = parameters;
381
}
382
383
public Object[] getParameters() { return parameters; }
384
385
public String getLocalizedMessage() {
386
if (parameters != null && parameters.length > 0) {
387
return String.format(getMessage(), parameters);
388
}
389
return getMessage();
390
}
391
}
392
```
393
394
## Usage Examples
395
396
### Creating a Component Factory
397
398
```java
399
public class EmailProviderFactory implements ComponentFactory<EmailProvider> {
400
public static final String PROVIDER_ID = "smtp-email";
401
402
@Override
403
public EmailProvider create(KeycloakSession session, ComponentModel model) {
404
return new SmtpEmailProvider(session, model);
405
}
406
407
@Override
408
public String getId() {
409
return PROVIDER_ID;
410
}
411
412
@Override
413
public List<ProviderConfigProperty> getConfigMetadata() {
414
return Arrays.asList(
415
new ProviderConfigProperty("host", "SMTP Host", "SMTP server hostname",
416
ProviderConfigProperty.STRING_TYPE, "localhost"),
417
new ProviderConfigProperty("port", "SMTP Port", "SMTP server port",
418
ProviderConfigProperty.STRING_TYPE, "587"),
419
new ProviderConfigProperty("username", "Username", "SMTP username",
420
ProviderConfigProperty.STRING_TYPE, null),
421
new ProviderConfigProperty("password", "Password", "SMTP password",
422
ProviderConfigProperty.PASSWORD, null),
423
new ProviderConfigProperty("tls", "Enable TLS", "Enable TLS encryption",
424
ProviderConfigProperty.BOOLEAN_TYPE, true),
425
new ProviderConfigProperty("from", "From Address", "Email from address",
426
ProviderConfigProperty.STRING_TYPE, "noreply@example.com")
427
);
428
}
429
430
@Override
431
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
432
throws ComponentValidationException {
433
434
String host = model.get("host");
435
if (host == null || host.trim().isEmpty()) {
436
throw new ComponentValidationException("SMTP host is required");
437
}
438
439
String port = model.get("port");
440
if (port != null && !port.isEmpty()) {
441
try {
442
int portNum = Integer.parseInt(port);
443
if (portNum < 1 || portNum > 65535) {
444
throw new ComponentValidationException("Port must be between 1 and 65535");
445
}
446
} catch (NumberFormatException e) {
447
throw new ComponentValidationException("Invalid port number: " + port);
448
}
449
}
450
451
String fromAddress = model.get("from");
452
if (fromAddress == null || !fromAddress.contains("@")) {
453
throw new ComponentValidationException("Valid from address is required");
454
}
455
456
// Test SMTP connection
457
try {
458
testSmtpConnection(model);
459
} catch (Exception e) {
460
throw new ComponentValidationException("Failed to connect to SMTP server: " + e.getMessage(), e);
461
}
462
}
463
464
@Override
465
public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
466
// Log component creation
467
logger.info("Created email provider component: " + model.getName());
468
}
469
470
@Override
471
public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
472
// Handle configuration changes
473
if (!Objects.equals(oldModel.get("host"), newModel.get("host"))) {
474
logger.info("Email provider host changed from {} to {}",
475
oldModel.get("host"), newModel.get("host"));
476
}
477
}
478
479
private void testSmtpConnection(ComponentModel model) throws Exception {
480
// Implementation to test SMTP connection
481
Properties props = new Properties();
482
props.put("mail.smtp.host", model.get("host"));
483
props.put("mail.smtp.port", model.get("port", "587"));
484
props.put("mail.smtp.starttls.enable", model.get("tls", true));
485
486
String username = model.get("username");
487
String password = model.get("password");
488
489
Session mailSession;
490
if (username != null && password != null) {
491
props.put("mail.smtp.auth", "true");
492
mailSession = Session.getInstance(props, new Authenticator() {
493
@Override
494
protected PasswordAuthentication getPasswordAuthentication() {
495
return new PasswordAuthentication(username, password);
496
}
497
});
498
} else {
499
mailSession = Session.getInstance(props);
500
}
501
502
Transport transport = mailSession.getTransport("smtp");
503
transport.connect();
504
transport.close();
505
}
506
}
507
```
508
509
### Creating a Sub-Component Factory
510
511
```java
512
public class LdapProviderFactory implements SubComponentFactory<UserStorageProvider, UserStorageProviderModel> {
513
public static final String PROVIDER_ID = "ldap";
514
515
@Override
516
public UserStorageProvider create(KeycloakSession session, ComponentModel model) {
517
return new LdapUserStorageProvider(session, model);
518
}
519
520
@Override
521
public String getId() {
522
return PROVIDER_ID;
523
}
524
525
@Override
526
public Class<UserStorageProviderModel> getSubComponentClass() {
527
return UserStorageProviderModel.class;
528
}
529
530
@Override
531
public List<String> getSubComponentTypes(KeycloakSession session, RealmModel realm, ComponentModel model) {
532
return Arrays.asList("attribute-mapper", "group-mapper", "role-mapper");
533
}
534
535
@Override
536
public List<ProviderConfigProperty> getSubComponentConfigProperties(KeycloakSession session, RealmModel realm,
537
ComponentModel parent, String subType) {
538
switch (subType) {
539
case "attribute-mapper":
540
return getAttributeMapperProperties();
541
case "group-mapper":
542
return getGroupMapperProperties();
543
case "role-mapper":
544
return getRoleMapperProperties();
545
default:
546
return Collections.emptyList();
547
}
548
}
549
550
@Override
551
public List<ProviderConfigProperty> getConfigMetadata() {
552
return Arrays.asList(
553
new ProviderConfigProperty("connectionUrl", "Connection URL", "LDAP connection URL",
554
ProviderConfigProperty.STRING_TYPE, "ldap://localhost:389"),
555
new ProviderConfigProperty("bindDn", "Bind DN", "DN of LDAP admin user",
556
ProviderConfigProperty.STRING_TYPE, null),
557
new ProviderConfigProperty("bindCredential", "Bind Credential", "Password of LDAP admin user",
558
ProviderConfigProperty.PASSWORD, null),
559
new ProviderConfigProperty("usersDn", "Users DN", "DN where users are stored",
560
ProviderConfigProperty.STRING_TYPE, "ou=users,dc=example,dc=com"),
561
new ProviderConfigProperty("usernameAttribute", "Username Attribute", "LDAP attribute for username",
562
ProviderConfigProperty.STRING_TYPE, "uid")
563
);
564
}
565
566
private List<ProviderConfigProperty> getAttributeMapperProperties() {
567
return Arrays.asList(
568
new ProviderConfigProperty("ldap.attribute", "LDAP Attribute", "LDAP attribute name",
569
ProviderConfigProperty.STRING_TYPE, null),
570
new ProviderConfigProperty("user.model.attribute", "User Model Attribute", "User model attribute name",
571
ProviderConfigProperty.STRING_TYPE, null),
572
new ProviderConfigProperty("read.only", "Read Only", "Whether attribute is read-only",
573
ProviderConfigProperty.BOOLEAN_TYPE, false)
574
);
575
}
576
577
private List<ProviderConfigProperty> getGroupMapperProperties() {
578
return Arrays.asList(
579
new ProviderConfigProperty("groups.dn", "Groups DN", "DN where groups are stored",
580
ProviderConfigProperty.STRING_TYPE, "ou=groups,dc=example,dc=com"),
581
new ProviderConfigProperty("group.name.attribute", "Group Name Attribute", "LDAP attribute for group name",
582
ProviderConfigProperty.STRING_TYPE, "cn"),
583
new ProviderConfigProperty("membership.attribute", "Membership Attribute", "LDAP attribute for membership",
584
ProviderConfigProperty.STRING_TYPE, "member")
585
);
586
}
587
588
private List<ProviderConfigProperty> getRoleMapperProperties() {
589
return Arrays.asList(
590
new ProviderConfigProperty("roles.dn", "Roles DN", "DN where roles are stored",
591
ProviderConfigProperty.STRING_TYPE, "ou=roles,dc=example,dc=com"),
592
new ProviderConfigProperty("role.name.attribute", "Role Name Attribute", "LDAP attribute for role name",
593
ProviderConfigProperty.STRING_TYPE, "cn"),
594
new ProviderConfigProperty("use.realm.roles.mapping", "Use Realm Roles", "Map to realm roles",
595
ProviderConfigProperty.BOOLEAN_TYPE, true)
596
);
597
}
598
}
599
```
600
601
### Working with Components
602
603
```java
604
// Create and configure a component
605
try (KeycloakSession session = sessionFactory.create()) {
606
RealmModel realm = session.realms().getRealmByName("myrealm");
607
608
// Create email provider component
609
ComponentModel emailComponent = new ComponentModel();
610
emailComponent.setName("SMTP Email Provider");
611
emailComponent.setProviderId("smtp-email");
612
emailComponent.setProviderType("email");
613
emailComponent.setParentId(realm.getId());
614
615
// Configure SMTP settings
616
emailComponent.put("host", "smtp.gmail.com");
617
emailComponent.put("port", "587");
618
emailComponent.put("username", "myapp@gmail.com");
619
emailComponent.put("password", "app-password");
620
emailComponent.put("tls", true);
621
emailComponent.put("from", "myapp@gmail.com");
622
623
// Add the component to the realm
624
realm.addComponent(emailComponent);
625
626
// Get the email provider instance
627
EmailProvider emailProvider = session.getProvider(EmailProvider.class, emailComponent.getId());
628
629
// Use the provider
630
emailProvider.send("test@example.com", "Test Subject", "Test message");
631
}
632
```
633
634
### Component Hierarchy Management
635
636
```java
637
// Working with component hierarchies
638
try (KeycloakSession session = sessionFactory.create()) {
639
RealmModel realm = session.realms().getRealmByName("myrealm");
640
641
// Create parent component (LDAP provider)
642
ComponentModel ldapComponent = new ComponentModel();
643
ldapComponent.setName("LDAP User Storage");
644
ldapComponent.setProviderId("ldap");
645
ldapComponent.setProviderType("org.keycloak.storage.UserStorageProvider");
646
ldapComponent.setParentId(realm.getId());
647
648
// Configure LDAP settings
649
ldapComponent.put("connectionUrl", "ldap://ldap.example.com:389");
650
ldapComponent.put("bindDn", "cn=admin,dc=example,dc=com");
651
ldapComponent.put("bindCredential", "admin123");
652
ldapComponent.put("usersDn", "ou=users,dc=example,dc=com");
653
654
realm.addComponent(ldapComponent);
655
656
// Create sub-component (attribute mapper)
657
ComponentModel attributeMapper = new ComponentModel();
658
attributeMapper.setName("Email Attribute Mapper");
659
attributeMapper.setProviderId("user-attribute-ldap-mapper");
660
attributeMapper.setProviderType("org.keycloak.storage.ldap.mappers.LDAPStorageMapper");
661
attributeMapper.setParentId(ldapComponent.getId());
662
attributeMapper.setSubType("attribute-mapper");
663
664
// Configure attribute mapping
665
attributeMapper.put("ldap.attribute", "mail");
666
attributeMapper.put("user.model.attribute", "email");
667
attributeMapper.put("read.only", false);
668
669
realm.addComponent(attributeMapper);
670
671
// Get all sub-components of the LDAP provider
672
Stream<ComponentModel> subComponents = realm.getComponentsStream(ldapComponent.getId());
673
subComponents.forEach(comp -> {
674
System.out.println("Sub-component: " + comp.getName() + " (" + comp.getSubType() + ")");
675
});
676
}
677
```
678
679
### Component Configuration Validation
680
681
```java
682
// Custom component with validation
683
public class CustomComponentFactory implements ComponentFactory<CustomProvider> {
684
@Override
685
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
686
throws ComponentValidationException {
687
688
// Validate required fields
689
validateRequired(model, "apiKey", "API Key is required");
690
validateRequired(model, "endpoint", "Endpoint URL is required");
691
692
// Validate URL format
693
String endpoint = model.get("endpoint");
694
try {
695
new URL(endpoint);
696
} catch (MalformedURLException e) {
697
throw new ComponentValidationException("Invalid endpoint URL: " + endpoint);
698
}
699
700
// Validate numeric fields
701
validateNumeric(model, "timeout", 1, 300, "Timeout must be between 1 and 300 seconds");
702
validateNumeric(model, "maxRetries", 0, 10, "Max retries must be between 0 and 10");
703
704
// Validate enum values
705
String protocol = model.get("protocol");
706
if (protocol != null && !Arrays.asList("http", "https").contains(protocol.toLowerCase())) {
707
throw new ComponentValidationException("Protocol must be 'http' or 'https'");
708
}
709
710
// Custom business logic validation
711
validateBusinessRules(session, realm, model);
712
}
713
714
private void validateRequired(ComponentModel model, String key, String errorMessage)
715
throws ComponentValidationException {
716
String value = model.get(key);
717
if (value == null || value.trim().isEmpty()) {
718
throw new ComponentValidationException(errorMessage);
719
}
720
}
721
722
private void validateNumeric(ComponentModel model, String key, int min, int max, String errorMessage)
723
throws ComponentValidationException {
724
String value = model.get(key);
725
if (value != null && !value.isEmpty()) {
726
try {
727
int intValue = Integer.parseInt(value);
728
if (intValue < min || intValue > max) {
729
throw new ComponentValidationException(errorMessage);
730
}
731
} catch (NumberFormatException e) {
732
throw new ComponentValidationException("Invalid number format for " + key + ": " + value);
733
}
734
}
735
}
736
737
private void validateBusinessRules(KeycloakSession session, RealmModel realm, ComponentModel model)
738
throws ComponentValidationException {
739
// Custom validation logic specific to your component
740
String apiKey = model.get("apiKey");
741
if (apiKey != null && apiKey.length() < 32) {
742
throw new ComponentValidationException("API key must be at least 32 characters long");
743
}
744
745
// Check for conflicts with existing components
746
realm.getComponentsStream(realm.getId(), "custom-provider")
747
.filter(comp -> !comp.getId().equals(model.getId()))
748
.filter(comp -> comp.get("endpoint").equals(model.get("endpoint")))
749
.findFirst()
750
.ifPresent(existing -> {
751
throw new RuntimeException(new ComponentValidationException(
752
"Another component is already using this endpoint: " + existing.getName()));
753
});
754
}
755
}
756
```