Search code examples
keycloakfederationtotp

Keycloak OTP for read only federated users


I have implemented a custom user storage provider for federating users from our database.

I want to manage OTP for those users via keycloak, when I set the OTP to required in the flow and Configure OTP as required action the otp form is shown after federated user login, but when I try to setup the OTP I receive the error user is read only for this update.

How can I allow read only federated users to allow OTP configuration via keycloak?

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)

the user adapter is

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();
  }
}

Solution

  • The reason is that in your UserAdminAdapter class, you have not implemented the removeRequiredAction and addRequiredAction methods. The message you're receiving is from the default implementation provided by the base class. You should either implement these methods yourself and store the required actions in your underlying storage, OR consider extending your class from AbstractUserAdapterFederatedStorage instead which delegates all such functionalities to the internal Keycloak implementation.