1

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 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)

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();
  }
}
zaerymoghaddam
  • 3,037
  • 1
  • 27
  • 33
simonC
  • 4,101
  • 10
  • 50
  • 78

2 Answers2

4

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.

zaerymoghaddam
  • 3,037
  • 1
  • 27
  • 33
  • 1
    Thank you, this fixed a lot of errors I had with my custom library. If you don't mind asking you a question, do you know of a way to manage OTP data using my custom UserStorage? I already implemented the interface CredentialInputUpdater (with @override updateCredential), but, when I configure my OTP client it isn't called, instead, keycloak manages that operation. And again, thank you for this, kinda fixed 80% of my problems. – RedArcCoder May 31 '22 at 14:26
  • 1
    It's actually difficult to tell without knowing how the code is implemented. Most of the time there is an 'Aha' moment when you see the code and it really helps to see what's the underlying problem. In our scenario, we decided to use KC provided storage for everything related to OTP so that it would be easier for us in future to utilize other KC features and new OTP types. We didn't implement `CredentialInputUpdater`, so unfortunately I don't have any guess about what could be the underlying reason :( – zaerymoghaddam Jun 01 '22 at 10:43
  • It kinda makes sense to leave keycloak to handle OTP authentication and just deal with password authentication, my only problem with that is that I wanna enable user to setup the OTP authentication if they wish to. So, what did I do? I implemented a custom OTP library, created a credentialProvider and a authenticator, that way, I can leave all my OTP data in my MySQL database (and with full control for my end-users and .Net API). Anyway, thank you so much, this comment literally saved me days of work. – RedArcCoder Jun 01 '22 at 15:10
2

FULL OTP support in my external DB

Well, finally after more than a week I got this working with Keycloak 18.0. What do you need to do?, simply, you have to implement each and every step in the authentication workflow:

  1. Create your user storage SPI
  2. Implement Credential Update SPI
  3. Implement a custom Credential Provider SPI
  4. Implement a custom Required Action SPI
  5. Implement your authenticator SPI
  6. Implement your forms (I kinda used the internal OTP forms in KC)
  7. Enable your Required action
  8. Create a copy of the browser workflow and plaster there your authenticator

And what do we get with this?

  1. We get a fully customizable OTP authenticator (realm's policy pending...)
  2. You can use that code for verification in your app (it's in your db)
  3. You can setup users for OTP authentication in your app (no KC admin page involved, so, you can leave the admin page outside the firewall)

In my opinion, this is kinda annoying, since there are a lot of loops we have to make to be able to store our data locally and how to deal with the integrated OTP forms (for a "natural look"), but it gives me full control over my OTP integration, also, I can backup my database and their OTP authentication is still there, so, if I have a failure in a KC upgrade or it gets corrupted, I still have all that data.

Lastly, heres what it should look like when your manager has the custom OTP authenticationenter image description here

RedArcCoder
  • 101
  • 8