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  public class KerberosHelper {
44      protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
45      protected static final Object LOCK = new Object();
46      protected static final KerberosCallbackHandler KERBEROS_CALLBACK_HANDLER;
47      private static LoginContext clientLoginContext;
48  
49      static {
50          // Load Jaas configuration from class
51          Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
52          // Kerberos callback handler singleton
53          KERBEROS_CALLBACK_HANDLER = new KerberosCallbackHandler();
54      }
55  
56      private KerberosHelper() {
57      }
58  
59      @SuppressWarnings("UseOfSystemOutOrSystemErr")
60      protected static class KerberosCallbackHandler implements CallbackHandler {
61          String principal;
62          String password;
63  
64          public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
65              for (Callback callback : callbacks) {
66                  if (callback instanceof NameCallback) {
67                      if (principal == null) {
68                          // if we get there kerberos token is missing or invalid
69                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
70                              // headless or server mode
71                              System.out.print(((NameCallback) callback).getPrompt());
72                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
73                              principal = inReader.readLine();
74                          } else {
75                              CredentialPromptDialog credentialPromptDialog = new CredentialPromptDialog(((NameCallback) callback).getPrompt());
76                              principal = credentialPromptDialog.getPrincipal();
77                              password = String.valueOf(credentialPromptDialog.getPassword());
78                          }
79                      }
80                      if (principal == null) {
81                          throw new IOException("KerberosCallbackHandler: failed to retrieve principal");
82                      }
83                      ((NameCallback) callback).setName(principal);
84  
85                  } else if (callback instanceof PasswordCallback) {
86                      if (password == null) {
87                          // if we get there kerberos token is missing or invalid
88                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
89                              // headless or server mode
90                              System.out.print(((PasswordCallback) callback).getPrompt());
91                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
92                              password = inReader.readLine();
93                          }
94                      }
95                      if (password == null) {
96                          throw new IOException("KerberosCallbackHandler: failed to retrieve password");
97                      }
98                      ((PasswordCallback) callback).setPassword(password.toCharArray());
99  
100                 } else {
101                     throw new UnsupportedCallbackException(callback);
102                 }
103             }
104         }
105     }
106 
107     /**
108      * Force client principal in callback handler
109      *
110      * @param principal client principal
111      */
112     public static void setClientPrincipal(String principal) {
113         KERBEROS_CALLBACK_HANDLER.principal = principal;
114     }
115 
116     /**
117      * Force client password in callback handler
118      *
119      * @param password client password
120      */
121     public static void setClientPassword(String password) {
122         KERBEROS_CALLBACK_HANDLER.password = password;
123     }
124 
125     /**
126      * Get response Kerberos token for host with provided token.
127      *
128      * @param protocol target protocol
129      * @param host     target host
130      * @param token    input token
131      * @return response token
132      * @throws GSSException   on error
133      * @throws LoginException on error
134      */
135     public static byte[] initSecurityContext(final String protocol, final String host, final byte[] token) throws GSSException, LoginException {
136         return initSecurityContext(protocol, host, null, token);
137     }
138 
139     /**
140      * Get response Kerberos token for host with provided token, use client provided delegation credentials.
141      * Used to authenticate with target host on a gateway server with client credentials,
142      * gateway must have its own principal authorized for delegation
143      *
144      * @param protocol             target protocol
145      * @param host                 target host
146      * @param delegatedCredentials client delegated credentials
147      * @param token                input token
148      * @return response token
149      * @throws GSSException   on error
150      * @throws LoginException on error
151      */
152     public static byte[] initSecurityContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) throws GSSException, LoginException {
153         LOGGER.debug("KerberosHelper.initSecurityContext " + protocol + '@' + host + ' ' + token.length + " bytes token");
154 
155         synchronized (LOCK) {
156             // check cached TGT
157             if (clientLoginContext != null) {
158                 for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
159                     KerberosTicket kerberosTicket = (KerberosTicket) ticket;
160                     if (kerberosTicket.getServer().getName().startsWith("krbtgt") && !kerberosTicket.isCurrent()) {
161                         LOGGER.debug("KerberosHelper.clientLogin cached TGT expired, try to relogin");
162                         clientLoginContext = null;
163                     }
164                 }
165             }
166             // create client login context
167             if (clientLoginContext == null) {
168                 final LoginContext localLoginContext = new LoginContext("spnego-client", KERBEROS_CALLBACK_HANDLER);
169                 localLoginContext.login();
170                 clientLoginContext = localLoginContext;
171             }
172             // try to renew almost expired tickets
173             for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
174                 KerberosTicket kerberosTicket = (KerberosTicket) ticket;
175                 LOGGER.debug("KerberosHelper.clientLogin ticket for " + kerberosTicket.getServer().getName() + " expires at " + kerberosTicket.getEndTime());
176                 if (kerberosTicket.getEndTime().getTime() < System.currentTimeMillis() + 10000) {
177                     if (kerberosTicket.isRenewable()) {
178                         try {
179                             kerberosTicket.refresh();
180                         } catch (RefreshFailedException e) {
181                             LOGGER.debug("KerberosHelper.clientLogin failed to renew ticket " + kerberosTicket);
182                         }
183                     } else {
184                         LOGGER.debug("KerberosHelper.clientLogin ticket is not renewable");
185                     }
186                 }
187             }
188 
189             Object result = internalInitSecContext(protocol, host, delegatedCredentials, token);
190             if (result instanceof GSSException) {
191                 LOGGER.info("KerberosHelper.initSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
192                 throw (GSSException) result;
193             }
194 
195             LOGGER.debug("KerberosHelper.initSecurityContext return " + ((byte[]) result).length + " bytes token");
196             return (byte[]) result;
197         }
198     }
199 
200     protected static Object internalInitSecContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) {
201         return Subject.doAs(clientLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
202             Object result;
203             GSSContext context = null;
204             try {
205                 GSSManager manager = GSSManager.getInstance();
206                 GSSName serverName = manager.createName(protocol + '@' + host, GSSName.NT_HOSTBASED_SERVICE);
207                 // Kerberos v5 OID
208                 Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
209 
210                 context = manager.createContext(serverName, krb5Oid, delegatedCredentials, GSSContext.DEFAULT_LIFETIME);
211 
212                 //context.requestMutualAuth(true);
213                 // TODO: used by IIS to pass token to Exchange ?
214                 context.requestCredDeleg(true);
215 
216                 result = context.initSecContext(token, 0, token.length);
217             } catch (GSSException e) {
218                 result = e;
219             } finally {
220                 if (context != null) {
221                     try {
222                         context.dispose();
223                     } catch (GSSException e) {
224                         LOGGER.debug("KerberosHelper.internalInitSecContext " + e + ' ' + e.getMessage());
225                     }
226                 }
227             }
228             return result;
229         });
230     }
231 
232     /**
233      * Create server side Kerberos login context for provided credentials.
234      *
235      * @param serverPrincipal server principal
236      * @param serverPassword  server passsword
237      * @return LoginContext server login context
238      * @throws LoginException on error
239      */
240     public static LoginContext serverLogin(final String serverPrincipal, final String serverPassword) throws LoginException {
241         LoginContext serverLoginContext = new LoginContext("spnego-server", callbacks -> {
242             for (Callback callback : callbacks) {
243                 if (callback instanceof NameCallback) {
244                     final NameCallback nameCallback = (NameCallback) callback;
245                     nameCallback.setName(serverPrincipal);
246                 } else if (callback instanceof PasswordCallback) {
247                     final PasswordCallback passCallback = (PasswordCallback) callback;
248                     passCallback.setPassword(serverPassword.toCharArray());
249                 } else {
250                     throw new UnsupportedCallbackException(callback);
251                 }
252             }
253 
254         });
255         serverLoginContext.login();
256         return serverLoginContext;
257     }
258 
259     /**
260      * Contains server Kerberos context information in server mode.
261      */
262     public static class SecurityContext {
263         /**
264          * response token
265          */
266         public byte[] token;
267         /**
268          * authenticated principal
269          */
270         public String principal;
271         /**
272          * client delegated credential
273          */
274         public GSSCredential clientCredential;
275     }
276 
277     /**
278      * Check client provided Kerberos token in server login context
279      *
280      * @param serverLoginContext server login context
281      * @param token              Kerberos client token
282      * @return result with client principal and optional returned Kerberos token
283      * @throws GSSException on error
284      */
285     public static SecurityContext acceptSecurityContext(LoginContext serverLoginContext, final byte[] token) throws GSSException {
286         Object result = Subject.doAs(serverLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
287             Object innerResult;
288             SecurityContext securityContext = new SecurityContext();
289             GSSContext context = null;
290             try {
291                 GSSManager manager = GSSManager.getInstance();
292 
293                 // get server credentials from context
294                 Oid krb5oid = new Oid("1.2.840.113554.1.2.2");
295                 GSSCredential serverCreds = manager.createCredential(null/* use name from login context*/,
296                         GSSCredential.DEFAULT_LIFETIME,
297                         krb5oid,
298                         GSSCredential.ACCEPT_ONLY/* server mode */);
299                 context = manager.createContext(serverCreds);
300 
301                 securityContext.token = context.acceptSecContext(token, 0, token.length);
302                 if (context.isEstablished()) {
303                     securityContext.principal = context.getSrcName().toString();
304                     LOGGER.debug("Authenticated user: " + securityContext.principal);
305                     if (!context.getCredDelegState()) {
306                         LOGGER.debug("Credentials can not be delegated");
307                     } else {
308                         // Get client delegated credentials from context (gateway mode)
309                         securityContext.clientCredential = context.getDelegCred();
310                     }
311                 }
312                 innerResult = securityContext;
313             } catch (GSSException e) {
314                 innerResult = e;
315             } finally {
316                 if (context != null) {
317                     try {
318                         context.dispose();
319                     } catch (GSSException e) {
320                         LOGGER.debug("KerberosHelper.acceptSecurityContext " + e + ' ' + e.getMessage());
321                     }
322                 }
323             }
324             return innerResult;
325         });
326         if (result instanceof GSSException) {
327             LOGGER.info("KerberosHelper.acceptSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
328             throw (GSSException) result;
329         }
330         return (SecurityContext) result;
331     }
332 }