simonC
simonC

Reputation: 4337

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

Upvotes: 2

Views: 5019

Answers (3)

0x46616c6b
0x46616c6b

Reputation: 1475

I can offer a much simpler solution, which is currently in production. The only changes I have made are in my implementation of the UserStorageProvider. Therefore, I extend the following methods: supportsCredentialType, isConfiguredFor, and isValid. My UserAdapter uses the UserCredentialManager for the credentialManager method.

Keycloak has been running since version 23, and I noticed that the browser authentication flow supports the conditional OTP flow.

Here are the mentioned methods from my implementation:

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

This means no custom Credential Update SPI, no Required Action SPI, no Authenticator SPI, nor custom Authentication flow is needed.

The complete implementation can be found here: https://github.com/systemli/userli-keycloak-provider

Upvotes: 1

RedArcCoder
RedArcCoder

Reputation: 121

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

Upvotes: 2

zaerymoghaddam
zaerymoghaddam

Reputation: 3127

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.

Upvotes: 5

Related Questions