我已经为我们数据库中的联合用户实现了自定义用户存储提供程序。
我想通过 keycloak 为这些用户管理 OTP,当我在流程中将 OTP 设置为必需并将 OTP 配置为必需操作时,联合用户登录后会显示 OTP 表单,但是当我尝试设置 OTP 时,我收到错误用户对于此更新是只读的。
如何允许只读联合用户通过 keycloak 进行 OTP 配置?
2022-01-31 17:00:12,704 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-669) Uncaught server error: org.keycloak.storage.ReadOnlyException: user is read only for this update
at [email protected]//org.keycloak.storage.adapter.AbstractUserAdapter.removeRequiredAction(AbstractUserAdapter.java:77)
at [email protected]//org.keycloak.services.resources.LoginActionsService.processRequireAction(LoginActionsService.java:1044)
at [email protected]//org.keycloak.services.resources.LoginActionsService.requiredActionPOST(LoginActionsService.java:967)
用户适配器是
public class UserAdminAdapter extends AbstractUserAdapter {
private final CustomUser user;
public UserAdminAdapter(
KeycloakSession session,
RealmModel realm,
ComponentModel storageProviderModel,
CustomUser user) {
super(session, realm, storageProviderModel);
this.user = user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Stream<String> getAttributeStream(String name) {
Map<String, List<String>> attributes = getAttributes();
return (attributes.containsKey(name)) ? attributes.get(name).stream() : Stream.empty();
}
@Override
protected Set<GroupModel> getGroupsInternal() {
if (user.getGroups() != null) {
return user.getGroups().stream().map(UserGroupModel::new).collect(Collectors.toSet());
}
return new HashSet<>();
}
@Override
protected Set<RoleModel> getRoleMappingsInternal() {
if (user.getRoles() != null) {
return user.getRoles().stream().map(roleName -> new UserRoleModel(roleName, realm)).collect(Collectors.toSet());
}
return new HashSet<>();
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
@Override
public String getId() {
return StorageId.keycloakId(storageProviderModel, user.getUserId() + "");
}
@Override
public String getFirstAttribute(String name) {
List<String> list = getAttributes().getOrDefault(name, Collections.emptyList());
return list.isEmpty() ? null : list.get(0);
}
@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
attributes.add(UserModel.USERNAME, getUsername());
attributes.add(UserModel.EMAIL, getEmail());
attributes.add(UserModel.FIRST_NAME, getFirstName());
attributes.add(UserModel.LAST_NAME, getLastName());
attributes.addAll(user.getAttributes());
return attributes;
}
@Override
public String getFirstName() {
return user.getFirstName();
}
@Override
public String getLastName() {
return user.getLastName();
}
@Override
public String getEmail() {
return user.getEmail();
}
}
原因是在你的
UserAdminAdapter
类中,你没有实现removeRequiredAction
和addRequiredAction
方法。您收到的消息来自基类提供的默认实现。您应该自己实现这些方法并将所需的操作存储在底层存储中,或者考虑从 AbstractUserAdapterFederatedStorage
扩展您的类,而不是将所有此类功能委托给内部 Keycloak 实现。
好吧,一个多星期后,我终于在 Keycloak 18.0 上使用了它。您需要做什么?简单地说,您必须实现身份验证工作流程中的每一步:
在我看来,这有点烦人,因为我们必须进行很多循环才能在本地存储数据以及如何处理集成的 OTP 表单(为了“自然的外观”),但它给出了我完全控制我的 OTP 集成,而且,我可以备份我的数据库,并且它们的 OTP 身份验证仍然存在,因此,如果我在 KC 升级中失败或它被损坏,我仍然拥有所有数据。
我可以提供一个更简单的解决方案,该解决方案目前正在生产中。我所做的唯一改变是实施
UserStorageProvider
。因此,我扩展了以下方法:supportsCredentialType
、isConfiguredFor
和isValid
。我的 UserAdapter
使用 UserCredentialManager
作为 credentialManager
方法。
Keycloak 从版本 23 开始运行,我注意到浏览器身份验证流程支持条件 OTP 流程。
以下是我的实现中提到的方法:
@Override
public boolean supportsCredentialType(String credentialType) {
return PasswordCredentialModel.TYPE.equals(credentialType) || OTPCredentialModel.TYPE.equals(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
return switch (credentialType) {
case PasswordCredentialModel.TYPE -> true;
case OTPCredentialModel.TYPE -> client.isConfiguredFor(user.getEmail(), credentialType);
default -> false;
};
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
if (!this.supportsCredentialType(credentialInput.getType()) || !(credentialInput instanceof UserCredentialModel)) {
return false;
}
return client.validate(user.getEmail(), credentialInput.getChallengeResponse(), credentialInput.getType());
}
这意味着不需要自定义凭据更新 SPI、不需要所需操作 SPI、不需要身份验证器 SPI,也不需要自定义身份验证流程。
完整的实现可以在这里找到:https://github.com/systemli/userli-keycloak-provider