or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication-sessions.mdcomponent-framework.mdcore-models.mdcredential-management.mdindex.mdorganization-management.mdprovider-framework.mdsession-management.mduser-storage.mdvalidation-framework.mdvault-integration.md

user-storage.mddocs/

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

```