Scott
Scott

Reputation: 1922

Keycloak Inputless Authorisation

I'm trying to write a custom Keycloak Authenticator that can retrieve user credentials from some request and dynamically submit these for authentication without the end user having to manually enter them into some login form.

Using this question as a starting point, I have created my own custom Authentication SPI in Keycloak. I have also configured the Keycloak Authentication Flow as necessary.

Custom Authenticator

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;

public class CustomUPForm extends UsernamePasswordForm implements Authenticator {

  @Override
  public void authenticate(AuthenticationFlowContext context) {
    System.out.println("Authenticating....");
    
    Response challenge = context.form().createForm("custom-up-form.ftl");
    context.challenge(challenge);
    
    MultivaluedMap<String, String> formData = new MultivaluedHashMap<>();
    //Changed here - but otherwise valid credentials
    formData.putSingle("username", "xxxxx");
    formData.putSingle("password", "xxxxx");
    context.form().setFormData(formData);
  }

  @Override
  public void action(AuthenticationFlowContext context) {
    System.out.println("Action....");
    context.success();
  }

}

Custom Authenticator Factory

import java.util.ArrayList;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

public class CustomUPFormFactory implements AuthenticatorFactory,
    ConfigurableAuthenticatorFactory {

  public static final String PROVIDER_ID = "custom-up-form";
  public static final CustomUPForm SINGLETON = new CustomUPForm();

  @Override
  public Authenticator create(KeycloakSession session) {
    return SINGLETON;
  }

  @Override
  public void init(Config.Scope config) {

  }

  @Override
  public void postInit(KeycloakSessionFactory factory) {

  }

  @Override
  public void close() {

  }

  @Override
  public String getId() {
    return PROVIDER_ID;
  }

  @Override
  public String getDisplayType() {
    return "Custom Authenticator";
  }

  @Override
  public String getReferenceCategory() {
    return "Reference Category";
  }

  @Override
  public boolean isConfigurable() {
    return true;
  }

  public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
      AuthenticationExecutionModel.Requirement.REQUIRED
  };

  @Override
  public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
    return REQUIREMENT_CHOICES;
  }

  @Override
  public String getHelpText() {
    return "POC Custom Authenticator";
  }

  private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();

  static {
    /*
    Add properties here
    */
  }

  @Override
  public List<ProviderConfigProperty> getConfigProperties() {
    return CONFIG_PROPERTIES;
  }

  @Override
  public boolean isUserSetupAllowed() {
    return false;
  }

}

And below is my custom login form, which based is off the form provided in the Keycloak "secret question" SPI example here

<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
    <#if section = "title">
        ${msg("loginTitle",realm.name)}
    <#elseif section = "header">
        ${msg("loginTitleHtml",realm.name)}
    <#elseif section = "form">
        <form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
            <div class="${properties.kcFormGroupClass!}">
                <div class="${properties.kcLabelWrapperClass!}">
                    <label for="totp" class="${properties.kcLabelClass!}">Login</label>
                </div>

                <div class="${properties.kcInputWrapperClass!}">
                    <input id="totp" name="username" type="text" class="${properties.kcInputClass!}" />
                    <input id="totp" name="password" type="password" class="${properties.kcInputClass!}" />
                </div>
            </div>

            <div class="${properties.kcFormGroupClass!}">
                <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
                    <div class="${properties.kcFormOptionsWrapperClass!}">
                    </div>
                </div>

                <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
                    <div class="${properties.kcFormButtonsWrapperClass!}">
                        <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
                        name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
                    </div>
                </div>
            </div>
        </form>
    </#if>
</@layout.registrationLayout>

All components are rendering fine, but I keep getting "invalid username/password" responses when dynamically trying to pass my credentials by adding them to the form data.

How can I pass a username and password combination without having a user manually enter them?

Upvotes: 1

Views: 1226

Answers (1)

Scott
Scott

Reputation: 1922

The problem was that I was attempting to perform this within the scope of authenticate(), when it should included in the logic for action().

Additionally the MultivaluedMap containing the form data should be derived from the HTTP request contained within the current Context

The following solution is derived from this question

public class CustomUPForm extends UsernamePasswordForm implements Authenticator {

  @Override
  public void authenticate(AuthenticationFlowContext context) {
    Response challenge = context.form()
        .createForm("custom-up-form.ftl");
    context.challenge(challenge);
  }

  @Override
  public void action(AuthenticationFlowContext context) {
    System.out.println("Processing form...");

    MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();

    formData.putSingle("username", "xxxxx");
    formData.putSingle("password", "xxxxx");

    if (!validateForm(context, formData)) {
      return;
    }

    context.success();
  }
}

Note that in this example validateForm() contains some custom validation logic that is not necessary for the scope of this question. However the values for username and password can be extracted through calling getFirst()

formData.getFirst("username");

Upvotes: 0

Related Questions