Java SPNEGO 身份验证和 Kerberos 约束委派 (KCD) 到后端服务

Java SPNEGO Authentication & Kerberos Constrained Delegation (KCD) to backend service

我有一个 Java Web 应用程序,它在 Windows Active Directory 环境中对客户端进行 SPNEGO 身份验证。 为了对用户进行身份验证,我们使用来自旧的 SPNEGO SourceForge 项目的代码。

String encodedAuthToken = (String) credentials;
LOG.debug("Encoded auth token: " + encodedAuthToken);
byte[] authToken = B64Code.decode(encodedAuthToken);
GSSManager manager = GSSManager.getInstance();

try {
    Oid krb5Oid = new Oid("1.3.6.1.5.5.2");
    GSSName gssName = manager.createName(_targetName, null);
    GSSCredential serverCreds = manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, krb5Oid, GSSCredential.INITIATE_AND_ACCEPT);
    GSSContext gContext = manager.createContext(serverCreds);

    if (gContext != null) { 
        while (!gContext.isEstablished()) {
            authToken = gContext.acceptSecContext(authToken, 0, authToken.length);
        }
        if (gContext.isEstablished()) {
            // Login succeeded!
            String clientName = gContext.getSrcName().toString();
        }
    }
}

身份验证工作正常,但我们还需要使用约束委托将用户凭据委托给后端服务 (Exchange EWS)。 在我们的 AD 中配置它时,它看起来像一个小差异,但事实并非如此。看: AD delegation settings

区别在这里描述:msdn.microsoft.com/en-us/library/cc246080.aspx?f=255&MSPPError=-2147217396 通过无约束委托,我们可以在调用后端服务时简单地使用可用的委托凭据,这一切都很好:

GSSCredential delegatedCreds = gContext.getDelegCred()
SpnegoHttpURLConnection conn = new SpnegoHttpURLConnection(clientCreds);

使用受限委派,我们无法访问用户 TGT,看来我们需要使用 Java 8 应该支持的 MS-SFU (S4U2proxy) Kerberos 扩展。 我能找到的唯一例子是这个:https://github.com/ymartin59/java-kerberos-sfudemo(感谢 Yves Martin!)

现在我的问题是...在我进行身份验证后,我基本上得到了经过身份验证的用户的用户名(请参阅上面代码中的 "clientName")。

这里真的需要使用S4U2self机制来冒充用户吗? 客户端刚刚向我们发送了它的 Kerberos 服务票证(包装在我无法解码的 SPNEGO 令牌中)。 理想情况下,我们应该能够使用该服务票证和我自己服务的 TGT 来验证用户(使用 S4U2proxy 机制)? 但我不明白如何。

所以现在我想知道是否可以将我们的 SPNEGO 身份验证与 S4U2proxy 委托结合在一起?

非常感谢您对此的任何意见。

我最近一直在做类似的事情,但我正在使用 spring 安全 kerberos。我在 github here. The key thing that I found that I needed set up to use constrained delegation like you want it and S4U2Proxy was to make sure (if you're using Oracle/OpenJDK) you set isInitiator=true in your JAAS Config so that when getDelegCred is called you get back a Krb5ProxyCredential. See comment here. With that credential, you can use it to create service ticket tokens on the Users behalf for the services you are constrained to use in the normal fashion, like this.

上举了一个例子

我对 Kerberos 约束委派进行了大量调查,最后我找到了使用 Java 的正确方法。

域控制器上的设置

1) 不委托:不要信任此帐户进行委托

您(服务用户)无法获得用户的委托凭据。这意味着您不能代表最终用户执行任何任务。 您最多可以做的是接受来自用户(通常是浏览器)的传入票证,并通过将其传递给 KDC 来对其进行验证。作为回应,KDC 会告诉您这张票是发给哪个用户(或委托人)的,但不会传递任何凭据。

2) Unconstrained Delegation:信任此帐户以委派任何服务(仅限 Kerberos)

使用此选项,您(服务用户)可以获得用户的委托凭证。而且,你得到的是用户的TGT。使用此 TGT,您可以代表用户为 任何 服务请求 TGS(服务票证)。

3) 信任此帐户以委派指定服务(仅限 Kerberos)

在这里,您指定可以使用委托凭据的服务。这意味着当启用此选项时,您将获得委托的凭据,但是,您只能使用它们来获取指定服务的最终用户的 TGS。

另一个重点是,您必须拥有最终用户的 TGS(您的网络应用程序的最终用户 TGS)。然后使用此 TGS,您可以向 KDC 请求最终用户的 TGS 以获得另一项服务。

4) 信任此帐户以委派指定服务(任何协议)

这也称为协议转换。在此选项中,您还需要指定可以代表用户向 KDC 请求 TGS 的服务。

您(服务用户)可以 'impersonate' 最终用户,无需最终用户提供任何类型的票证。 您可以模拟任何用户,获取指定服务的TGS。 此选项对于无法进行最终用户交互的背景流程或计划很有用。

Java 代码示例

1) 获取委托凭证(在上述选项 2 和 3 中很有用)

        // ---------------------------------
        // step 1: Login using service user credentials and get its TGT
        // ---------------------------------

        Subject subject = new Subject();
        Krb5LoginModule krb5LoginModule = new Krb5LoginModule();
        Map<String,String> optionMap = new HashMap<String,String>();

        optionMap.put("keyTab", "c:\ticket\sapuser.keytab");
        optionMap.put("principal", "HTTP/TEST"); // SPN you mapped to the service user while creating the keytab file
        optionMap.put("doNotPrompt", "true");
        optionMap.put("refreshKrb5Config", "true");
        optionMap.put("useTicketCache", "true");
        optionMap.put("renewTGT", "true");
        optionMap.put("useKeyTab", "true");
        optionMap.put("storeKey", "true");
        optionMap.put("isInitiator", "true"); // needed for delegation
        optionMap.put("debug", "true"); // trace will be printed on console

        krb5LoginModule.initialize(subject, null, new HashMap<String,String>(), optionMap);

        krb5LoginModule.login();
        krb5LoginModule.commit();


      // ---------------------------------
      // Step 2: Use login context of this service user, accept the kerberos token (TGS) coming from end user
      // ---------------------------------

public GSSCredential validateTicket(byte[] token) { 
    try {
        return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
    }
    catch (PrivilegedActionException e) {
        throw new BadCredentialsException("Kerberos validation not successful", e);
    }
}


private class KerberosValidateAction implements PrivilegedExceptionAction<GSSCredential> {
    byte[] kerberosTicket;

    public KerberosValidateAction(byte[] kerberosTicket) {
        this.kerberosTicket = kerberosTicket;
    }

    @Override
    public GSSCredential run() throws Exception {
        byte[] responseToken = new byte[0];
        GSSName gssName = null;
        GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);

        while (!context.isEstablished()) {
            responseToken = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
            gssName = context.getSrcName();
            if (gssName == null) {
                throw new BadCredentialsException("GSSContext name of the context initiator is null");
            }
        }

        //check if the credentials can be delegated
        if (!context.getCredDelegState()) {
            SecurityLogger.getLogger().error("Credentials can not be delegated. Please make sure that delegation is enabled for the service user. This may cause failures while creating Kerberized application.");
            return null;
        }

        // only accepts the delegated credentials from the calling peer
        GSSCredential clientCred = context.getDelegCred(); // in case of Unconstrained Delegation, you get the end user's TGT, otherwise TGS only
        return clientCred;
    }
}

    // ---------------------------------
    // Step 3: Initiate TGS request for another service using delegated credentials obtained in previous step
    // ---------------------------------
    private Object getServiceTicket(GSSCredential clientCred) throws PrivilegedActionException {
    Object o = Subject.doAs(new Subject(), (PrivilegedExceptionAction<Object>) () -> {

        GSSManager manager = GSSManager.getInstance();
        Oid SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
        Oid KRB5_PRINCIPAL_OID = new Oid("1.2.840.113554.1.2.2.1");
        GSSName servicePrincipal = manager.createName("HTTP/TEST", KRB5_PRINCIPAL_OID); // service to which the service user is allowed to delegate credentials
        ExtendedGSSContext extendedContext = (ExtendedGSSContext) manager.createContext(servicePrincipal, SPNEGO_OID, clientCred, GSSContext.DEFAULT_LIFETIME);
        extendedContext.requestCredDeleg(true);

        byte[] token = new byte[0];
        token = extendedContext.initSecContext(token, 0, token.length); // this token is the end user's TGS for "HTTP/TEST" service, you can pass this to the actual HTTP/TEST service endpoint in "Authorization" header.

        return token;
    });
    return o;
}

2) 获取模拟凭据(在上述选项 4 中很有用)

初始步骤与上面第 1 步中提到的类似。您需要使用服务用户凭据登录。 'run' 方法略有改动,如下所示:

            @Override
            public GSSCredential run() throws Exception {
                GSSName gssName = null;
                GSSManager manager = GSSManager.getInstance();
                GSSCredential serviceCredentials = manager.createCredential(GSSCredential.INITIATE_ONLY);
                GSSName other = manager.createName("bhushan", GSSName.NT_USER_NAME, kerberosOid); // any existing user
                GSSCredential impersonatedCredentials = ((ExtendedGSSCredential) serviceCredentials).impersonate(other);
                return impersonatedCredentials;
            }
        }

你可以看到在这种情况下我们不需要用户的TGS。
代表用户获取TGS用于其他服务,与上面代码中给出的步骤3中提到的相同。只需传递这些 impersonatedCredentials 而不是 delegatedCredentials。

希望对您有所帮助。

谢谢,
布山