Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. * * 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, * version 2 of the License. * * 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, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ package com.zimbra.cs.security.sasl; import com.zimbra.cs.account.Account; import com.zimbra.cs.account.Provisioning; import com.zimbra.cs.account.auth.AuthContext; import com.zimbra.cs.security.kerberos.Krb5Keytab; import com.zimbra.common.account.Key; import com.zimbra.common.localconfig.LC; import com.zimbra.common.service.ServiceException; import com.zimbra.common.util.ZimbraLog; import javax.security.sasl.SaslServer; import javax.security.sasl.Sasl; import javax.security.sasl.SaslException; import javax.security.sasl.AuthorizeCallback; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.kerberos.KerberosKey; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.Callback; import javax.security.auth.callback.UnsupportedCallbackException; import java.util.Map; import java.util.HashMap; import java.util.List; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.PrivilegedExceptionAction; import java.security.PrivilegedActionException; import org.apache.commons.codec.binary.Base64; public class GssAuthenticator extends Authenticator { private SaslServer mSaslServer; private boolean mEncryptionEnabled; private static final String QOP_AUTH = "auth"; private static final String QOP_AUTH_INT = "auth-int"; private static final String QOP_AUTH_CONF = "auth-conf"; private static final int MAX_RECEIVE_SIZE = 4096; private static final int MAX_SEND_SIZE = 4096; public static final String KRB5_DEBUG_ENABLED_PROP = "ZimbraKrb5DebugEnabled"; private static final boolean DEBUG = LC.krb5_debug_enabled.booleanValue() || Boolean.getBoolean(KRB5_DEBUG_ENABLED_PROP); public static final String MECHANISM = "GSSAPI"; // TODO Remove this debugging option for final release private static final Boolean GSS_ENABLED = Boolean.getBoolean("ZimbraGssEnabled"); // SASL properties to enable encryption private static final Map<String, String> ENCRYPTION_PROPS = new HashMap<String, String>(); static { ENCRYPTION_PROPS.put(Sasl.QOP, QOP_AUTH + "," + QOP_AUTH_INT + "," + QOP_AUTH_CONF); ENCRYPTION_PROPS.put(Sasl.MAX_BUFFER, String.valueOf(MAX_RECEIVE_SIZE)); ENCRYPTION_PROPS.put(Sasl.RAW_SEND_SIZE, String.valueOf(MAX_SEND_SIZE)); if (DEBUG) { System.setProperty("sun.security.krb5.debug", "true"); System.setProperty("sun.security.jgss.debug", "true"); } } public GssAuthenticator(AuthenticatorUser user) { super(MECHANISM, user); } @Override protected boolean isSupported() { // GSSAPI is switched on and off on a per-protocol basis via LDAP settings if (!GSS_ENABLED && !mAuthUser.isGssapiAvailable()) return false; // check whether this server requires encryption for GSSAPI try { return mAuthUser.isSSLEnabled() || !Provisioning.getInstance().getLocalServer() .getBooleanAttr(Provisioning.A_zimbraSaslGssapiRequiresTls, false); } catch (ServiceException e) { ZimbraLog.security.warn( "could not determine whether TLS encryption is required for GSSAPI auth; defaulting to FALSE", e); return false; } } @Override public boolean initialize() throws IOException { Krb5Keytab keytab = getKeytab(LC.krb5_keytab.value()); if (keytab == null) { sendFailed("mechanism not supported"); return false; } debug("keytab file = %s", keytab.getFile()); final String host; if (LC.krb5_service_principal_from_interface_address.booleanValue()) { String localSocketHostname = localAddress.getCanonicalHostName().toLowerCase(); if (localSocketHostname.length() == 0 || Character.isDigit(localSocketHostname.charAt(0))) localSocketHostname = LC.zimbra_server_hostname.value(); host = localSocketHostname; } else { host = LC.zimbra_server_hostname.value(); } KerberosPrincipal kp = new KerberosPrincipal(getProtocol() + '/' + host); debug("kerberos principal = %s", kp); Subject subject = getSubject(keytab, kp); if (subject == null) { sendFailed(); return false; } debug("subject = %s", subject); final Map<String, String> props = getSaslProperties(); if (DEBUG && props != null) { String qop = props.get(Sasl.QOP); debug("Sent QOP = " + (qop != null ? qop : "auth")); } try { mSaslServer = (SaslServer) Subject.doAs(subject, new PrivilegedExceptionAction<Object>() { @Override public Object run() throws SaslException { return Sasl.createSaslServer(getMechanism(), getProtocol(), host, props, new GssCallbackHandler()); } }); } catch (PrivilegedActionException e) { sendFailed(); getLog().warn("Could not create SaslServer", e.getCause()); return false; } return true; } private Krb5Keytab getKeytab(String path) { try { return Krb5Keytab.getInstance(path); } catch (IOException e) { getLog().warn("Keytab file '" + path + "' not found"); return null; } } private Subject getSubject(Krb5Keytab keytab, KerberosPrincipal kp) throws IOException { List<KerberosKey> keys = keytab.getKeys(kp); if (keys == null) { getLog().warn("Key not found in keystore for service principal '" + kp + "'"); return null; } Subject subject = new Subject(); subject.getPrincipals().add(kp); subject.getPrivateCredentials().addAll(keys); return subject; } @Override public void handle(final byte[] data) throws IOException { if (isComplete()) { throw new IllegalStateException("Authentication already completed"); } // Evaluate client response and get challenge bytes byte[] bytes; try { bytes = mSaslServer.evaluateResponse(data); } catch (SaslException e) { getLog().warn("SaslServer.evaluateResponse() failed", e); // Only send if the callback hadn't already sent an error response if (!isComplete()) sendBadRequest(); return; } // If exchange not complete, send additional challenge if (!isComplete()) { assert !mSaslServer.isComplete(); String s = new String(Base64.encodeBase64(bytes), "US-ASCII"); sendContinuation(s); return; } // Authentication complete, so finish up assert mSaslServer.isComplete(); if (DEBUG) dumpNegotiatedProperties(); // If authentication failed, dispose of SaslServer instance if (!isAuthenticated()) { debug("Authentication failed"); dispose(); return; } // Authentication successful, so check if encryption enabled String qop = (String) mSaslServer.getNegotiatedProperty(Sasl.QOP); if (QOP_AUTH_INT.equals(qop) || QOP_AUTH_CONF.equals(qop)) { debug("SASL encryption enabled (%s)", qop); mEncryptionEnabled = true; } else { dispose(); // No need for SaslServer any longer } } @Override public Account authenticate(String username, String principal, String unused, AuthContext.Protocol protocol, String origRemoteIp, String remoteIp, String userAgent) throws ServiceException { Provisioning prov = Provisioning.getInstance(); Account authAccount = prov.get(Key.AccountBy.krb5Principal, principal); if (authAccount == null) { ZimbraLog.account.warn( "authentication failed (no account associated with Kerberos principal " + principal + ')'); return null; } // make sure the protocol is enabled for the user if (!isProtocolEnabled(authAccount, protocol)) { ZimbraLog.account.info("Authentication failed - %s not enabled for %s", protocol, authAccount.getName()); return null; } Account targetAcct = authorize(authAccount, username, true); if (targetAcct != null) prov.accountAuthed(authAccount); return targetAcct; } private Map<String, String> getSaslProperties() { // Don't offer encryption if SSL is enabled return mAuthUser.isSSLEnabled() ? null : ENCRYPTION_PROPS; } @Override public boolean isEncryptionEnabled() { return mEncryptionEnabled; } @Override public InputStream unwrap(InputStream is) { return new SaslInputStream(is, mSaslServer); } @Override public OutputStream wrap(OutputStream os) { return new SaslOutputStream(os, mSaslServer); } @Override public SaslServer getSaslServer() { return mSaslServer; } @Override public void dispose() { debug("dispose called"); try { mSaslServer.dispose(); } catch (SaslException e) { getLog().warn("SaslServer.dispose() failed", e); } } private class GssCallbackHandler implements CallbackHandler { GssCallbackHandler() { } @Override public void handle(Callback[] cbs) throws IOException, UnsupportedCallbackException { if (cbs == null || cbs.length != 1) { throw new IOException("Bad callback"); } if (!(cbs[0] instanceof AuthorizeCallback)) { throw new UnsupportedCallbackException(cbs[0]); } AuthorizeCallback cb = (AuthorizeCallback) cbs[0]; debug("gss authorization_id = %s", cb.getAuthorizationID()); debug("gss authentication_id = %s", cb.getAuthenticationID()); cb.setAuthorized(authenticate(cb.getAuthorizationID(), cb.getAuthenticationID(), null)); } } private void dumpNegotiatedProperties() { pp("QOP", Sasl.QOP); pp("MAX_BUFFER", Sasl.MAX_BUFFER); pp("MAX_RECEIVE_SIZE", Sasl.RAW_SEND_SIZE); pp("STRENGTH", Sasl.STRENGTH); } private void pp(String printName, String propName) { Object obj = mSaslServer.getNegotiatedProperty(propName); if (obj != null) debug("Negotiated property %s = %s", printName, obj); } static void debug(String format, Object... args) { if (DEBUG) System.out.printf("[DEBUG GssAuthenticator] " + format + "\n", args); } }