All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.client.util.SPNEGOAuthentication Maven / Gradle / Ivy

There is a newer version: 7.4.3.112-ga112
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.client.util;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;

/**
 * 

Implementation of the SPNEGO (or "Negotiate") authentication defined in RFC 4559.

*

A {@link #getUserName() user} is logged in via JAAS (either via userName/password or * via userName/keyTab) once only.

*

For every request that needs authentication, a {@link GSSContext} is initiated and * later established after reading the response from the server.

*

Applications should create objects of this class and add them to the * {@link AuthenticationStore} retrieved from the {@link HttpClient} * via {@link HttpClient#getAuthenticationStore()}.

*/ public class SPNEGOAuthentication extends AbstractAuthentication { private static final Logger LOG = Log.getLogger(SPNEGOAuthentication.class); private static final String NEGOTIATE = HttpHeader.NEGOTIATE.asString(); private final GSSManager gssManager = GSSManager.getInstance(); private String userName; private String userPassword; private Path userKeyTabPath; private String serviceName; private boolean useTicketCache; private Path ticketCachePath; private boolean renewTGT; public SPNEGOAuthentication(URI uri) { super(uri, ANY_REALM); } @Override public String getType() { return NEGOTIATE; } /** * @return the user name of the user to login */ public String getUserName() { return userName; } /** * @param userName user name of the user to login */ public void setUserName(String userName) { this.userName = userName; } /** * @return the password of the user to login */ public String getUserPassword() { return userPassword; } /** * @param userPassword the password of the user to login * @see #setUserKeyTabPath(Path) */ public void setUserPassword(String userPassword) { this.userPassword = userPassword; } /** * @return the path of the keyTab file with the user credentials */ public Path getUserKeyTabPath() { return userKeyTabPath; } /** * @param userKeyTabPath the path of the keyTab file with the user credentials * @see #setUserPassword(String) */ public void setUserKeyTabPath(Path userKeyTabPath) { this.userKeyTabPath = userKeyTabPath; } /** * @return the name of the service to use */ public String getServiceName() { return serviceName; } /** * @param serviceName the name of the service to use */ public void setServiceName(String serviceName) { this.serviceName = serviceName; } /** * @return whether to use the ticket cache during login */ public boolean isUseTicketCache() { return useTicketCache; } /** * @param useTicketCache whether to use the ticket cache during login * @see #setTicketCachePath(Path) */ public void setUseTicketCache(boolean useTicketCache) { this.useTicketCache = useTicketCache; } /** * @return the path of the ticket cache file */ public Path getTicketCachePath() { return ticketCachePath; } /** * @param ticketCachePath the path of the ticket cache file * @see #setUseTicketCache(boolean) */ public void setTicketCachePath(Path ticketCachePath) { this.ticketCachePath = ticketCachePath; } /** * @return whether to renew the ticket granting ticket */ public boolean isRenewTGT() { return renewTGT; } /** * @param renewTGT whether to renew the ticket granting ticket */ public void setRenewTGT(boolean renewTGT) { this.renewTGT = renewTGT; } @Override public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) { SPNEGOContext spnegoContext = (SPNEGOContext)context.getAttribute(SPNEGOContext.ATTRIBUTE); if (LOG.isDebugEnabled()) LOG.debug("Authenticate with context {}", spnegoContext); if (spnegoContext == null) { spnegoContext = login(); context.setAttribute(SPNEGOContext.ATTRIBUTE, spnegoContext); } String b64Input = headerInfo.getBase64(); byte[] input = b64Input == null ? new byte[0] : Base64.getDecoder().decode(b64Input); byte[] output = Subject.doAs(spnegoContext.subject, initGSSContext(spnegoContext, request.getHost(), input)); String b64Output = output == null ? null : new String(Base64.getEncoder().encode(output)); // The result cannot be used for subsequent requests, // so it always has a null URI to avoid being cached. return new SPNEGOResult(null, b64Output); } private SPNEGOContext login() { try { // First login via JAAS using the Kerberos AS_REQ call, with a client user. // This will populate the Subject with the client user principal and the TGT. String user = getUserName(); if (LOG.isDebugEnabled()) LOG.debug("Logging in user {}", user); CallbackHandler callbackHandler = new PasswordCallbackHandler(); LoginContext loginContext = new LoginContext("", null, callbackHandler, new SPNEGOConfiguration()); loginContext.login(); Subject subject = loginContext.getSubject(); SPNEGOContext spnegoContext = new SPNEGOContext(); spnegoContext.subject = subject; if (LOG.isDebugEnabled()) LOG.debug("Initialized {}", spnegoContext); return spnegoContext; } catch (LoginException x) { throw new RuntimeException(x); } } private PrivilegedAction initGSSContext(SPNEGOContext spnegoContext, String host, byte[] bytes) { return () -> { try { // The call to initSecContext with the service name will // trigger the Kerberos TGS_REQ call, asking for the SGT, // which will be added to the Subject credentials because // initSecContext() is called from within Subject.doAs(). GSSContext gssContext = spnegoContext.gssContext; if (gssContext == null) { String principal = getServiceName() + "@" + host; GSSName serviceName = gssManager.createName(principal, GSSName.NT_HOSTBASED_SERVICE); Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); gssContext = gssManager.createContext(serviceName, spnegoOid, null, GSSContext.INDEFINITE_LIFETIME); spnegoContext.gssContext = gssContext; gssContext.requestMutualAuth(true); } byte[] result = gssContext.initSecContext(bytes, 0, bytes.length); if (LOG.isDebugEnabled()) LOG.debug("{} {}", gssContext.isEstablished() ? "Initialized" : "Initializing", gssContext); return result; } catch (GSSException x) { throw new RuntimeException(x); } }; } public static class SPNEGOResult implements Result { private final URI uri; private final HttpHeader header; private final String value; public SPNEGOResult(URI uri, String token) { this(uri, HttpHeader.AUTHORIZATION, token); } public SPNEGOResult(URI uri, HttpHeader header, String token) { this.uri = uri; this.header = header; this.value = NEGOTIATE + (token == null ? "" : " " + token); } @Override public URI getURI() { return uri; } @Override public void apply(Request request) { request.header(header, value); } } private static class SPNEGOContext { private static final String ATTRIBUTE = SPNEGOContext.class.getName(); private Subject subject; private GSSContext gssContext; @Override public String toString() { return String.format("%s@%x[context=%s]", getClass().getSimpleName(), hashCode(), gssContext); } } private class PasswordCallbackHandler implements CallbackHandler { @Override public void handle(Callback[] callbacks) throws IOException { PasswordCallback callback = Arrays.stream(callbacks) .filter(PasswordCallback.class::isInstance) .map(PasswordCallback.class::cast) .findAny() .filter(c -> c.getPrompt().contains(getUserName())) .orElseThrow(IOException::new); callback.setPassword(getUserPassword().toCharArray()); } } private class SPNEGOConfiguration extends Configuration { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { Map options = new HashMap<>(); if (LOG.isDebugEnabled()) options.put("debug", "true"); options.put("refreshKrb5Config", "true"); options.put("principal", getUserName()); options.put("isInitiator", "true"); Path keyTabPath = getUserKeyTabPath(); if (keyTabPath != null) { options.put("doNotPrompt", "true"); options.put("useKeyTab", "true"); options.put("keyTab", keyTabPath.toAbsolutePath().toString()); options.put("storeKey", "true"); } boolean useTicketCache = isUseTicketCache(); if (useTicketCache) { options.put("useTicketCache", "true"); Path ticketCachePath = getTicketCachePath(); if (ticketCachePath != null) options.put("ticketCache", ticketCachePath.toAbsolutePath().toString()); options.put("renewTGT", String.valueOf(isRenewTGT())); } String moduleClass = "com.sun.security.auth.module.Krb5LoginModule"; AppConfigurationEntry config = new AppConfigurationEntry(moduleClass, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); return new AppConfigurationEntry[]{config}; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy