View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2012  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.http;
20  
21  import davmail.Settings;
22  import davmail.ui.CredentialPromptDialog;
23  import org.apache.log4j.Logger;
24  import org.ietf.jgss.*;
25  
26  import javax.security.auth.RefreshFailedException;
27  import javax.security.auth.Subject;
28  import javax.security.auth.callback.*;
29  import javax.security.auth.kerberos.KerberosTicket;
30  import javax.security.auth.login.LoginContext;
31  import javax.security.auth.login.LoginException;
32  import java.awt.*;
33  import java.io.BufferedReader;
34  import java.io.IOException;
35  import java.io.InputStreamReader;
36  import java.security.PrivilegedAction;
37  import java.security.Security;
38  
39  
40  /**
41   * Kerberos helper class.
42   */
43  @SuppressWarnings( {"removal"})
44  public class KerberosHelper {
45      protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
46      protected static final Object LOCK = new Object();
47      protected static final KerberosCallbackHandler KERBEROS_CALLBACK_HANDLER;
48      private static LoginContext clientLoginContext;
49  
50      static {
51          // Load Jaas configuration from class
52          Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
53          // Kerberos callback handler singleton
54          KERBEROS_CALLBACK_HANDLER = new KerberosCallbackHandler();
55      }
56  
57      private KerberosHelper() {
58      }
59  
60      @SuppressWarnings("UseOfSystemOutOrSystemErr")
61      protected static class KerberosCallbackHandler implements CallbackHandler {
62          String principal;
63          String password;
64  
65          public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
66              for (Callback callback : callbacks) {
67                  if (callback instanceof NameCallback) {
68                      if (principal == null) {
69                          // if we get there kerberos token is missing or invalid
70                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
71                              // headless or server mode
72                              System.out.print(((NameCallback) callback).getPrompt());
73                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
74                              principal = inReader.readLine();
75                          } else {
76                              CredentialPromptDialog credentialPromptDialog = new CredentialPromptDialog(((NameCallback) callback).getPrompt());
77                              principal = credentialPromptDialog.getPrincipal();
78                              password = String.valueOf(credentialPromptDialog.getPassword());
79                          }
80                      }
81                      if (principal == null) {
82                          throw new IOException("KerberosCallbackHandler: failed to retrieve principal");
83                      }
84                      ((NameCallback) callback).setName(principal);
85  
86                  } else if (callback instanceof PasswordCallback) {
87                      if (password == null) {
88                          // if we get there kerberos token is missing or invalid
89                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
90                              // headless or server mode
91                              System.out.print(((PasswordCallback) callback).getPrompt());
92                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
93                              password = inReader.readLine();
94                          }
95                      }
96                      if (password == null) {
97                          throw new IOException("KerberosCallbackHandler: failed to retrieve password");
98                      }
99                      ((PasswordCallback) callback).setPassword(password.toCharArray());
100 
101                 } else {
102                     throw new UnsupportedCallbackException(callback);
103                 }
104             }
105         }
106     }
107 
108     /**
109      * Force client principal in callback handler
110      *
111      * @param principal client principal
112      */
113     public static void setClientPrincipal(String principal) {
114         KERBEROS_CALLBACK_HANDLER.principal = principal;
115     }
116 
117     /**
118      * Force client password in callback handler
119      *
120      * @param password client password
121      */
122     public static void setClientPassword(String password) {
123         KERBEROS_CALLBACK_HANDLER.password = password;
124     }
125 
126     /**
127      * Get response Kerberos token for host with provided token.
128      *
129      * @param protocol target protocol
130      * @param host     target host
131      * @param token    input token
132      * @return response token
133      * @throws GSSException   on error
134      * @throws LoginException on error
135      */
136     public static byte[] initSecurityContext(final String protocol, final String host, final byte[] token) throws GSSException, LoginException {
137         return initSecurityContext(protocol, host, null, token);
138     }
139 
140     /**
141      * Get response Kerberos token for host with provided token, use client provided delegation credentials.
142      * Used to authenticate with target host on a gateway server with client credentials,
143      * gateway must have its own principal authorized for delegation
144      *
145      * @param protocol             target protocol
146      * @param host                 target host
147      * @param delegatedCredentials client delegated credentials
148      * @param token                input token
149      * @return response token
150      * @throws GSSException   on error
151      * @throws LoginException on error
152      */
153     public static byte[] initSecurityContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) throws GSSException, LoginException {
154         LOGGER.debug("KerberosHelper.initSecurityContext " + protocol + '@' + host + ' ' + token.length + " bytes token");
155 
156         synchronized (LOCK) {
157             // check cached TGT
158             if (clientLoginContext != null) {
159                 for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
160                     KerberosTicket kerberosTicket = (KerberosTicket) ticket;
161                     if (kerberosTicket.getServer().getName().startsWith("krbtgt") && !kerberosTicket.isCurrent()) {
162                         LOGGER.debug("KerberosHelper.clientLogin cached TGT expired, try to relogin");
163                         clientLoginContext = null;
164                     }
165                 }
166             }
167             // create client login context
168             if (clientLoginContext == null) {
169                 final LoginContext localLoginContext = new LoginContext("spnego-client", KERBEROS_CALLBACK_HANDLER);
170                 localLoginContext.login();
171                 clientLoginContext = localLoginContext;
172             }
173             // try to renew almost expired tickets
174             for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
175                 KerberosTicket kerberosTicket = (KerberosTicket) ticket;
176                 LOGGER.debug("KerberosHelper.clientLogin ticket for " + kerberosTicket.getServer().getName() + " expires at " + kerberosTicket.getEndTime());
177                 if (kerberosTicket.getEndTime().getTime() < System.currentTimeMillis() + 10000) {
178                     if (kerberosTicket.isRenewable()) {
179                         try {
180                             kerberosTicket.refresh();
181                         } catch (RefreshFailedException e) {
182                             LOGGER.debug("KerberosHelper.clientLogin failed to renew ticket " + kerberosTicket);
183                         }
184                     } else {
185                         LOGGER.debug("KerberosHelper.clientLogin ticket is not renewable");
186                     }
187                 }
188             }
189 
190             Object result = internalInitSecContext(protocol, host, delegatedCredentials, token);
191             if (result instanceof GSSException) {
192                 LOGGER.info("KerberosHelper.initSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
193                 throw (GSSException) result;
194             }
195 
196             LOGGER.debug("KerberosHelper.initSecurityContext return " + ((byte[]) result).length + " bytes token");
197             return (byte[]) result;
198         }
199     }
200 
201     protected static Object internalInitSecContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) {
202         return Subject.doAs(clientLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
203             Object result;
204             GSSContext context = null;
205             try {
206                 GSSManager manager = GSSManager.getInstance();
207                 GSSName serverName = manager.createName(protocol + '@' + host, GSSName.NT_HOSTBASED_SERVICE);
208                 // Kerberos v5 OID
209                 Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
210 
211                 context = manager.createContext(serverName, krb5Oid, delegatedCredentials, GSSContext.DEFAULT_LIFETIME);
212 
213                 //context.requestMutualAuth(true);
214                 // TODO: used by IIS to pass token to Exchange ?
215                 context.requestCredDeleg(true);
216 
217                 result = context.initSecContext(token, 0, token.length);
218             } catch (GSSException e) {
219                 result = e;
220             } finally {
221                 if (context != null) {
222                     try {
223                         context.dispose();
224                     } catch (GSSException e) {
225                         LOGGER.debug("KerberosHelper.internalInitSecContext " + e + ' ' + e.getMessage());
226                     }
227                 }
228             }
229             return result;
230         });
231     }
232 
233     /**
234      * Create server side Kerberos login context for provided credentials.
235      *
236      * @param serverPrincipal server principal
237      * @param serverPassword  server passsword
238      * @return LoginContext server login context
239      * @throws LoginException on error
240      */
241     public static LoginContext serverLogin(final String serverPrincipal, final String serverPassword) throws LoginException {
242         LoginContext serverLoginContext = new LoginContext("spnego-server", callbacks -> {
243             for (Callback callback : callbacks) {
244                 if (callback instanceof NameCallback) {
245                     final NameCallback nameCallback = (NameCallback) callback;
246                     nameCallback.setName(serverPrincipal);
247                 } else if (callback instanceof PasswordCallback) {
248                     final PasswordCallback passCallback = (PasswordCallback) callback;
249                     passCallback.setPassword(serverPassword.toCharArray());
250                 } else {
251                     throw new UnsupportedCallbackException(callback);
252                 }
253             }
254 
255         });
256         serverLoginContext.login();
257         return serverLoginContext;
258     }
259 
260     /**
261      * Contains server Kerberos context information in server mode.
262      */
263     public static class SecurityContext {
264         /**
265          * response token
266          */
267         public byte[] token;
268         /**
269          * authenticated principal
270          */
271         public String principal;
272         /**
273          * client delegated credential
274          */
275         public GSSCredential clientCredential;
276     }
277 
278     /**
279      * Check client provided Kerberos token in server login context
280      *
281      * @param serverLoginContext server login context
282      * @param token              Kerberos client token
283      * @return result with client principal and optional returned Kerberos token
284      * @throws GSSException on error
285      */
286     public static SecurityContext acceptSecurityContext(LoginContext serverLoginContext, final byte[] token) throws GSSException {
287         Object result = Subject.doAs(serverLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
288             Object innerResult;
289             SecurityContext securityContext = new SecurityContext();
290             GSSContext context = null;
291             try {
292                 GSSManager manager = GSSManager.getInstance();
293 
294                 // get server credentials from context
295                 Oid krb5oid = new Oid("1.2.840.113554.1.2.2");
296                 GSSCredential serverCreds = manager.createCredential(null/* use name from login context*/,
297                         GSSCredential.DEFAULT_LIFETIME,
298                         krb5oid,
299                         GSSCredential.ACCEPT_ONLY/* server mode */);
300                 context = manager.createContext(serverCreds);
301 
302                 securityContext.token = context.acceptSecContext(token, 0, token.length);
303                 if (context.isEstablished()) {
304                     securityContext.principal = context.getSrcName().toString();
305                     LOGGER.debug("Authenticated user: " + securityContext.principal);
306                     if (!context.getCredDelegState()) {
307                         LOGGER.debug("Credentials can not be delegated");
308                     } else {
309                         // Get client delegated credentials from context (gateway mode)
310                         securityContext.clientCredential = context.getDelegCred();
311                     }
312                 }
313                 innerResult = securityContext;
314             } catch (GSSException e) {
315                 innerResult = e;
316             } finally {
317                 if (context != null) {
318                     try {
319                         context.dispose();
320                     } catch (GSSException e) {
321                         LOGGER.debug("KerberosHelper.acceptSecurityContext " + e + ' ' + e.getMessage());
322                     }
323                 }
324             }
325             return innerResult;
326         });
327         if (result instanceof GSSException) {
328             LOGGER.info("KerberosHelper.acceptSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
329             throw (GSSException) result;
330         }
331         return (SecurityContext) result;
332     }
333 }