Reputation: 4337
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
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
Reputation: 121
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:
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 authentication
Upvotes: 2
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