From 4d8c6d4c5279eb081f4e9954db78b267459d3e23 Mon Sep 17 00:00:00 2001 From: mguessan Date: Mon, 11 Feb 2013 12:56:35 +0000 Subject: [PATCH] Kerberos authentication implementation: SpNegoScheme to implement Negotiate authentication scheme, KerberosHelper to handle ticket access and KerberosLoginConfiguration to replace JAAS configuration file git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@2057 3d1905a2-6b24-0410-a738-b14d5a86fcbd --- src/java/davmail/http/KerberosHelper.java | 130 +++++++++++ .../http/KerberosLoginConfiguration.java | 56 +++++ src/java/davmail/http/SpNegoScheme.java | 217 ++++++++++++++++++ 3 files changed, 403 insertions(+) create mode 100644 src/java/davmail/http/KerberosHelper.java create mode 100644 src/java/davmail/http/KerberosLoginConfiguration.java create mode 100644 src/java/davmail/http/SpNegoScheme.java diff --git a/src/java/davmail/http/KerberosHelper.java b/src/java/davmail/http/KerberosHelper.java new file mode 100644 index 00000000..48dcc403 --- /dev/null +++ b/src/java/davmail/http/KerberosHelper.java @@ -0,0 +1,130 @@ +/* + * 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; + +import org.apache.log4j.Logger; +import org.ietf.jgss.*; + +import javax.security.auth.Subject; +import javax.security.auth.callback.*; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import java.io.IOException; +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(); + protected static KerberosCallbackHandler kerberosCallbackHandler; + protected static LoginContext loginContext; + + static { + Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration"); + kerberosCallbackHandler = new KerberosCallbackHandler(); + } + + protected static class KerberosCallbackHandler implements CallbackHandler { + String principal; + String password; + + protected KerberosCallbackHandler() { + } + + protected KerberosCallbackHandler(String principal, String password) { + this.principal = principal; + this.password = password; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + if (principal == null) { + throw new UnsupportedCallbackException(callbacks[i]); + } + final NameCallback nameCallback = (NameCallback) callbacks[i]; + nameCallback.setName(principal); + } else if (callbacks[i] instanceof PasswordCallback) { + if (password == null) { + throw new UnsupportedCallbackException(callbacks[i]); + } + final PasswordCallback passCallback = (PasswordCallback) callbacks[i]; + passCallback.setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + } + } + + public static byte[] getToken(final String host, final byte[] token) throws GSSException, LoginException { + LOGGER.debug("KerberosHelper.getToken " + host + " " + token.length + " bytes token"); + + LoginContext loginContext = login(); + + Object result = Subject.doAs(loginContext.getSubject(), new PrivilegedAction() { + + public Object run() { + Object result; + try { + GSSManager manager = GSSManager.getInstance(); + GSSName serverName = manager.createName("HTTP/" + host, null); + // Kerberos v5 OID + Oid krb5Oid = new Oid("1.2.840.113554.1.2.2"); + + GSSContext context = manager.createContext(serverName, krb5Oid, null, + GSSContext.DEFAULT_LIFETIME); + + //context.requestMutualAuth(true); + context.requestCredDeleg(true); + + result = context.initSecContext(token, 0, token.length); + } catch (GSSException e) { + result = e; + } + return result; + } + }); + if (result instanceof GSSException) { + throw (GSSException) result; + } + + LOGGER.debug("KerberosHelper.getToken return " + ((byte[]) result).length + " bytes token"); + return (byte[]) result; + } + + public static void setCredentials(String principal, String password) { + kerberosCallbackHandler = new KerberosCallbackHandler(principal, password); + } + + public static LoginContext login() throws LoginException { + synchronized (LOCK) { + if (loginContext == null) { + final LoginContext localLoginContext = new LoginContext("spnego-client", kerberosCallbackHandler); + localLoginContext.login(); + loginContext = localLoginContext; + } + } + return loginContext; + } +} diff --git a/src/java/davmail/http/KerberosLoginConfiguration.java b/src/java/davmail/http/KerberosLoginConfiguration.java new file mode 100644 index 00000000..3170307f --- /dev/null +++ b/src/java/davmail/http/KerberosLoginConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import java.util.HashMap; + +/** + * Custom JAAS login configuration. + * Equivalent to the following configuration: + * spnego-client { + * com.sun.security.auth.module.Krb5LoginModule required; + * }; + *

+ */ +public class KerberosLoginConfiguration extends Configuration { + protected static final AppConfigurationEntry[] CLIENT_LOGIN_MODULE; + + static { + HashMap loginModuleOptions = new HashMap(); + loginModuleOptions.put("useTicketCache", "true"); + //loginModuleOptions.put("doNotPrompt", "true"); + CLIENT_LOGIN_MODULE = new AppConfigurationEntry[]{new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, loginModuleOptions)}; + + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + if ("spnego-client".equals(name)) { + return CLIENT_LOGIN_MODULE; + } else { + return null; + } + } + + public void refresh() { + // nothing to do + } +} \ No newline at end of file diff --git a/src/java/davmail/http/SpNegoScheme.java b/src/java/davmail/http/SpNegoScheme.java new file mode 100644 index 00000000..c1c3d457 --- /dev/null +++ b/src/java/davmail/http/SpNegoScheme.java @@ -0,0 +1,217 @@ +/* + * 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; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.auth.*; +import org.apache.commons.httpclient.util.EncodingUtil; +import org.ietf.jgss.GSSException; + +import javax.security.auth.login.LoginException; + +/** + * Implement spnego (Negotiate) authentication scheme. + */ +public class SpNegoScheme implements AuthScheme { + private static final int UNINITIATED = 0; + private static final int INITIATED = 1; + private static final int TYPE1_MSG_GENERATED = 2; + private static final int TYPE2_MSG_RECEIVED = 3; + private static final int TYPE3_MSG_GENERATED = 4; + private static final int FAILED = Integer.MAX_VALUE; + + private byte[] serverToken; + /** + * Authentication process state + */ + private int state; + + /** + * Processes the Negotiate challenge. + * + * @param challenge the challenge string + * @throws MalformedChallengeException is thrown if the authentication challenge is malformed + */ + public void processChallenge(final String challenge) throws MalformedChallengeException { + String authScheme = AuthChallengeParser.extractScheme(challenge); + if (!authScheme.equalsIgnoreCase(getSchemeName())) { + throw new MalformedChallengeException("Invalid Negotiate challenge: " + challenge); + } + int spaceIndex = challenge.indexOf(' '); + if (spaceIndex != -1) { + // step 2: received server challenge + serverToken = Base64.decodeBase64(EncodingUtil.getBytes( + challenge.substring(spaceIndex, challenge.length()).trim(), "ASCII")); + this.state = TYPE2_MSG_RECEIVED; + } else { + this.serverToken = null; + if (this.state == UNINITIATED) { + this.state = INITIATED; + } else { + this.state = FAILED; + } + } + } + + + /** + * Returns textual designation of the Negotiate authentication scheme. + * + * @return Negotiate + */ + public String getSchemeName() { + return "Negotiate"; + } + + /** + * Not used with Negotiate. + * + * @return null + */ + public String getParameter(String s) { + return null; + } + + /** + * Not used with Negotiate. + * + * @return null + */ + public String getRealm() { + return null; + } + + /** + * Deprecated. + */ + @Deprecated + public String getID() { + throw new UnsupportedOperationException(); + } + + /** + * Negotiate is connection based. + * + * @return true + */ + public boolean isConnectionBased() { + return true; + } + + /** + * Tests if the Negotiate authentication process has been completed. + * + * @return true if authorization has been processed + */ + public boolean isComplete() { + return state == TYPE3_MSG_GENERATED || state == FAILED; + } + + /** + * Not implemented. + * + * @param credentials user credentials + * @param method method name + * @param uri URI + * @return an Negotiate authorization string + * @throws org.apache.commons.httpclient.auth.InvalidCredentialsException + * if authentication credentials + * are not valid or not applicable for this authentication scheme + * @throws org.apache.commons.httpclient.auth.AuthenticationException + * if authorization string cannot + * be generated due to an authentication failure + */ + @Deprecated + public String authenticate(final Credentials credentials, String method, String uri) throws AuthenticationException { + throw new UnsupportedOperationException(); + } + + /** + * Produces Negotiate authorization string for the given set of + * {@link Credentials}. + * + * @param credentials The set of credentials to be used for authentication + * @param httpMethod The method being authenticated + * @return an Negotiate authorization string + * @throws org.apache.commons.httpclient.auth.InvalidCredentialsException + * if authentication credentials + * are not valid or not applicable for this authentication scheme + * @throws AuthenticationException if authorization string cannot + * be generated due to an authentication failure + */ + public String authenticate(Credentials credentials, HttpMethod httpMethod) throws AuthenticationException { + if (this.state == UNINITIATED) { + throw new IllegalStateException("Negotiate authentication process has not been initiated"); + } + String host = null; + try { + host = httpMethod.getURI().getHost(); + } catch (URIException e) { + // ignore + } + if (host == null) { + Header header = httpMethod.getRequestHeader("Host"); + if (header != null) { + host = header.getValue(); + if (host.indexOf(':') >= 0) { + host = host.substring(0, host.indexOf(':')); + } + } + } + if (host == null) { + throw new IllegalStateException("Negotiate authentication failed: empty host"); + } + + // no credentials needed + String response; + try { + if (this.state == INITIATED || this.state == FAILED) { + // send initial token to server + response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.getToken(host, new byte[0]))); + this.state = TYPE1_MSG_GENERATED; + } else { + // send challenge response + response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.getToken(host, serverToken))); + this.state = TYPE3_MSG_GENERATED; + } + } catch (GSSException gsse) { + state = FAILED; + if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL + || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + if (gsse.getMajor() == GSSException.NO_CRED) + throw new CredentialsNotAvailableException(gsse.getMessage(), gsse); + if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN + || gsse.getMajor() == GSSException.DUPLICATE_TOKEN + || gsse.getMajor() == GSSException.OLD_TOKEN) + throw new AuthChallengeException(gsse.getMessage(), gsse); + // other error + throw new AuthenticationException(gsse.getMessage(), gsse); + } catch (LoginException e) { + state = FAILED; + throw new InvalidCredentialsException(e.getMessage(), e); + } + return "Negotiate " + response; + } + +}