2013-02-11 07:56:35 -05:00
|
|
|
/*
|
|
|
|
* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
|
|
|
|
* Copyright (C) 2012 Mickael Guessant
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
package davmail.http;
|
|
|
|
|
2013-02-13 04:15:47 -05:00
|
|
|
import davmail.Settings;
|
2013-03-28 19:06:59 -04:00
|
|
|
import davmail.ui.CredentialPromptDialog;
|
2013-02-11 07:56:35 -05:00
|
|
|
import org.apache.log4j.Logger;
|
|
|
|
import org.ietf.jgss.*;
|
|
|
|
|
2013-03-25 07:40:07 -04:00
|
|
|
import javax.security.auth.RefreshFailedException;
|
2013-02-11 07:56:35 -05:00
|
|
|
import javax.security.auth.Subject;
|
|
|
|
import javax.security.auth.callback.*;
|
2013-03-25 07:40:07 -04:00
|
|
|
import javax.security.auth.kerberos.KerberosTicket;
|
2013-02-11 07:56:35 -05:00
|
|
|
import javax.security.auth.login.LoginContext;
|
|
|
|
import javax.security.auth.login.LoginException;
|
2014-03-19 05:29:50 -04:00
|
|
|
import java.awt.*;
|
2013-02-13 04:15:47 -05:00
|
|
|
import java.io.BufferedReader;
|
2013-02-11 07:56:35 -05:00
|
|
|
import java.io.IOException;
|
2013-02-13 04:15:47 -05:00
|
|
|
import java.io.InputStreamReader;
|
2013-02-11 07:56:35 -05:00
|
|
|
import java.security.PrivilegedAction;
|
|
|
|
import java.security.Security;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Kerberos helper class.
|
|
|
|
*/
|
|
|
|
public class KerberosHelper {
|
|
|
|
protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
|
|
|
|
protected static final Object LOCK = new Object();
|
2013-03-20 07:38:31 -04:00
|
|
|
protected static final KerberosCallbackHandler KERBEROS_CALLBACK_HANDLER;
|
2014-03-19 05:29:50 -04:00
|
|
|
private static LoginContext clientLoginContext;
|
2013-02-11 07:56:35 -05:00
|
|
|
|
|
|
|
static {
|
2013-03-20 07:38:31 -04:00
|
|
|
// Load Jaas configuration from class
|
2013-02-11 07:56:35 -05:00
|
|
|
Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
|
2013-03-20 07:38:31 -04:00
|
|
|
// Kerberos callback handler singleton
|
|
|
|
KERBEROS_CALLBACK_HANDLER = new KerberosCallbackHandler();
|
2013-02-11 07:56:35 -05:00
|
|
|
}
|
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
private KerberosHelper() {
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressWarnings("UseOfSystemOutOrSystemErr")
|
2013-02-11 07:56:35 -05:00
|
|
|
protected static class KerberosCallbackHandler implements CallbackHandler {
|
|
|
|
String principal;
|
|
|
|
String password;
|
|
|
|
|
|
|
|
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
2014-03-19 05:29:50 -04:00
|
|
|
for (Callback callback : callbacks) {
|
|
|
|
if (callback instanceof NameCallback) {
|
2013-02-11 07:56:35 -05:00
|
|
|
if (principal == null) {
|
2013-02-13 04:15:47 -05:00
|
|
|
// if we get there kerberos token is missing or invalid
|
|
|
|
if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
|
|
|
|
// headless or server mode
|
2014-03-19 05:29:50 -04:00
|
|
|
System.out.print(((NameCallback) callback).getPrompt());
|
2013-02-13 04:15:47 -05:00
|
|
|
BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
|
|
|
|
principal = inReader.readLine();
|
|
|
|
} else {
|
2014-03-19 05:29:50 -04:00
|
|
|
CredentialPromptDialog credentialPromptDialog = new CredentialPromptDialog(((NameCallback) callback).getPrompt());
|
2013-03-28 19:06:59 -04:00
|
|
|
principal = credentialPromptDialog.getPrincipal();
|
|
|
|
password = String.valueOf(credentialPromptDialog.getPassword());
|
2013-02-13 04:15:47 -05:00
|
|
|
}
|
2013-02-11 07:56:35 -05:00
|
|
|
}
|
2013-03-24 18:54:18 -04:00
|
|
|
if (principal == null) {
|
|
|
|
throw new IOException("KerberosCallbackHandler: failed to retrieve principal");
|
|
|
|
}
|
2014-03-19 05:29:50 -04:00
|
|
|
((NameCallback) callback).setName(principal);
|
2013-03-20 07:38:31 -04:00
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
} else if (callback instanceof PasswordCallback) {
|
2013-02-11 07:56:35 -05:00
|
|
|
if (password == null) {
|
2013-02-13 04:15:47 -05:00
|
|
|
// if we get there kerberos token is missing or invalid
|
|
|
|
if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
|
|
|
|
// headless or server mode
|
2014-03-19 05:29:50 -04:00
|
|
|
System.out.print(((PasswordCallback) callback).getPrompt());
|
2013-02-13 04:15:47 -05:00
|
|
|
BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
|
|
|
|
password = inReader.readLine();
|
|
|
|
}
|
2013-02-11 07:56:35 -05:00
|
|
|
}
|
2013-03-24 18:54:18 -04:00
|
|
|
if (password == null) {
|
|
|
|
throw new IOException("KerberosCallbackHandler: failed to retrieve password");
|
|
|
|
}
|
2014-03-19 05:29:50 -04:00
|
|
|
((PasswordCallback) callback).setPassword(password.toCharArray());
|
2013-03-20 07:38:31 -04:00
|
|
|
|
2013-02-11 07:56:35 -05:00
|
|
|
} else {
|
2014-03-19 05:29:50 -04:00
|
|
|
throw new UnsupportedCallbackException(callback);
|
2013-02-11 07:56:35 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
/**
|
|
|
|
* Force client principal in callback handler
|
|
|
|
*
|
|
|
|
* @param principal client principal
|
|
|
|
*/
|
|
|
|
public static void setClientPrincipal(String principal) {
|
2013-03-24 18:54:18 -04:00
|
|
|
KERBEROS_CALLBACK_HANDLER.principal = principal;
|
|
|
|
}
|
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
/**
|
|
|
|
* Force client password in callback handler
|
|
|
|
*
|
|
|
|
* @param password client password
|
|
|
|
*/
|
|
|
|
public static void setClientPassword(String password) {
|
2013-03-24 18:54:18 -04:00
|
|
|
KERBEROS_CALLBACK_HANDLER.password = password;
|
|
|
|
}
|
|
|
|
|
2013-03-20 07:38:31 -04:00
|
|
|
/**
|
|
|
|
* Get response Kerberos token for host with provided token.
|
|
|
|
*
|
2013-03-24 18:54:18 -04:00
|
|
|
* @param protocol target protocol
|
|
|
|
* @param host target host
|
|
|
|
* @param token input token
|
2013-03-20 07:38:31 -04:00
|
|
|
* @return response token
|
|
|
|
* @throws GSSException on error
|
|
|
|
* @throws LoginException on error
|
|
|
|
*/
|
2013-03-24 18:54:18 -04:00
|
|
|
public static byte[] initSecurityContext(final String protocol, final String host, final byte[] token) throws GSSException, LoginException {
|
|
|
|
return initSecurityContext(protocol, host, null, token);
|
2013-03-20 07:38:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get response Kerberos token for host with provided token, use client provided delegation credentials.
|
|
|
|
* Used to authenticate with target host on a gateway server with client credentials,
|
|
|
|
* gateway must have its own principal authorized for delegation
|
|
|
|
*
|
2013-03-24 18:54:18 -04:00
|
|
|
* @param protocol target protocol
|
2013-03-20 07:38:31 -04:00
|
|
|
* @param host target host
|
|
|
|
* @param delegatedCredentials client delegated credentials
|
|
|
|
* @param token input token
|
|
|
|
* @return response token
|
|
|
|
* @throws GSSException on error
|
|
|
|
* @throws LoginException on error
|
|
|
|
*/
|
2013-03-24 18:54:18 -04:00
|
|
|
public static byte[] initSecurityContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) throws GSSException, LoginException {
|
2014-07-31 17:08:47 -04:00
|
|
|
LOGGER.debug("KerberosHelper.initSecurityContext " + protocol + '@' + host + ' ' + token.length + " bytes token");
|
2013-02-11 07:56:35 -05:00
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
synchronized (LOCK) {
|
2014-03-20 18:14:34 -04:00
|
|
|
// check cached TGT
|
|
|
|
if (clientLoginContext != null) {
|
|
|
|
for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
|
|
|
|
KerberosTicket kerberosTicket = (KerberosTicket) ticket;
|
|
|
|
if (kerberosTicket.getServer().getName().startsWith("krbtgt") && !kerberosTicket.isCurrent()) {
|
|
|
|
LOGGER.debug("KerberosHelper.clientLogin cached TGT expired, try to relogin");
|
|
|
|
clientLoginContext = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-03-19 05:29:50 -04:00
|
|
|
// create client login context
|
2014-03-20 18:14:34 -04:00
|
|
|
if (clientLoginContext == null) {
|
|
|
|
final LoginContext localLoginContext = new LoginContext("spnego-client", KERBEROS_CALLBACK_HANDLER);
|
|
|
|
localLoginContext.login();
|
|
|
|
clientLoginContext = localLoginContext;
|
|
|
|
}
|
|
|
|
// try to renew almost expired tickets
|
|
|
|
for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
|
|
|
|
KerberosTicket kerberosTicket = (KerberosTicket) ticket;
|
|
|
|
LOGGER.debug("KerberosHelper.clientLogin ticket for " + kerberosTicket.getServer().getName() + " expires at " + kerberosTicket.getEndTime());
|
|
|
|
if (kerberosTicket.getEndTime().getTime() < System.currentTimeMillis() + 10000) {
|
|
|
|
if (kerberosTicket.isRenewable()) {
|
|
|
|
try {
|
|
|
|
kerberosTicket.refresh();
|
|
|
|
} catch (RefreshFailedException e) {
|
|
|
|
LOGGER.debug("KerberosHelper.clientLogin failed to renew ticket " + kerberosTicket.toString());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
LOGGER.debug("KerberosHelper.clientLogin ticket is not renewable");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-02-11 07:56:35 -05:00
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
Object result = internalInitSecContext(protocol, host, delegatedCredentials, token);
|
|
|
|
if (result instanceof GSSException) {
|
|
|
|
LOGGER.info("KerberosHelper.initSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
|
|
|
|
throw (GSSException) result;
|
|
|
|
}
|
2013-03-24 18:54:18 -04:00
|
|
|
|
2014-03-19 05:29:50 -04:00
|
|
|
LOGGER.debug("KerberosHelper.initSecurityContext return " + ((byte[]) result).length + " bytes token");
|
|
|
|
return (byte[]) result;
|
|
|
|
}
|
2013-03-24 18:54:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
protected static Object internalInitSecContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) {
|
|
|
|
return Subject.doAs(clientLoginContext.getSubject(), new PrivilegedAction() {
|
2013-02-11 07:56:35 -05:00
|
|
|
|
|
|
|
public Object run() {
|
|
|
|
Object result;
|
2013-03-25 07:40:07 -04:00
|
|
|
GSSContext context = null;
|
2013-02-11 07:56:35 -05:00
|
|
|
try {
|
|
|
|
GSSManager manager = GSSManager.getInstance();
|
2014-07-28 17:50:37 -04:00
|
|
|
GSSName serverName = manager.createName(protocol + '@' + host, GSSName.NT_HOSTBASED_SERVICE);
|
2013-02-11 07:56:35 -05:00
|
|
|
// Kerberos v5 OID
|
|
|
|
Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
|
|
|
|
|
2013-03-25 07:40:07 -04:00
|
|
|
context = manager.createContext(serverName, krb5Oid, delegatedCredentials, GSSContext.DEFAULT_LIFETIME);
|
2013-02-11 07:56:35 -05:00
|
|
|
|
|
|
|
//context.requestMutualAuth(true);
|
2013-03-20 07:38:31 -04:00
|
|
|
// TODO: used by IIS to pass token to Exchange ?
|
2013-02-11 07:56:35 -05:00
|
|
|
context.requestCredDeleg(true);
|
|
|
|
|
|
|
|
result = context.initSecContext(token, 0, token.length);
|
|
|
|
} catch (GSSException e) {
|
|
|
|
result = e;
|
2013-03-25 07:40:07 -04:00
|
|
|
} finally {
|
|
|
|
if (context != null) {
|
|
|
|
try {
|
|
|
|
context.dispose();
|
|
|
|
} catch (GSSException e) {
|
2014-03-19 05:29:50 -04:00
|
|
|
LOGGER.debug("KerberosHelper.internalInitSecContext " + e + ' ' + e.getMessage());
|
2013-03-25 07:40:07 -04:00
|
|
|
}
|
|
|
|
}
|
2013-02-11 07:56:35 -05:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2013-02-26 16:53:53 -05:00
|
|
|
/**
|
2013-03-20 07:38:31 -04:00
|
|
|
* Create server side Kerberos login context for provided credentials.
|
2013-02-26 16:53:53 -05:00
|
|
|
*
|
2014-03-19 05:29:50 -04:00
|
|
|
* @param serverPrincipal server principal
|
|
|
|
* @param serverPassword server passsword
|
2013-03-20 07:38:31 -04:00
|
|
|
* @return LoginContext server login context
|
2013-02-26 16:53:53 -05:00
|
|
|
* @throws LoginException on error
|
|
|
|
*/
|
2014-03-19 05:29:50 -04:00
|
|
|
public static LoginContext serverLogin(final String serverPrincipal, final String serverPassword) throws LoginException {
|
2013-02-26 16:53:53 -05:00
|
|
|
LoginContext serverLoginContext = new LoginContext("spnego-server", new CallbackHandler() {
|
|
|
|
|
|
|
|
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
2014-03-19 05:29:50 -04:00
|
|
|
for (Callback callback : callbacks) {
|
|
|
|
if (callback instanceof NameCallback) {
|
|
|
|
final NameCallback nameCallback = (NameCallback) callback;
|
|
|
|
nameCallback.setName(serverPrincipal);
|
|
|
|
} else if (callback instanceof PasswordCallback) {
|
|
|
|
final PasswordCallback passCallback = (PasswordCallback) callback;
|
|
|
|
passCallback.setPassword(serverPassword.toCharArray());
|
2013-02-26 16:53:53 -05:00
|
|
|
} else {
|
2014-03-19 05:29:50 -04:00
|
|
|
throw new UnsupportedCallbackException(callback);
|
2013-02-26 16:53:53 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
serverLoginContext.login();
|
|
|
|
return serverLoginContext;
|
|
|
|
}
|
|
|
|
|
2013-03-20 07:38:31 -04:00
|
|
|
/**
|
|
|
|
* Contains server Kerberos context information in server mode.
|
|
|
|
*/
|
2013-02-26 16:53:53 -05:00
|
|
|
public static class SecurityContext {
|
2013-03-20 07:38:31 -04:00
|
|
|
/**
|
|
|
|
* response token
|
|
|
|
*/
|
2013-02-26 16:53:53 -05:00
|
|
|
public byte[] token;
|
2013-03-20 07:38:31 -04:00
|
|
|
/**
|
|
|
|
* authenticated principal
|
|
|
|
*/
|
2013-02-26 16:53:53 -05:00
|
|
|
public String principal;
|
2013-03-20 07:38:31 -04:00
|
|
|
/**
|
|
|
|
* client delegated credential
|
|
|
|
*/
|
|
|
|
public GSSCredential clientCredential;
|
2013-02-26 16:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-03-20 07:38:31 -04:00
|
|
|
* Check client provided Kerberos token in server login context
|
2013-02-26 16:53:53 -05:00
|
|
|
*
|
|
|
|
* @param serverLoginContext server login context
|
|
|
|
* @param token Kerberos client token
|
|
|
|
* @return result with client principal and optional returned Kerberos token
|
|
|
|
* @throws GSSException on error
|
|
|
|
*/
|
|
|
|
public static SecurityContext acceptSecurityContext(LoginContext serverLoginContext, final byte[] token) throws GSSException {
|
|
|
|
Object result = Subject.doAs(serverLoginContext.getSubject(), new PrivilegedAction() {
|
|
|
|
|
|
|
|
public Object run() {
|
2014-03-19 05:29:50 -04:00
|
|
|
Object innerResult;
|
2013-02-26 16:53:53 -05:00
|
|
|
SecurityContext securityContext = new SecurityContext();
|
2013-03-25 07:40:07 -04:00
|
|
|
GSSContext context = null;
|
2013-02-26 16:53:53 -05:00
|
|
|
try {
|
|
|
|
GSSManager manager = GSSManager.getInstance();
|
|
|
|
|
|
|
|
// get server credentials from context
|
|
|
|
Oid krb5oid = new Oid("1.2.840.113554.1.2.2");
|
2013-03-20 07:38:31 -04:00
|
|
|
GSSCredential serverCreds = manager.createCredential(null/* use name from login context*/,
|
2013-02-26 16:53:53 -05:00
|
|
|
GSSCredential.DEFAULT_LIFETIME,
|
|
|
|
krb5oid,
|
2013-03-20 07:38:31 -04:00
|
|
|
GSSCredential.ACCEPT_ONLY/* server mode */);
|
2013-03-25 07:40:07 -04:00
|
|
|
context = manager.createContext(serverCreds);
|
2013-02-26 16:53:53 -05:00
|
|
|
|
|
|
|
securityContext.token = context.acceptSecContext(token, 0, token.length);
|
|
|
|
if (context.isEstablished()) {
|
|
|
|
securityContext.principal = context.getSrcName().toString();
|
|
|
|
LOGGER.debug("Authenticated user: " + securityContext.principal);
|
2013-03-20 07:38:31 -04:00
|
|
|
if (!context.getCredDelegState()) {
|
|
|
|
LOGGER.debug("Credentials can not be delegated");
|
|
|
|
} else {
|
|
|
|
// Get client delegated credentials from context (gateway mode)
|
|
|
|
securityContext.clientCredential = context.getDelegCred();
|
|
|
|
}
|
2013-02-26 16:53:53 -05:00
|
|
|
}
|
2014-03-19 05:29:50 -04:00
|
|
|
innerResult = securityContext;
|
2013-02-26 16:53:53 -05:00
|
|
|
} catch (GSSException e) {
|
2014-03-19 05:29:50 -04:00
|
|
|
innerResult = e;
|
2013-03-25 07:40:07 -04:00
|
|
|
} finally {
|
|
|
|
if (context != null) {
|
|
|
|
try {
|
|
|
|
context.dispose();
|
|
|
|
} catch (GSSException e) {
|
2014-03-19 05:29:50 -04:00
|
|
|
LOGGER.debug("KerberosHelper.acceptSecurityContext " + e + ' ' + e.getMessage());
|
2013-03-25 07:40:07 -04:00
|
|
|
}
|
|
|
|
}
|
2013-02-26 16:53:53 -05:00
|
|
|
}
|
2014-03-19 05:29:50 -04:00
|
|
|
return innerResult;
|
2013-02-26 16:53:53 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
if (result instanceof GSSException) {
|
2014-03-19 05:29:50 -04:00
|
|
|
LOGGER.info("KerberosHelper.acceptSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
|
2013-02-26 16:53:53 -05:00
|
|
|
throw (GSSException) result;
|
|
|
|
}
|
|
|
|
return (SecurityContext) result;
|
|
|
|
}
|
2013-02-11 07:56:35 -05:00
|
|
|
}
|