1

We are using KeyCloak for Identity and Access Management for our website. I have also implemented the UserStorageProvider interface for authenticating the user from an external user provider. Now, I am trying to implement the UserRegistrationProvider interface to register a user with that external user provider.

I have implemented the interface and the addUser method which calls the external provider's API via a RestTemplate.

When registering a new user, my custom implementation of addUser is called and the rest API of the external provider is also called returning a message

"Your username is pending confirmation. An email will be sent to confirm your registration"

After that, the user receives an email to confirm the registration.

But, the problem I am facing is that after addUser method is called, KeyCloak calls the getUserByUsername method to log in, but the user is not yet registered because email verification is pending. So, it throws an error.

Ideally, the flow should be that after calling addUser method, KeyCloak should not call the getUserByUsername method and redirect to a custom page that shows the message received from the provider on the screen.

Below is the implementation of addUser method :

    @Override
    public UserModel addUser(RealmModel realmModel, String s) {

        //Getting additional attributes from the form
        MultivaluedMap<String, String> attributes = session.getContext().getContextObject(HttpRequest.class)
                .getDecodedFormParameters();

        //Some code to map the attributes to the POJO
        .......
        //
        Usuario usuario = new Usuario(datosContacto,datosPersonales,datosCredenciales);

        //Calling repository method which then calls the RestTemplate
        // ABCUserAdaptor is a custom class which extends AbstractUserAdapterFederatedStorage
        // And ABCUser is a custom POJO

        ABCUser user = repository.saveUser(usuario);
        ABCUserAdapter abcUserAdapter = new ABCUserAdapter(session, realmModel, model, user);
        abcUserAdapter.setEnabled(false);
        return abcUserAdapter;
    }

I tried returning null from addUser method expecting to get the desired flow. But, it resulted in KeyCloak saving the user in its database and logging in with the credentials.

Thank you in advance for replying, if there is anything else which should be included for the reference please tell me, I will add it.

1 Answers1

1

So, after researching for some days, I found a solution to my problem.

After referring to the KeyCloak's Source Code, I found out that .ftl files are rendered by a Provider Interface in my case LoginFormsProvider interface. FreeMarkerLoginFormsProvider is the class that implements the interface and has a method defined as createForm. This method creates a form from the FTL file and returns it as a Response object.

Now, addUser returns a UserModel object and the flow continues to go to the getUserByUsername method. To stop this flow I threw an AuthenticationFlowException with the form in Response.

Here's the logic:

@Override
public UserModel addUser(RealmModel realmModel, String s) {

    // Getting additional attributes from the form
    MultivaluedMap<String, String> attributes = session.getContext().getContextObject(HttpRequest.class)
            .getDecodedFormParameters();

    // Some code to map the attributes to the POJO
    .......
    //
    Usuario usuario = new Usuario(datosContacto,datosPersonales,datosCredenciales);

    // Calling repository method which then calls the RestTemplate
    // saveUser now returns a string containing the response message from the rest client

    String response = repository.saveUser(usuario);

    // An object of FreeMarkerUtil class to pass as an argument 
    // to FreeMarkerLoginFormsProvider's constructor
    FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil();
    // Initializing FreeMarkerLoginFormsProvider's object
    FreeMarkerLoginFormsProvider freeMarkerLoginFormsProvider = new FreeMarkerLoginFormsProvider(session,freeMarkerUtil);
    // Name of the FTL file to render custom page
    String templateName = "login-verify-email.ftl";

    // Setting form attribute - response from the rest client
    // To show on the page
    LoginFormsProvider formsProvider = freeMarkerLoginFormsProvider.setAttribute("messageResponse", response);

    // Throw AuthenticationFlowException to stop the flow and redirect to a custom page 
    throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER, formsProvider.createForm(templateName));
}

This is a solution that I can find for now. It may be wrong or there can be a much more efficient way to achieve this. Let me know if there are any.

  • Sounds interesting, I'm having similar requirements. Could you tell me: did you call your external API from within your overridden `addUser` method? If so, you might have sent an incomplete user object (only with username and maybe generated id), right? Since other attributes only get attached later to the `UserModel` object returned by `addUser` method? – developer10 Apr 23 '23 at 12:31
  • @developer10 Yes I'm calling the external API from the `addUser` method. I am sending my custom POJO which has the fields set from `attributes`. **"A Complete User Object"** is a gray area for me. Could you elaborate? – Awais Memon Apr 24 '23 at 13:23
  • By "A Complete User Object" I meant that - your POJO. However, I don't understand how your POJO could have all attributes in `addUser` method if only `username` is available there (as a param)? You mentioned `attributes` - how are you accessing those, through the `session`, `realm` or some other object? – developer10 Apr 25 '23 at 15:11
  • @developer10 I am getting the `attributes` from this line of code: `MultivaluedMap attributes =session.getContext().getContextObject(HttpRequest.class).getDecodedFormParameters(); ` . So yes, I am accessing it from `session`. – Awais Memon Apr 26 '23 at 14:21
  • In the FTL file added this in the input tag: `value="${(register.formData.contactNo!'')}"`. And to get the value `attributes.getFirst("contactNo")`. – Awais Memon Apr 26 '23 at 14:29
  • You must then be using some older version (I'm using the latest 21.1.0) because there is no `.getDecodedFormParameters()` method on `session.getContext().getContextObject(HttpRequest.class)` in this version. Thus, I'm still unable to retrieve all the attrs from `addUser` method and make my REST call from there – developer10 Apr 26 '23 at 15:05
  • Yes, we are using v16. – Awais Memon Apr 27 '23 at 16:51
  • @developer10 You can try using this : `context.getHttpRequest().getDecodedFormParameters();`. I found this in the [documentation](https://www.keycloak.org/docs/latest/server_development/#implementing-an-authenticator) , in the code snippet of `validateAnswer()` method. – Awais Memon Apr 28 '23 at 13:11