0
# User Storage
1
2
The user storage framework provides interfaces for integrating external user stores with Keycloak. This enables federation with LDAP directories, databases, custom APIs, and other user repositories.
3
4
## Core Storage Interfaces
5
6
### UserStorageProvider
7
8
Base interface for all user storage providers.
9
10
```java { .api }
11
public interface UserStorageProvider extends Provider {
12
// Marker interface - specific capabilities are defined by extending interfaces
13
}
14
```
15
16
### UserLookupProvider
17
18
Provides user lookup capabilities by ID, username, or email.
19
20
```java { .api }
21
public interface UserLookupProvider {
22
/**
23
* Looks up a user by ID.
24
*
25
* @param realm the realm
26
* @param id the user ID
27
* @return user model or null if not found
28
*/
29
UserModel getUserById(RealmModel realm, String id);
30
31
/**
32
* Looks up a user by username.
33
*
34
* @param realm the realm
35
* @param username the username
36
* @return user model or null if not found
37
*/
38
UserModel getUserByUsername(RealmModel realm, String username);
39
40
/**
41
* Looks up a user by email address.
42
*
43
* @param realm the realm
44
* @param email the email address
45
* @return user model or null if not found
46
*/
47
UserModel getUserByEmail(RealmModel realm, String email);
48
}
49
```
50
51
### UserQueryProvider
52
53
Provides user query and search capabilities.
54
55
```java { .api }
56
public interface UserQueryProvider extends UserQueryMethodsProvider, UserCountMethodsProvider {
57
// Inherited from UserQueryMethodsProvider:
58
59
/**
60
* Searches for users by search term.
61
*
62
* @param realm the realm
63
* @param search the search term
64
* @return stream of matching users
65
*/
66
Stream<UserModel> searchForUserStream(RealmModel realm, String search);
67
68
/**
69
* Searches for users by search term with pagination.
70
*
71
* @param realm the realm
72
* @param search the search term
73
* @param firstResult first result index
74
* @param maxResults maximum number of results
75
* @return stream of matching users
76
*/
77
Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
78
79
/**
80
* Searches for users by attributes.
81
*
82
* @param realm the realm
83
* @param attributes search attributes map
84
* @return stream of matching users
85
*/
86
Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue);
87
88
/**
89
* Gets users in a specific group.
90
*
91
* @param realm the realm
92
* @param group the group
93
* @return stream of group members
94
*/
95
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group);
96
97
/**
98
* Gets users in a specific group with pagination.
99
*
100
* @param realm the realm
101
* @param group the group
102
* @param firstResult first result index
103
* @param maxResults maximum number of results
104
* @return stream of group members
105
*/
106
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);
107
108
/**
109
* Searches for users by attributes with pagination.
110
*
111
* @param realm the realm
112
* @param attributes search attributes map
113
* @param firstResult first result index
114
* @param maxResults maximum number of results
115
* @return stream of matching users
116
*/
117
Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue, Integer firstResult, Integer maxResults);
118
119
// Inherited from UserCountMethodsProvider:
120
121
/**
122
* Gets total number of users in the realm.
123
*
124
* @param realm the realm
125
* @return user count
126
*/
127
int getUsersCount(RealmModel realm);
128
129
/**
130
* Gets number of users matching search criteria.
131
*
132
* @param realm the realm
133
* @param search the search term
134
* @return matching user count
135
*/
136
int getUsersCount(RealmModel realm, String search);
137
138
/**
139
* Gets number of users with specific attribute value.
140
*
141
* @param realm the realm
142
* @param attrName attribute name
143
* @param attrValue attribute value
144
* @return matching user count
145
*/
146
int getUsersCount(RealmModel realm, String attrName, String attrValue);
147
}
148
```
149
150
### UserRegistrationProvider
151
152
Enables user registration operations (create, update, delete).
153
154
```java { .api }
155
public interface UserRegistrationProvider {
156
/**
157
* Adds a new user to the storage.
158
*
159
* @param realm the realm
160
* @param username the username
161
* @return created user model
162
*/
163
UserModel addUser(RealmModel realm, String username);
164
165
/**
166
* Removes a user from the storage.
167
*
168
* @param realm the realm
169
* @param user the user to remove
170
* @return true if user was removed
171
*/
172
boolean removeUser(RealmModel realm, UserModel user);
173
}
174
```
175
176
### UserBulkUpdateProvider
177
178
Provides bulk user update capabilities.
179
180
```java { .api }
181
public interface UserBulkUpdateProvider {
182
/**
183
* Disables all users in the realm.
184
*
185
* @param realm the realm
186
* @param includeServiceAccount whether to include service accounts
187
*/
188
void disableUsers(RealmModel realm, boolean includeServiceAccount);
189
190
/**
191
* Removes imported users (federated users) from local storage.
192
*
193
* @param realm the realm
194
* @param storageProviderId the storage provider ID
195
*/
196
void removeImportedUsers(RealmModel realm, String storageProviderId);
197
198
/**
199
* Removes expired users based on configuration.
200
*
201
* @param realm the realm
202
*/
203
void removeExpiredUsers(RealmModel realm);
204
}
205
```
206
207
## Storage Utilities
208
209
### StorageId
210
211
Utility class for handling federated storage IDs.
212
213
```java { .api }
214
public class StorageId {
215
private final String storageProviderId;
216
private final String externalId;
217
218
/**
219
* Creates a StorageId from provider ID and external ID.
220
*
221
* @param storageProviderId the storage provider ID
222
* @param externalId the external ID
223
*/
224
public StorageId(String storageProviderId, String externalId) {
225
this.storageProviderId = storageProviderId;
226
this.externalId = externalId;
227
}
228
229
/**
230
* Parses a storage ID string.
231
*
232
* @param id the storage ID string
233
* @return parsed StorageId
234
*/
235
public static StorageId resolveId(String id) {
236
// Implementation parses "f:{providerId}:{externalId}" format
237
}
238
239
/**
240
* Checks if an ID is a federated/external ID.
241
*
242
* @param id the ID to check
243
* @return true if federated ID
244
*/
245
public static boolean isLocalId(String id) {
246
return id != null && !id.startsWith("f:");
247
}
248
249
/**
250
* Gets the storage provider ID.
251
*
252
* @return storage provider ID
253
*/
254
public String getProviderId() {
255
return storageProviderId;
256
}
257
258
/**
259
* Gets the external ID.
260
*
261
* @return external ID
262
*/
263
public String getExternalId() {
264
return externalId;
265
}
266
267
/**
268
* Gets the full keycloak ID.
269
*
270
* @return full ID in format "f:{providerId}:{externalId}"
271
*/
272
public String getId() {
273
return "f:" + storageProviderId + ":" + externalId;
274
}
275
}
276
```
277
278
### SynchronizationResult
279
280
Result object for user synchronization operations.
281
282
```java { .api }
283
public class SynchronizationResult {
284
private boolean ignored = false;
285
private int added = 0;
286
private int updated = 0;
287
private int removed = 0;
288
private int failed = 0;
289
290
public static SynchronizationResult empty() {
291
return new SynchronizationResult();
292
}
293
294
public static SynchronizationResult ignored() {
295
SynchronizationResult result = new SynchronizationResult();
296
result.ignored = true;
297
return result;
298
}
299
300
// Getters and setters
301
public boolean isIgnored() { return ignored; }
302
public void setIgnored(boolean ignored) { this.ignored = ignored; }
303
304
public int getAdded() { return added; }
305
public void setAdded(int added) { this.added = added; }
306
public void increaseAdded() { this.added++; }
307
308
public int getUpdated() { return updated; }
309
public void setUpdated(int updated) { this.updated = updated; }
310
public void increaseUpdated() { this.updated++; }
311
312
public int getRemoved() { return removed; }
313
public void setRemoved(int removed) { this.removed = removed; }
314
public void increaseRemoved() { this.removed++; }
315
316
public int getFailed() { return failed; }
317
public void setFailed(int failed) { this.failed = failed; }
318
public void increaseFailed() { this.failed++; }
319
320
public void add(SynchronizationResult other) {
321
this.added += other.added;
322
this.updated += other.updated;
323
this.removed += other.removed;
324
this.failed += other.failed;
325
}
326
327
@Override
328
public String toString() {
329
return String.format("SynchronizationResult [added=%d, updated=%d, removed=%d, failed=%d, ignored=%s]",
330
added, updated, removed, failed, ignored);
331
}
332
}
333
```
334
335
## Storage Lookup Providers
336
337
### ClientLookupProvider
338
339
Provides client lookup capabilities for federated client storage.
340
341
```java { .api }
342
public interface ClientLookupProvider {
343
/**
344
* Looks up a client by ID.
345
*
346
* @param realm the realm
347
* @param id the client ID
348
* @return client model or null if not found
349
*/
350
ClientModel getClientById(RealmModel realm, String id);
351
352
/**
353
* Looks up a client by client ID.
354
*
355
* @param realm the realm
356
* @param clientId the client ID string
357
* @return client model or null if not found
358
*/
359
ClientModel getClientByClientId(RealmModel realm, String clientId);
360
361
/**
362
* Searches for clients by client ID.
363
*
364
* @param realm the realm
365
* @param clientId the client ID pattern
366
* @param firstResult first result index
367
* @param maxResults max results
368
* @return stream of matching clients
369
*/
370
Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults);
371
372
/**
373
* Searches for clients by attributes.
374
*
375
* @param realm the realm
376
* @param attributes search attributes
377
* @param firstResult first result index
378
* @param maxResults max results
379
* @return stream of matching clients
380
*/
381
Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults);
382
383
/**
384
* Gets all clients in the realm.
385
*
386
* @param realm the realm
387
* @param firstResult first result index
388
* @param maxResults max results
389
* @return stream of clients
390
*/
391
Stream<ClientModel> getClientsStream(RealmModel realm, Integer firstResult, Integer maxResults);
392
}
393
```
394
395
### GroupLookupProvider
396
397
Provides group lookup capabilities for federated group storage.
398
399
```java { .api }
400
public interface GroupLookupProvider {
401
/**
402
* Looks up a group by ID.
403
*
404
* @param realm the realm
405
* @param id the group ID
406
* @return group model or null if not found
407
*/
408
GroupModel getGroupById(RealmModel realm, String id);
409
410
/**
411
* Searches for groups by name.
412
*
413
* @param realm the realm
414
* @param search the search term
415
* @param firstResult first result index
416
* @param maxResults max results
417
* @return stream of matching groups
418
*/
419
Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
420
}
421
```
422
423
### RoleLookupProvider
424
425
Provides role lookup capabilities for federated role storage.
426
427
```java { .api }
428
public interface RoleLookupProvider {
429
/**
430
* Looks up a role by name.
431
*
432
* @param realm the realm
433
* @param name the role name
434
* @return role model or null if not found
435
*/
436
RoleModel getRealmRole(RealmModel realm, String name);
437
438
/**
439
* Looks up a client role by name.
440
*
441
* @param realm the realm
442
* @param client the client
443
* @param name the role name
444
* @return role model or null if not found
445
*/
446
RoleModel getClientRole(RealmModel realm, ClientModel client, String name);
447
448
/**
449
* Searches for roles by name.
450
*
451
* @param realm the realm
452
* @param search the search term
453
* @param firstResult first result index
454
* @param maxResults max results
455
* @return stream of matching roles
456
*/
457
Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
458
}
459
```
460
461
## Exception Handling
462
463
### ReadOnlyException
464
465
Exception thrown when attempting write operations on read-only storage.
466
467
```java { .api }
468
public class ReadOnlyException extends RuntimeException {
469
public ReadOnlyException(String message) {
470
super(message);
471
}
472
473
public ReadOnlyException(String message, Throwable cause) {
474
super(message, cause);
475
}
476
}
477
```
478
479
## Usage Examples
480
481
### Creating a Custom User Storage Provider
482
483
```java
484
public class LdapUserStorageProvider implements UserStorageProvider,
485
UserLookupProvider,
486
UserQueryProvider,
487
CredentialInputValidator {
488
489
private final KeycloakSession session;
490
private final ComponentModel model;
491
private final LdapConnectionManager connectionManager;
492
493
public LdapUserStorageProvider(KeycloakSession session, ComponentModel model) {
494
this.session = session;
495
this.model = model;
496
this.connectionManager = new LdapConnectionManager(model);
497
}
498
499
@Override
500
public UserModel getUserById(RealmModel realm, String id) {
501
StorageId storageId = new StorageId(id);
502
if (!storageId.getProviderId().equals(model.getId())) {
503
return null;
504
}
505
return getUserByLdapDn(realm, storageId.getExternalId());
506
}
507
508
@Override
509
public UserModel getUserByUsername(RealmModel realm, String username) {
510
try (LdapContext context = connectionManager.getLdapContext()) {
511
String dn = findUserDnByUsername(context, username);
512
if (dn != null) {
513
return createUserModel(realm, dn, username);
514
}
515
} catch (Exception e) {
516
logger.error("LDAP search failed", e);
517
}
518
return null;
519
}
520
521
@Override
522
public UserModel getUserByEmail(RealmModel realm, String email) {
523
try (LdapContext context = connectionManager.getLdapContext()) {
524
String dn = findUserDnByEmail(context, email);
525
if (dn != null) {
526
return createUserModel(realm, dn, extractUsername(dn));
527
}
528
} catch (Exception e) {
529
logger.error("LDAP search failed", e);
530
}
531
return null;
532
}
533
534
@Override
535
public Stream<UserModel> searchForUserStream(RealmModel realm, String search,
536
Integer firstResult, Integer maxResults) {
537
try (LdapContext context = connectionManager.getLdapContext()) {
538
List<String> userDns = searchUsers(context, search, firstResult, maxResults);
539
return userDns.stream()
540
.map(dn -> createUserModel(realm, dn, extractUsername(dn)))
541
.filter(Objects::nonNull);
542
} catch (Exception e) {
543
logger.error("LDAP search failed", e);
544
return Stream.empty();
545
}
546
}
547
548
@Override
549
public int getUsersCount(RealmModel realm) {
550
try (LdapContext context = connectionManager.getLdapContext()) {
551
return countUsers(context);
552
} catch (Exception e) {
553
logger.error("LDAP count failed", e);
554
return 0;
555
}
556
}
557
558
@Override
559
public boolean supportsCredentialType(String credentialType) {
560
return PasswordCredentialModel.TYPE.equals(credentialType);
561
}
562
563
@Override
564
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
565
return supportsCredentialType(credentialType);
566
}
567
568
@Override
569
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
570
if (!supportsCredentialType(input.getType())) {
571
return false;
572
}
573
574
String username = user.getUsername();
575
String password = input.getChallengeResponse();
576
577
try (LdapContext context = connectionManager.getLdapContext(username, password)) {
578
return context != null;
579
} catch (Exception e) {
580
logger.debug("LDAP authentication failed for user " + username, e);
581
return false;
582
}
583
}
584
585
@Override
586
public void close() {
587
if (connectionManager != null) {
588
connectionManager.close();
589
}
590
}
591
592
private UserModel createUserModel(RealmModel realm, String ldapDn, String username) {
593
String id = new StorageId(model.getId(), ldapDn).getId();
594
return new LdapUserModel(session, realm, model, id, username, ldapDn);
595
}
596
}
597
```
598
599
### Creating the User Storage Provider Factory
600
601
```java
602
public class LdapUserStorageProviderFactory implements UserStorageProviderFactory<LdapUserStorageProvider> {
603
public static final String PROVIDER_ID = "ldap";
604
605
@Override
606
public LdapUserStorageProvider create(KeycloakSession session, ComponentModel model) {
607
return new LdapUserStorageProvider(session, model);
608
}
609
610
@Override
611
public String getId() {
612
return PROVIDER_ID;
613
}
614
615
@Override
616
public List<ProviderConfigProperty> getConfigProperties() {
617
return Arrays.asList(
618
new ProviderConfigProperty("ldap.host", "LDAP Host", "LDAP server hostname",
619
ProviderConfigProperty.STRING_TYPE, "localhost"),
620
new ProviderConfigProperty("ldap.port", "LDAP Port", "LDAP server port",
621
ProviderConfigProperty.STRING_TYPE, "389"),
622
new ProviderConfigProperty("ldap.baseDn", "Base DN", "Base DN for user searches",
623
ProviderConfigProperty.STRING_TYPE, "ou=users,dc=example,dc=com"),
624
new ProviderConfigProperty("ldap.bindDn", "Bind DN", "DN for LDAP binding",
625
ProviderConfigProperty.STRING_TYPE, null),
626
new ProviderConfigProperty("ldap.bindPassword", "Bind Password", "Password for LDAP binding",
627
ProviderConfigProperty.PASSWORD, null)
628
);
629
}
630
631
@Override
632
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
633
throws ComponentValidationException {
634
String host = config.get("ldap.host");
635
if (host == null || host.trim().isEmpty()) {
636
throw new ComponentValidationException("LDAP host is required");
637
}
638
639
String baseDn = config.get("ldap.baseDn");
640
if (baseDn == null || baseDn.trim().isEmpty()) {
641
throw new ComponentValidationException("Base DN is required");
642
}
643
644
// Test LDAP connection
645
try {
646
testLdapConnection(config);
647
} catch (Exception e) {
648
throw new ComponentValidationException("Failed to connect to LDAP server: " + e.getMessage());
649
}
650
}
651
652
private void testLdapConnection(ComponentModel config) throws Exception {
653
// Implementation to test LDAP connection
654
}
655
}
656
```
657
658
### Implementing a Read-Only User Model
659
660
```java
661
public class LdapUserModel implements UserModel {
662
private final KeycloakSession session;
663
private final RealmModel realm;
664
private final ComponentModel storageProvider;
665
private final String id;
666
private final String username;
667
private final String ldapDn;
668
private final Map<String, List<String>> attributes = new HashMap<>();
669
670
public LdapUserModel(KeycloakSession session, RealmModel realm, ComponentModel storageProvider,
671
String id, String username, String ldapDn) {
672
this.session = session;
673
this.realm = realm;
674
this.storageProvider = storageProvider;
675
this.id = id;
676
this.username = username;
677
this.ldapDn = ldapDn;
678
679
// Load attributes from LDAP
680
loadAttributesFromLdap();
681
}
682
683
@Override
684
public String getId() {
685
return id;
686
}
687
688
@Override
689
public String getUsername() {
690
return username;
691
}
692
693
@Override
694
public void setUsername(String username) {
695
throw new ReadOnlyException("User is read-only");
696
}
697
698
@Override
699
public String getFirstName() {
700
return getFirstAttribute("givenName");
701
}
702
703
@Override
704
public void setFirstName(String firstName) {
705
throw new ReadOnlyException("User is read-only");
706
}
707
708
@Override
709
public String getLastName() {
710
return getFirstAttribute("sn");
711
}
712
713
@Override
714
public void setLastName(String lastName) {
715
throw new ReadOnlyException("User is read-only");
716
}
717
718
@Override
719
public String getEmail() {
720
return getFirstAttribute("mail");
721
}
722
723
@Override
724
public void setEmail(String email) {
725
throw new ReadOnlyException("User is read-only");
726
}
727
728
@Override
729
public boolean isEnabled() {
730
return true; // Assume LDAP users are enabled
731
}
732
733
@Override
734
public void setEnabled(boolean enabled) {
735
throw new ReadOnlyException("User is read-only");
736
}
737
738
@Override
739
public Map<String, List<String>> getAttributes() {
740
return attributes;
741
}
742
743
@Override
744
public String getFirstAttribute(String name) {
745
List<String> values = attributes.get(name);
746
return values != null && !values.isEmpty() ? values.get(0) : null;
747
}
748
749
@Override
750
public Stream<String> getAttributeStream(String name) {
751
List<String> values = attributes.get(name);
752
return values != null ? values.stream() : Stream.empty();
753
}
754
755
@Override
756
public void setSingleAttribute(String name, String value) {
757
throw new ReadOnlyException("User is read-only");
758
}
759
760
@Override
761
public void setAttribute(String name, List<String> values) {
762
throw new ReadOnlyException("User is read-only");
763
}
764
765
@Override
766
public void removeAttribute(String name) {
767
throw new ReadOnlyException("User is read-only");
768
}
769
770
private void loadAttributesFromLdap() {
771
// Implementation to load attributes from LDAP
772
}
773
774
// Implement other UserModel methods...
775
}
776
```
777
778
### Using Storage Providers
779
780
```java
781
// Access federated users
782
try (KeycloakSession session = sessionFactory.create()) {
783
RealmModel realm = session.realms().getRealmByName("myrealm");
784
785
// Get user from any storage (local or federated)
786
UserModel user = session.users().getUserByUsername(realm, "john.doe");
787
788
// Check if user is from federated storage
789
if (!StorageId.isLocalId(user.getId())) {
790
StorageId storageId = new StorageId(user.getId());
791
String providerId = storageId.getProviderId();
792
String externalId = storageId.getExternalId();
793
794
System.out.println("User is from storage provider: " + providerId);
795
System.out.println("External ID: " + externalId);
796
}
797
798
// Search across all storage providers
799
Stream<UserModel> users = session.users()
800
.searchForUserStream(realm, "john", 0, 10);
801
}
802
```