只读联合用户的Keycloak OTP

Keycloak OTP for read only federated users

我已经实现了一个自定义用户存储提供程序,用于从我们的数据库中联合用户。

我想通过 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 org.keycloak.keycloak-server-spi@15.1.1//org.keycloak.storage.adapter.AbstractUserAdapter.removeRequiredAction(AbstractUserAdapter.java:77)
at org.keycloak.keycloak-services@15.1.1//org.keycloak.services.resources.LoginActionsService.processRequireAction(LoginActionsService.java:1044)
at org.keycloak.keycloak-services@15.1.1//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();
  }
}

原因是你的UserAdminAdapterclass中没有实现removeRequiredActionaddRequiredAction方法。您收到的消息来自基础 class 提供的默认实现。您应该自己实现这些方法并将所需的操作存储在底层存储中,或者考虑从 AbstractUserAdapterFederatedStorage 扩展您的 class 而不是将所有此类功能委托给内部 Keycloak 实现。

我的外部数据库支持完整的 OTP

好吧,一个多星期后,我终于在 Keycloak 18.0 上使用了它。您需要做什么?简单来说,您必须执行身份验证工作流程中的每一步:

  1. 创建您的用户存储 SPI
  2. 实施凭据更新 SPI
  3. 实施自定义凭据提供程序 SPI
  4. 实施自定义所需操作 SPI
  5. 实施您的身份验证器 SPI
  6. 实现你的表格(我有点用了 KC 中的内部 OTP 表格)
  7. 启用您需要的操作
  8. 创建浏览器工作流程的副本并在其中粘贴您的身份验证器

我们从中得到了什么?

  1. 我们获得了一个完全可定制的 OTP 验证器(领域的政策待定...)
  2. 您可以在您的应用中使用该代码进行验证(它在您的数据库中)
  3. 您可以在您的应用中为用户设置 OTP 身份验证(不涉及 KC 管理页面,因此,您可以将管理页面留在防火墙之外)

在我看来,这有点烦人,因为我们必须做很多循环才能在本地存储我们的数据以及如何处理集成的 OTP 表单(为了“自然的外观”) ,但它让我可以完全控制我的 OTP 集成,而且,我可以备份我的数据库并且他们的 OTP 身份验证仍然存在,所以,如果我在 KC 升级中失败或它被损坏,我仍然拥有所有这些数据。

最后,这是当您的经理拥有自定义 OTP 身份验证时的样子