首页 > 解决方案 > Keycloak 无输入授权

问题描述

我正在尝试编写一个自定义 Keycloak Authenticator,它可以从某个请求中检索用户凭据并动态提交这些凭据以进行身份​​验证,而最终用户不必手动将它们输入到某个登录表单中。

这个问题为起点,我在 Keycloak 中创建了我自己的自定义身份验证 SPI。我还根据需要配置了 Keycloak 身份验证流程。

自定义身份验证器

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

}

自定义验证器工厂

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

}

下面是我的自定义登录表单,它基于 Keycloak“秘密问题”SPI 示例中提供的表单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>

所有组件都呈现正常,但是当我通过将它们添加到表单数据来动态尝试传递我的凭据时,我不断收到“无效的用户名/密码”响应。

如何在不让用户手动输入的情况下传递用户名和密码组合?

标签: javakeycloak

解决方案


问题是我试图在 的范围内执行此操作authenticate(),而它应该包含在action().

此外,MultivaluedMap包含表单数据的内容应该来自当前包含的 HTTP 请求Context

以下解决方案来自这个问题

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

请注意,在此示例validateForm()中包含一些自定义验证逻辑,这对于此问题的范围不是必需的。但是用户名和密码的值可以通过调用来提取getFirst()

formData.getFirst("username");

推荐阅读