View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    *
4    * This program is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU General Public License as published by the Free
6    * Software Foundation; either version 2 of the License, or (at your option)
7    * any later version.
8    *
9    * This program is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12   * more details.
13   *
14   * You should have received a copy of the GNU General Public License along with
15   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16   * Place - Suite 330, Boston, MA 02111-1307, USA.
17   *
18   * $Id: MoriaController.java,v 1.82 2006/02/27 12:02:22 catoolsen Exp $
19   */
20  
21  package no.feide.moria.controller;
22  
23  import java.net.URI;
24  import java.net.URISyntaxException;
25  import java.util.Arrays;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Properties;
31  
32  import javax.servlet.ServletContext;
33  
34  import no.feide.moria.authorization.AuthorizationManager;
35  import no.feide.moria.authorization.UnknownAttributeException;
36  import no.feide.moria.authorization.UnknownServicePrincipalException;
37  import no.feide.moria.configuration.ConfigurationManager;
38  import no.feide.moria.configuration.ConfigurationManagerException;
39  import no.feide.moria.directory.Credentials;
40  import no.feide.moria.directory.DirectoryManager;
41  import no.feide.moria.directory.backend.AuthenticationFailedException;
42  import no.feide.moria.directory.backend.BackendException;
43  // import no.feide.moria.ldap.SimpleLdapServer;
44  import no.feide.moria.log.AccessLogger;
45  import no.feide.moria.log.AccessStatusType;
46  import no.feide.moria.log.MessageLogger;
47  import no.feide.moria.store.InvalidTicketException;
48  import no.feide.moria.store.MoriaAuthnAttempt;
49  import no.feide.moria.store.MoriaStore;
50  import no.feide.moria.store.MoriaTicketType;
51  import no.feide.moria.store.MoriaStoreConfigurationException;
52  import no.feide.moria.store.MoriaStoreException;
53  import no.feide.moria.store.MoriaStoreFactory;
54  import no.feide.moria.store.NonExistentTicketException;
55  
56  import org.apache.log4j.Level;
57  
58  /***
59   * Intermediator for the sub modules of Moria. The controller is the only entry
60   * point for accessing Moria. Basically, all work is done by the authorization
61   * module, the distributed store, the directory manager and the logger. The
62   * controller must be initialized from the servlets that are using it. This can
63   * be done by calling the <code>initController</code> method.
64   * @author Lars Preben S. Arnesen &lt;lars.preben.arnesen@conduct.no&gt;
65   * @version $Revision: 1.82 $
66   * @see MoriaController#initController(javax.servlet.ServletContext)
67   */
68  public final class MoriaController {
69  
70      /***
71       * Ticket type constant, indicating an SSO ticket, for use when returning a
72       * HashMap of two tickets.
73       * @see MoriaController#attemptLogin(java.lang.String, java.lang.String,
74       *      java.lang.String, java.lang.String, boolean)
75       * @see MoriaController#attemptSingleSignOn(java.lang.String,
76       *      java.lang.String)
77       */
78      public static final String SSO_TICKET = "sso";
79  
80      /***
81       * Ticket type constant, indicating a login ticket, for use when returning a
82       * HashMap with multiple tickets.
83       * @see MoriaController#attemptLogin(java.lang.String, java.lang.String,
84       *      java.lang.String, java.lang.String, boolean)
85       * @see MoriaController#attemptSingleSignOn(java.lang.String,
86       *      java.lang.String)
87       */
88      public static final String SERVICE_TICKET = "service";
89  
90      /***
91       * Operation type for local authentication.
92       */
93      private static final String DIRECT_AUTH_OPER = "DirectAuth";
94  
95      /***
96       * Operation type for interactive authentication.
97       */
98      private static final String INTERACTIVE_AUTH_OPER = "InteractiveAuth";
99  
100     /***
101      * Operation type for interactive authentication.
102      */
103     private static final String PROXY_AUTH_OPER = "ProxyAuth";
104 
105     /***
106      * Operation type for verify user existence.
107      */
108     private static final String VERIFY_USER_EXISTENCE_OPER = "VerifyUserExistence";
109 
110     /***
111      * Identifier for the TGT used in attribute requests.
112      */
113     static final String TGT_IDENTIFIER = "tgt";
114 
115     /***
116      * Standard exception message for indication that the store is unavailable.
117      */
118     private static final String STORE_DOWN = "Moria is unavailable, the store is down.";
119 
120     /***
121      * Standard exception message for indication that the controller is not
122      * ready.
123      */
124     private static final String NOT_READY = "Moria is unavailable, the controller is not ready.";
125 
126     /***
127      * Standard exception message for indication that ticket does not exist.
128      */
129     private static final String NONEXISTENT_TICKET = "Ticket does not exist.";
130 
131     /***
132      * Standard log message for NonExistentTicketException.
133      */
134     private static final String CAUGHT_NONEXISTENT_TICKET = "NonExistentTicketException caught";
135 
136     /***
137      * Standard log message for InvalidTicketException.
138      */
139     private static final String CAUGHT_INVALID_TICKET = "InvalidTicketException caught";
140 
141     /***
142      * Standard log message for InvalidTicketException.
143      */
144     private static final String CAUGHT_STORE = "MoriaStoreException caught";
145 
146     /***
147      * Log message for AuthorizationException.
148      */
149     private static final String CAUGHT_DENIED_USERORG = "AuthorizationException caught";
150 
151     /***
152      * The single instance of the data store.
153      */
154     private static MoriaStore store;
155 
156     /***
157      * The single instance of the configuration manager.
158      */
159     private static ConfigurationManager configManager;
160 
161     /***
162      * The single instance of the authorization manager.
163      */
164     private static AuthorizationManager authzManager;
165 
166     /***
167      * The single instance of the directory manager.
168      */
169     private static DirectoryManager directoryManager;
170 
171     /***
172      * Flag set to true if the controller has been initialized.
173      */
174     private static Boolean isInitialized = new Boolean(false);
175 
176     /***
177      * Flag set to true if the controller and all modules are ready.
178      */
179     private static boolean ready = false;
180 
181     /***
182      * Flag set to true if the authorization manager is ready.
183      */
184     private static boolean amReady = false;
185 
186     /***
187      * Flag set to true if the directory manager is ready.
188      */
189     private static boolean dmReady = false;
190 
191     /***
192      * Flag set to true if the store manager is ready.
193      */
194     private static boolean smReady = false;
195 
196     /***
197      * The servlet context for the servlets using the controller.
198      */
199     private static ServletContext servletContext;
200 
201     /***
202      * Used for access logging.
203      */
204     private static AccessLogger accessLogger;
205 
206     /***
207      * Used for message/error logging.
208      */
209     private static MessageLogger messageLogger;
210 
211 
212     /***
213      * Private constructor. Never to be used.
214      */
215     private MoriaController() {
216 
217         // Never to be used; no action taken.
218 
219     }
220 
221 
222     /***
223      * Initiates the controller. The initialization includes the initialization
224      * of all sub modules.
225      * @throws InoperableStateException
226      *             If Moria is not ready for use.
227      */
228     static synchronized void init() throws InoperableStateException {
229 
230         synchronized (isInitialized) {
231 
232             /* Only run once */
233             if (isInitialized.booleanValue()) { return; }
234             isInitialized = new Boolean(true);
235 
236             /* Logger */
237             messageLogger = new MessageLogger(MoriaController.class);
238             accessLogger = new AccessLogger();
239 
240             /* Store */
241             try {
242                 store = MoriaStoreFactory.createMoriaStore();
243             } catch (MoriaStoreException e) {
244                 messageLogger.logCritical("Store failed to start", e);
245                 throw new InoperableStateException("Moria cannot start, the store is unavailable.");
246             }
247 
248             /* Authorization */
249             authzManager = new AuthorizationManager();
250 
251             /* Directory */
252             directoryManager = new DirectoryManager();
253 
254             /* Configuration manager */
255             try {
256                 configManager = new ConfigurationManager();
257             } catch (ConfigurationManagerException e) {
258                 messageLogger.logCritical("Moria cannot start, configuration failed.", e);
259                 throw new InoperableStateException("Moria cannot start, configuration failed. " + e.getMessage());
260             }
261 
262             // Starting SimpleLdapServer on (hard-coded) port 389.
263             // SimpleLdapServer.start(389);
264 
265         }
266     }
267 
268 
269     /***
270      * Shuts down the controller. All ready status fields are set to false.
271      */
272     static synchronized void stop() {
273 
274         synchronized (isInitialized) {
275             if (ready) {
276 
277                 // Shutting down SimpleLdapServer.
278                 // SimpleLdapServer.stop();
279 
280                 authzManager = null;
281                 amReady = false;
282                 configManager.stop();
283                 configManager = null;
284                 directoryManager.stop();
285                 directoryManager = null;
286                 dmReady = false;
287                 store.stop();
288                 store = null;
289                 smReady = false;
290                 servletContext = null;
291                 ready = false;
292                 isInitialized = new Boolean(false);
293             }
294         }
295     }
296 
297 
298     /***
299      * Gets the total status of the controller. The method returns a HashMap
300      * with Boolean values. The following elements are in the map:
301      * <ul>
302      * <li>init: <code>true</code> if the <code>initController</code>
303      * method has been called, else <code>false</code>.
304      * <li>dm: <code>true</code> if the
305      * <code>DirectoryManager.setConfig</code> method has been called, else
306      * <code>false</code>.
307      * <li>sm: <code>true</code> if the <code>MoriaStore.setConfig</code>
308      * method has been called, else <code>false</code>.
309      * <li>am: <code>true</code> if the
310      * <code>AuthorizationManager.setConfig</code> method has been called,
311      * else <code>false</code>.
312      * <li>moria: <code>true</code> all the above are true (the controller is
313      * ready to use).
314      * </ul>
315      * @return A <code>HashMap</code> with all status fields for the
316      *         controller (<code>init</code>,<code>dm</code>,
317      *         <code>sm</code>,<code>am</code> and <code>moria</code>).
318      * @see MoriaController#initController(javax.servlet.ServletContext)
319      * @see DirectoryManager#setConfig(java.util.Properties)
320      * @see MoriaStore#setConfig(java.util.Properties)
321      * @see AuthorizationManager#setConfig(java.util.Properties)
322      */
323     public static HashMap getStatus() {
324 
325         final HashMap totalStatus = new HashMap();
326         totalStatus.put("init", isInitialized);
327         totalStatus.put("dm", new Boolean(dmReady));
328         totalStatus.put("sm", new Boolean(smReady));
329         totalStatus.put("am", new Boolean(amReady));
330         totalStatus.put("moria", new Boolean(ready));
331 
332         return totalStatus;
333     }
334 
335 
336     /***
337      * Attempts single sign on (non-interactive) with an SSO ticket together
338      * with the login ticket. If both tickets are valid and the requested
339      * attributes are cached, a service ticket is returned and there is no need
340      * to perform the regular interactive authentication.
341      * @param loginTicketId
342      *            The reference to the authentication attempt.
343      * @param ssoTicketId
344      *            The SSO ticket received from the users browser.
345      * @return A service ticket.
346      * @throws UnknownTicketException
347      *             If either the login ticket or the SSO ticket is invalid or
348      *             non-existing, the authetication attempt requires interactive
349      *             authentication, or the SSO ticket does not point to a cached
350      *             user data object with enough attributes.
351      * @throws InoperableStateException
352      *             If the controller is not ready.
353      * @throws IllegalInputException
354      *             If the <code>loginTicketId</code> and/or
355      *             <code>ssoTicketId</code> is null or empty.
356      * @throws UnknownServicePrincipalException
357      *             If the service principal cannot be resolved, in which case
358      *             there is probably an issue with the Authentication Module
359      *             configuration.
360      */
361     public static String attemptSingleSignOn(final String loginTicketId,
362                                              final String ssoTicketId)
363     throws UnknownTicketException, InoperableStateException,
364     IllegalInputException, UnknownServicePrincipalException {
365 
366         /* Check controller status */
367         if (!ready) { throw new InoperableStateException(NOT_READY); }
368 
369         /* Validate arguments */
370         if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalInputException("loginTicketId must be a non-empty string."); }
371         if (ssoTicketId == null || ssoTicketId.equals("")) { throw new IllegalInputException("ssoTicketId must be a non-empty string."); }
372 
373         /* Get authentication attempt */
374         final MoriaAuthnAttempt authnAttempt;
375         try {
376             authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
377 
378             // Warn when service attempts to get non-SSO attributes in SSO
379             // context.
380             final String servicePrincipal = authnAttempt.getServicePrincipal();
381             final String[] nonSSOAttributes = authzManager.getNonSSOAttributeNames(servicePrincipal);
382             final String[] requestedAttributes = authnAttempt.getRequestedAttributes();
383             String unavailableAttributes = "";
384             for (int i = 0; i < requestedAttributes.length; i++)
385                 for (int j = 0; j < nonSSOAttributes.length; j++)
386                     if (requestedAttributes[i].equalsIgnoreCase(nonSSOAttributes[j]))
387                         unavailableAttributes = unavailableAttributes + requestedAttributes[i] + ", ";
388             if (unavailableAttributes.length() > 0) {
389                 unavailableAttributes = unavailableAttributes.substring(0, unavailableAttributes.length() - 2);
390                 messageLogger.logWarn("Service '" + servicePrincipal + "' denied attributes in SSO context: [" + unavailableAttributes + "]", loginTicketId);
391             }
392 
393         } catch (InvalidTicketException e) {
394             accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, null, loginTicketId, null);
395             messageLogger.logWarn(CAUGHT_INVALID_TICKET, loginTicketId, e);
396             throw new UnknownTicketException(NONEXISTENT_TICKET);
397         } catch (NonExistentTicketException e) {
398             accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, null, loginTicketId, null);
399             messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, loginTicketId, e);
400             throw new UnknownTicketException(NONEXISTENT_TICKET);
401         } catch (MoriaStoreException e) {
402             messageLogger.logCritical(CAUGHT_STORE, loginTicketId, e);
403             throw new InoperableStateException(STORE_DOWN);
404         }
405 
406         /* Check if SSO is enabled in authentication attempt */
407         if (authnAttempt.isForceInterativeAuthentication()) {
408             messageLogger.logInfo("SSO authentication attempt denied by web service provider.");
409             throw new UnknownTicketException("Authentication attempt requires interactive authentication.");
410         }
411 
412         /* Service can only request cached attributes or SSO fails */
413         final String[] requestedAttributes = authnAttempt.getRequestedAttributes();
414         final HashSet cachedAttributes = authzManager.getCachableAttributes();
415         for (int i = 0; i < requestedAttributes.length; i++) {
416             if (!cachedAttributes.contains(requestedAttributes[i])) {
417                 messageLogger.logDebug("SSO authentication failed, request for non-cached attributes.");
418                 throw new UnknownTicketException("SSO ticket not sufficient, service requests uncached attributes.");
419             }
420         }
421 
422         /* Transfer attributes from cached user data to authentication attempt. */
423         final String serviceTicket;
424         try {
425             // Put transient (SSO-enabled) attributes into authentication
426             // attempt, removing non-SSO attributes in the process.
427             store.setTransientSSOAttributes(loginTicketId, ssoTicketId, authzManager.getNonSSOAttributeNames(authnAttempt.getServicePrincipal()));
428 
429             /* set loginTicket userorg from SSO ticket */
430             String userorg = store.getTicketUserorg(ssoTicketId, MoriaTicketType.SSO_TICKET);
431             store.setTicketUserorg(loginTicketId, MoriaTicketType.LOGIN_TICKET, userorg);
432 
433             /* Get service ticket */
434             serviceTicket = store.createServiceTicket(loginTicketId);
435         } catch (InvalidTicketException e) {
436             accessLogger.logUser(AccessStatusType.INVALID_SSO_TICKET, null, null, ssoTicketId, null);
437             messageLogger.logWarn(CAUGHT_INVALID_TICKET, ssoTicketId, e);
438             throw new UnknownTicketException(NONEXISTENT_TICKET);
439         } catch (NonExistentTicketException e) {
440             accessLogger.logUser(AccessStatusType.NONEXISTENT_SSO_TICKET, null, null, ssoTicketId, null);
441             messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, ssoTicketId, e);
442             throw new UnknownTicketException(NONEXISTENT_TICKET);
443         } catch (MoriaStoreException e) {
444             messageLogger.logCritical(CAUGHT_STORE, ssoTicketId, e);
445             throw new InoperableStateException(STORE_DOWN);
446         }
447 
448         accessLogger.logUser(AccessStatusType.SUCCESSFUL_SSO_AUTHENTICATION, authnAttempt.getServicePrincipal(), null, loginTicketId, serviceTicket);
449         return serviceTicket;
450     }
451 
452 
453     /***
454      * Performs interactive login attempt using tickets and credentials. The
455      * authentication is performed by the directory service, using the supplied
456      * username and password. All retrieved user data is cached in the
457      * authentication attempt, identified by the <code>loginTicketId</code>.
458      * A new cached userdata object is created and all cachable attributes are
459      * stored in it. The existing SSO ticket is removed. After a successful
460      * authentication a new service ticket, pointing to the same authentication
461      * attempt, is created. A new SSO ticket is created, pointing to the cached
462      * userdata object.
463      * @param loginTicketId
464      *            The ticket identifying the authentication attempt.
465      * @param ssoTicketId
466      *            The ticket identifying the existing cached user data object.
467      * @param userId
468      *            The user's userId.
469      * @param password
470      *            The user's password.
471      * @param denySSO
472      *            The user's SSO choice.
473      * @return A HashMap with two tickets: login and SSO, indexed with
474      *         <code>MoriaController.SSO_TICKET</code> and
475      *         <code>MoiraController.LOGIN_TICKET</code>.
476      * @throws UnknownTicketException
477      *             If the login ticket is invalid or does not exist.
478      * @throws InoperableStateException
479      *             If the controller is not ready to be used, or the store
480      *             cannot be accessed.
481      * @throws IllegalInputException
482      *             If any of <code>loginTicketId</code>,<code>userId</code>,
483      *             or <code>password</code> are <code>null</code> or an
484      *             empty string.
485      * @throws AuthenticationException
486      *             If the authentication failed due to wrong credentials.
487      * @throws AuthorizationException
488      *             If the user's organization is not allowed to use this service
489      * @throws DirectoryUnavailableException
490      *             If the directory of the user's home organization is
491      *             unavailable.
492      */
493     public static Map attemptLogin(final String loginTicketId,
494                                    final String ssoTicketId,
495                                    final String userId,
496                                    final String password,
497                                    final boolean denySSO)
498     throws UnknownTicketException, InoperableStateException,
499     IllegalInputException, AuthenticationException,
500     DirectoryUnavailableException, AuthorizationException {
501 
502         // Sanity checks.
503         if (!ready)
504             throw new InoperableStateException(NOT_READY);
505         if (loginTicketId == null || loginTicketId.equals(""))
506             throw new IllegalInputException("Login ticket ID must be a non-empty string");
507         if (userId == null || userId.equals(""))
508             throw new IllegalInputException("User ID must be a non-empty string");
509         if (password == null || password.equals(""))
510             throw new IllegalInputException("Password must be a non-empty string");
511 
512         // Look up authentication attempt from the store.
513         final MoriaAuthnAttempt authnAttempt;
514         String userorg = null;
515         try {
516             String servicePrincipal = store.getTicketServicePrincipal(loginTicketId, MoriaTicketType.LOGIN_TICKET);
517             userorg = getUserOrg(userId);
518             // remember userorg for this ticket
519             store.setTicketUserorg(loginTicketId, MoriaTicketType.LOGIN_TICKET, userorg);
520             /* check userorg */
521             if (!authzManager.allowUserorg(servicePrincipal, userorg)) { throw new AuthorizationException("Access to the requested service is denied for " + userorg + "."); }
522             authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
523         } catch (NonExistentTicketException e) {
524             accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, userId, loginTicketId, null);
525             messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET, loginTicketId, e);
526             throw new UnknownTicketException(NONEXISTENT_TICKET);
527         } catch (InvalidTicketException e) {
528             accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, userId, loginTicketId, null);
529             messageLogger.logWarn(CAUGHT_INVALID_TICKET, loginTicketId, e);
530             throw new UnknownTicketException(NONEXISTENT_TICKET);
531         } catch (MoriaStoreException e) {
532             messageLogger.logCritical(CAUGHT_STORE, loginTicketId, e);
533             throw new InoperableStateException(STORE_DOWN);
534         } catch (UnknownServicePrincipalException e) {
535             // Should not happen.
536             throw new AuthorizationException("Access to the requested service is denied for " + userorg + ".");
537         }
538 
539         // TODO: Must be done for directNonInteractiveAuthentication. Should be
540         // extracted to a method.
541 
542         // Parse requested attributes and extract special attributes.
543         final String[] requestedAttributes = authnAttempt.getRequestedAttributes();
544         final HashSet parsedRequestAttributes = new HashSet();
545         boolean appendTGT = false;
546 
547         // TGT is only granted when users use SSO
548         for (int i = 0; i < requestedAttributes.length; i++) {
549             if (requestedAttributes[i].equals(TGT_IDENTIFIER) && !denySSO) {
550                 appendTGT = true;
551             } else {
552                 parsedRequestAttributes.add(requestedAttributes[i]);
553             }
554         }
555 
556         // Resolve list of cached and requested attributes.
557         final HashSet cachableAttributes = authzManager.getCachableAttributes();
558         final HashSet retrieveAttributes = new HashSet(cachableAttributes);
559         retrieveAttributes.addAll(parsedRequestAttributes);
560 
561         // Authentication.
562         final HashMap fetchedAttributes;
563         try {
564             fetchedAttributes = authenticate(loginTicketId, new Credentials(userId, password), (String[]) retrieveAttributes.toArray(new String[retrieveAttributes.size()]));
565         } catch (AuthenticationFailedException e) {
566             accessLogger.logUser(AccessStatusType.BAD_USER_CREDENTIALS, null, userId, loginTicketId, null);
567             messageLogger.logDebug("AuthenticationFailedException caught", loginTicketId, e);
568             throw new AuthenticationException();
569         } catch (BackendException e) {
570             messageLogger.logWarn("BackendException caught", loginTicketId, e);
571             throw new DirectoryUnavailableException();
572         }
573 
574         // Remove any existing SSO ticket.
575         if (ssoTicketId != null && !ssoTicketId.equals("")) {
576             try {
577                 store.removeSSOTicket(ssoTicketId);
578             } catch (NonExistentTicketException e) {
579                 // The ticket has probably already timed out.
580                 messageLogger.logDebug("SSO ticket does not exist - may have timed out", ssoTicketId, e);
581             } catch (MoriaStoreException e) {
582                 // Unable to access the store.
583                 messageLogger.logCritical(CAUGHT_STORE, ssoTicketId, e);
584                 throw new InoperableStateException(STORE_DOWN);
585             }
586         }
587 
588         // Cache attributes and get tickets.
589         final String serviceTicketId;
590         final String newSSOTicketId;
591         final HashMap cacheAttributes = new HashMap();
592         final HashMap authnAttemptAttrs = new HashMap();
593 
594         final Iterator it = cachableAttributes.iterator();
595         while (it.hasNext()) {
596             final String attrName = (String) it.next();
597             cacheAttributes.put(attrName, fetchedAttributes.get(attrName));
598         }
599 
600         for (int i = 0; i < requestedAttributes.length; i++) {
601             authnAttemptAttrs.put(requestedAttributes[i], fetchedAttributes.get(requestedAttributes[i]));
602         }
603 
604         try {
605             newSSOTicketId = store.cacheUserData(cacheAttributes, userorg);
606             store.setTicketUserorg(newSSOTicketId, MoriaTicketType.SSO_TICKET, userorg);
607             if (appendTGT) {
608                 authnAttemptAttrs.put(TGT_IDENTIFIER, store.createTicketGrantingTicket(newSSOTicketId, authnAttempt.getServicePrincipal()));
609             }
610             store.setTransientAttributes(loginTicketId, authnAttemptAttrs);
611             serviceTicketId = store.createServiceTicket(loginTicketId);
612             store.setTicketUserorg(serviceTicketId, MoriaTicketType.SERVICE_TICKET, userorg);
613 
614         } catch (NonExistentTicketException e) {
615             /* Should not happen due to previous validation in this method */
616             messageLogger.logWarn(CAUGHT_NONEXISTENT_TICKET + ", should not happen (already validated)", loginTicketId, e);
617             throw new UnknownTicketException(NONEXISTENT_TICKET);
618         } catch (InvalidTicketException e) {
619             messageLogger.logWarn(CAUGHT_INVALID_TICKET + ", should not happen (already validated)", loginTicketId, e);
620             throw new UnknownTicketException(NONEXISTENT_TICKET);
621         } catch (MoriaStoreException e) {
622             messageLogger.logCritical(CAUGHT_STORE, ssoTicketId, e);
623             throw new InoperableStateException(STORE_DOWN);
624         }
625 
626         /* Return tickets */
627         final HashMap tickets = new HashMap();
628         tickets.put(SERVICE_TICKET, serviceTicketId);
629         tickets.put(SSO_TICKET, newSSOTicketId);
630 
631         accessLogger.logUser(AccessStatusType.SUCCESSFUL_INTERACTIVE_AUTHENTICATION, authnAttempt.getServicePrincipal(), userId, loginTicketId, "Service: " + serviceTicketId + " SSO: " + ssoTicketId);
632 
633         return tickets;
634     }
635 
636 
637     /***
638      * Initiates authentication through Moria. An authentication attempt is
639      * created and the supplied argument is stored in it for later use. After a
640      * successful authentication, the user is redirected back to a URL
641      * consisting of the URL prefix and postfix, with the service ticket added
642      * in the middle.
643      * @param attributes
644      *            The requested attributes. Cannot be <code>null</code>.
645      * @param returnURLPrefix
646      *            Prefix of the redirect URL, used to direct the user back to
647      *            the web service. Cannot be <code>null</code> or an empty
648      *            string.
649      * @param returnURLPostfix
650      *            Postfix of the redirect URL, used to direct the user back to
651      *            the web service. Cannot be <code>null</code>.
652      * @param forceInteractiveAuthentication
653      *            If <code>true</code>, do not use SSO.
654      * @param servicePrincipal
655      *            The principal of the requesting service. Cannot be
656      *            <code>null</code> or an empty string.
657      * @return A login ticket ID.
658      * @throws AuthorizationException
659      *             If the service requests attributes it is not authorized to
660      *             receive.
661      * @throws IllegalInputException
662      *             If <code>attributes</code> or <code>returnURLPostfix</code>
663      *             is <code>null</code>, or <code>returnURLPrefix</code> or
664      *             <code>servicePrincipal</code> is <code>null</code> or an
665      *             empty string.
666      * @throws InoperableStateException
667      *             If the controller is not yet ready for use, or if the store
668      *             cannot be accessed at this time.
669      */
670     public static String initiateAuthentication(final String[] attributes,
671                                                 final String returnURLPrefix,
672                                                 final String returnURLPostfix,
673                                                 final boolean forceInteractiveAuthentication,
674                                                 final String servicePrincipal)
675     throws AuthorizationException, IllegalInputException,
676     InoperableStateException {
677 
678         // Is the controller ready?
679         if (!ready)
680             throw new InoperableStateException("Controller is not ready");
681 
682         // Sanity checks.
683         if (servicePrincipal == null || servicePrincipal.equals("")) {
684             messageLogger.logCritical("Missing service principal - check service container configuration");
685             throw new IllegalInputException("Service principal cannot be null or an empty string");
686         }
687         if (attributes == null)
688             throw new IllegalInputException("Attributes cannot be null");
689         if (returnURLPrefix == null || returnURLPrefix.equals(""))
690             throw new IllegalInputException("URL prefix cannot be null or an empty string");
691         if (returnURLPostfix == null)
692             throw new IllegalInputException("URL postfix cannot be null");
693 
694         // Check authorization for this service.
695         authorizationCheck(servicePrincipal, attributes, INTERACTIVE_AUTH_OPER);
696 
697         // Validate the URL pre- and postfix.
698         final String validationURL = returnURLPrefix + "FakeMoriaID" + "urlPostfix";
699         if (!(isLegalURL(validationURL))) {
700             accessLogger.logService(AccessStatusType.INITIATE_DENIED_INVALID_URL, servicePrincipal, null, null);
701             messageLogger.logWarn("Service '" + servicePrincipal + "' tried to submit invalid URL '" + validationURL + "'");
702             throw new IllegalInputException("Service supplied an invalid URL");
703         }
704 
705         // Create authentication attempt.
706         final String loginTicketId;
707         try {
708             loginTicketId = store.createAuthnAttempt(attributes, returnURLPrefix, returnURLPostfix, forceInteractiveAuthentication, servicePrincipal);
709         } catch (MoriaStoreException e) {
710             messageLogger.logCritical(CAUGHT_STORE, e);
711             throw new InoperableStateException(STORE_DOWN);
712         }
713 
714         // Log successful authentication initialization, and return the ticket
715         // ID.
716         accessLogger.logService(AccessStatusType.SUCCESSFUL_AUTH_INIT, servicePrincipal, null, loginTicketId);
717 
718         return loginTicketId;
719     }
720 
721 
722     /***
723      * Performs an authorization validation of a service request; can the
724      * service perform the requested operation? If no exception is thrown, the
725      * authorization was successful.
726      * @param servicePrincipal
727      *            The principal for the service performing the request. Must be
728      *            a non-empty string.
729      * @param attributes
730      *            The requested attributes, if any.
731      * @param operation
732      *            The requested operation. Must be a non-empty string.
733      * @throws AuthorizationException
734      *             If the authorization failed, for some reason.
735      * @throws IllegalArgumentException
736      *             If <code>servicePrincipal</code> is an empty string, or
737      *             <code>operation</code> is unknown or <code>null</code>.
738      */
739     private static void authorizationCheck(final String servicePrincipal,
740                                            final String[] attributes,
741                                            final String operation)
742     throws AuthorizationException {
743 
744         // Sanity checks.
745         if (servicePrincipal == null || servicePrincipal.equals(""))
746             throw new IllegalArgumentException("Service principal must be a non-empty string");
747         if (operation == null || operation.length() == 0)
748             throw new IllegalArgumentException("Operation must be a non-empty string");
749 
750         // Set status type, in case something has to be logged.
751         final AccessStatusType statusType;
752         if (operation == DIRECT_AUTH_OPER)
753             statusType = AccessStatusType.ACCESS_DENIED_DIRECT_AUTH;
754         else if (operation.equals(INTERACTIVE_AUTH_OPER))
755             statusType = AccessStatusType.ACCESS_DENIED_INITIATE_AUTH;
756         else if (operation.equals(PROXY_AUTH_OPER))
757             statusType = AccessStatusType.ACCESS_DENIED_PROXY_AUTH;
758         else if (operation.equals(VERIFY_USER_EXISTENCE_OPER))
759             statusType = AccessStatusType.ACCESS_DENIED_VERIFY_USER_EXISTENCE;
760         else
761             throw new IllegalArgumentException("Unknown operation '" + operation + "' attempted");
762 
763         try {
764 
765             // Check operations.
766             if (!authzManager.allowOperations(servicePrincipal, new String[] {operation})) {
767                 accessLogger.logService(statusType, servicePrincipal, null, null);
768                 messageLogger.logInfo("Service '" + servicePrincipal + "' tried to perform '" + operation + "', but can only do '" + authzManager.getOperations(servicePrincipal));
769                 throw new AuthorizationException("Access to the requested operation is denied");
770             }
771 
772             // Check attributes.
773             if (!authzManager.allowAccessTo(servicePrincipal, attributes)) {
774 
775                 // Resolve offending attributes.
776                 HashSet deniedAttributes = new HashSet(Arrays.asList(attributes));
777                 final HashSet allowedAttributes = authzManager.getAttributes(servicePrincipal);
778                 Iterator i = allowedAttributes.iterator();
779                 while (i.hasNext())
780                     deniedAttributes.remove(i.next());
781 
782                 accessLogger.logService(statusType, servicePrincipal, null, null);
783                 messageLogger.logInfo("Service \"" + servicePrincipal + "\" does not have access to attribute(s) " + deniedAttributes + "");
784                 throw new AuthorizationException("Access to the requested attribute(s) is denied");
785             }
786 
787         } catch (UnknownServicePrincipalException e) {
788 
789             // Unknown service.
790             messageLogger.logWarn("UnknownServicePrincipalException caught during authorizationCheck, " + "service probably not configured in authorization database.", e);
791             throw new AuthorizationException("Authorization failed for service '" + servicePrincipal + "'");
792 
793         }
794     }
795 
796 
797     /***
798      * Checks whether the user's organization is allowed to use the service in
799      * question. If no exception is thrown, this is allowed.
800      * @param servicePrincipal
801      *            The principal for the service performing the request.
802      * @param userOrganization
803      *            The organization the user comes from. Must be a non-empty
804      *            string.
805      * @throws AuthorizationException
806      *             If the user is not allowed to use this service.
807      * @throws IllegalArgumentException
808      *             If <code>servicePrincipal</code> is an empty string.
809      */
810     private static void organizationCheck(final String servicePrincipal,
811                                           final String userOrganization)
812     throws AuthorizationException {
813 
814         // Sanity check.
815         if (servicePrincipal == null || servicePrincipal.equals(""))
816             throw new IllegalArgumentException("Service principal must be a non-empty string");
817 
818         try {
819 
820             // Check whether the service is accessible for users from this
821             // user's organization.
822             if (!isOrganizationAllowedForService(servicePrincipal, userOrganization)) {
823 
824                 // Access denied.
825                 accessLogger.logService(AccessStatusType.ACCESS_DENIED_USERORG, servicePrincipal, null, null);
826                 messageLogger.logInfo("Access to the service '" + servicePrincipal + "' is denied for the organization '" + userOrganization + "'");
827                 throw new AuthorizationException("Access to the service '" + servicePrincipal + "' is denied for the organization '" + userOrganization + "'");
828 
829             }
830 
831         } catch (UnknownServicePrincipalException e) {
832 
833             // Unknown service.
834             messageLogger.logWarn("Unknown service '" + servicePrincipal + "'");
835             throw new AuthorizationException("Unknown service '" + servicePrincipal + "'");
836 
837         }
838     }
839 
840 
841     /***
842      * Check whether a given service may allow users from a given organization.
843      * @param servicePrincipal
844      *            The service's unique principal.
845      * @param userOrganization
846      *            The home organization, in short form.
847      * @return <code>true</code> if users from this organization can access
848      *         this service.
849      * @throws IllegalArgumentException
850      *             If <code>servicePrincipal</code> or
851      *             <code>userOrganization</code> is <code>null</code> or an
852      *             empty string.
853      * @throws UnknownServicePrincipalException
854      *             If <code>servicePrincipal</code> is unknown.
855      */
856     public static boolean isOrganizationAllowedForService(final String servicePrincipal,
857                                                           final String userOrganization)
858     throws IllegalArgumentException, UnknownServicePrincipalException {
859 
860         if (servicePrincipal == null || servicePrincipal.equals(""))
861             throw new IllegalArgumentException("Service principal must be a non-empty string");
862         if (userOrganization == null || userOrganization.equals(""))
863             throw new IllegalArgumentException("User organization must be a non-empty string");
864 
865         // Can this organization use this service?
866         return (userOrganization != null && authzManager.allowUserorg(servicePrincipal, userOrganization));
867 
868     }
869 
870 
871     /***
872      * Gets the name of the attributes a service requests, based on the
873      * loginTicket.
874      * @param loginTicket
875      *            the login ticket
876      * @param servicePrincipal
877      *            the name of the service that requested the attributes
878      * @return An array with attribute names.
879      * @throws IllegalInputException
880      * @throws UnknownTicketException
881      * @throws InoperableStateException
882      * @throws AuthorizationException
883      */
884     public static String[] getRequestedAttributes(final String loginTicket,
885                                                   final String servicePrincipal)
886     throws IllegalInputException, UnknownTicketException,
887     InoperableStateException, AuthorizationException {
888 
889         // Sanity checks.
890         if (!ready)
891             throw new InoperableStateException(NOT_READY);
892         if (loginTicket == null || loginTicket.equals(""))
893             throw new IllegalInputException("Login ticket ID must be a non-empty string.");
894         if (servicePrincipal == null || servicePrincipal.equals(""))
895             throw new IllegalInputException("Service principal must be a non-empty string.");
896 
897         String[] attr = null;
898         try {
899             attr = store.getAuthnAttempt(loginTicket, true, servicePrincipal).getRequestedAttributes();
900         } catch (MoriaStoreException e) {
901             messageLogger.logCritical(CAUGHT_STORE, e);
902             throw new InoperableStateException(STORE_DOWN);
903         } catch (InvalidTicketException e) {
904             throw new UnknownTicketException(NONEXISTENT_TICKET);
905         } catch (NonExistentTicketException e) {
906             throw new UnknownTicketException(NONEXISTENT_TICKET);
907         }
908 
909         return attr;
910     }
911 
912 
913     /***
914      * Retrieves user attributes from an authentication attempt. The method
915      * returns the user attributes stored in the authentication attempt, which
916      * is referenced to by the service ticket. <br>
917      * <br>
918      * Note that this method can only be used once for each non-SSO
919      * authentication attempt. For security reasons, Moria will not cache
920      * attribute values longer than absolutely necessary.
921      * @param serviceTicketId
922      *            The ticket associated with the authentication attempt. Cannot
923      *            be <code>null</code> or an empty string.
924      * @param servicePrincipal
925      *            The principal of the calling service. Cannot be
926      *            <code>null</code> or an empty string.
927      * @return A newly instantiated <code>Map</code> object containing the
928      *         requested user attributes, if found. Entries have a
929      *         <code>String</code> key and a <code>String[]</code> value.
930      * @throws AuthorizationException
931      *             If userorg isn't set for ticket, userorg is denied access to
932      *             the service or service principal is unknown.
933      * @throws IllegalInputException
934      *             If <code>serviceTicketId</code> or
935      *             <code>servicePrincipal</code> is <code>null</code> or an
936      *             empty string.
937      * @throws UnknownTicketException
938      *             If the service ticket does not exist in the store, or is
939      *             invalid.
940      * @throws InoperableStateException
941      *             If Moria is not ready for use.
942      */
943     public static Map getUserAttributes(final String serviceTicketId,
944                                         final String servicePrincipal)
945     throws IllegalInputException, UnknownTicketException,
946     InoperableStateException, AuthorizationException {
947 
948         // Sanity checks.
949         if (!ready)
950             throw new InoperableStateException(NOT_READY);
951         if (serviceTicketId == null || serviceTicketId.equals(""))
952             throw new IllegalInputException("Service ticket ID must be a non-empty string.");
953         if (servicePrincipal == null || servicePrincipal.equals(""))
954             throw new IllegalInputException("Service principal must be a non-empty string.");
955 
956         // Look up and return the requested attributes.
957         HashMap filteredAttributes = new HashMap();
958         try {
959             String userorg = null;
960             userorg = store.getTicketUserorg(serviceTicketId, MoriaTicketType.SERVICE_TICKET);
961             if (userorg == null) { throw new AuthorizationException("Userorg is not set for ticket"); }
962             /* check userorg */
963             if (!authzManager.allowUserorg(servicePrincipal, userorg)) {
964                 accessLogger.logService(AccessStatusType.ACCESS_DENIED_USERORG, servicePrincipal, serviceTicketId, null);
965                 messageLogger.logWarn(CAUGHT_DENIED_USERORG + ", userorg (" + userorg + ") tried to access service (" + servicePrincipal + ")", serviceTicketId);
966                 throw new AuthorizationException("Access to the requested service is denied for " + userorg + ".");
967             }
968 
969             // Get the originally requested attributes and all cached values.
970             MoriaAuthnAttempt authenticationAttempt = store.getAuthnAttempt(serviceTicketId, false, servicePrincipal);
971             String[] requestedAttributes = authenticationAttempt.getRequestedAttributes();
972             final Map cachedAttributes = authenticationAttempt.getTransientAttributes();
973 
974             // Filter cached attributes; only those requested are to be
975             // returned.
976             for (int i = 0; i < requestedAttributes.length; i++) {
977                 if (cachedAttributes.containsKey(requestedAttributes[i]))
978                     filteredAttributes.put(requestedAttributes[i], cachedAttributes.get(requestedAttributes[i]));
979             }
980 
981             // The service principal is unknown.
982         } catch (UnknownServicePrincipalException e) {
983             accessLogger.logService(AccessStatusType.GET_USER_ATTRIBUTES_DENIED_INVALID_PRINCIPAL, servicePrincipal, serviceTicketId, null);
984             messageLogger.logInfo("UnknownServicePrincipalException caught", e);
985             throw new AuthorizationException("Unknown service principal: " + servicePrincipal);
986         } catch (NonExistentTicketException e) {
987 
988             // Ticket did not exist in the store.
989             accessLogger.logService(AccessStatusType.NONEXISTENT_SERVICE_TICKET, servicePrincipal, serviceTicketId, null);
990             messageLogger.logWarn(CAUGHT_NONEXISTENT_TICKET + ", service (" + servicePrincipal + ") tried to fetch attributes too late", serviceTicketId, e);
991             throw new UnknownTicketException(NONEXISTENT_TICKET);
992 
993         } catch (InvalidTicketException e) {
994 
995             // The ticket was found, but was invalid.
996             accessLogger.logService(AccessStatusType.INVALID_SERVICE_TICKET, servicePrincipal, serviceTicketId, null);
997             messageLogger.logWarn(CAUGHT_INVALID_TICKET + ", service (" + servicePrincipal + ") tried to fetch attributes too late", serviceTicketId, e);
998             throw new UnknownTicketException(NONEXISTENT_TICKET);
999 
1000         } catch (MoriaStoreException e) {
1001 
1002             // Unable to access the store.
1003             messageLogger.logCritical(CAUGHT_STORE, serviceTicketId, e);
1004             throw new InoperableStateException(STORE_DOWN);
1005 
1006         }
1007 
1008         // Return the filtered attributes only.
1009         accessLogger.logService(AccessStatusType.SUCCESSFUL_GET_ATTRIBUTES, servicePrincipal, serviceTicketId, null);
1010         return filteredAttributes;
1011     }
1012 
1013 
1014     /***
1015      * Performs a direct authentication without the use of tickets. The user is
1016      * authenticated directly against the backend, and the attributes retrieved
1017      * are returned to the caller.
1018      * @param requestedAttributes
1019      *            The requested attributes.
1020      * @param userId
1021      *            The user's username.
1022      * @param password
1023      *            The user's password.
1024      * @param servicePrincipal
1025      *            The principal (read: username) of the calling service.
1026      * @return Map containing user attributes with <code>String</code>
1027      *         (attribute name) as key and <code>String[]</code> (user
1028      *         attributes) as value.
1029      * @throws AuthorizationException
1030      *             If the service is not allowed to perform this operation, or
1031      *             if the user's organization does not allow the use of this
1032      *             service.
1033      * @throws IllegalInputException
1034      *             If <code>requestedAttributes</code> is <code>null</code>,
1035      *             or <code>userId</code>, <code>password</code>, or
1036      *             <code>servicePrincipal</code> is <code>null</code> or an
1037      *             empty string.
1038      * @throws InoperableStateException
1039      *             If Moria is not currently ready for use.
1040      * @throws AuthenticationException
1041      *             If the authentication failed due to bad user credentials.
1042      * @throws DirectoryUnavailableException
1043      *             If directory of the user's home organization is unavailable.
1044      */
1045     public static Map directNonInteractiveAuthentication(final String[] requestedAttributes,
1046                                                          final String userId,
1047                                                          final String password,
1048                                                          final String servicePrincipal)
1049     throws AuthorizationException, IllegalInputException,
1050     InoperableStateException, AuthenticationException,
1051     DirectoryUnavailableException {
1052 
1053         // Check controller state.
1054         if (!ready) { throw new InoperableStateException(NOT_READY); }
1055 
1056         // Sanity checks.
1057         if (requestedAttributes == null)
1058             throw new IllegalInputException("Attributes cannot be null");
1059         if (userId == null || userId.equals(""))
1060             throw new IllegalInputException("Username must be a non-empty string");
1061         if (password == null || password.equals(""))
1062             throw new IllegalInputException("User password must be a non-empty string");
1063         if (servicePrincipal == null || servicePrincipal.equals(""))
1064             throw new IllegalInputException("Service principal must be a non-empty string");
1065 
1066         // Can the service perform this operation?
1067         authorizationCheck(servicePrincipal, requestedAttributes, DIRECT_AUTH_OPER);
1068 
1069         // Does the user's organization allow this service?
1070         organizationCheck(servicePrincipal, getUserOrg(userId));
1071 
1072         /* Authenticate */
1073         final HashMap attributes;
1074         try {
1075             attributes = authenticate(null, new Credentials(userId, password), requestedAttributes);
1076         } catch (AuthenticationFailedException e) {
1077             accessLogger.logService(AccessStatusType.BAD_USER_CREDENTIALS, servicePrincipal, null, null);
1078             messageLogger.logInfo("AuthenticationFailedException caught", e);
1079             throw new AuthenticationException();
1080         } catch (BackendException e) {
1081             messageLogger.logWarn("Directory is unavailable. Tried to authenticate user: " + userId, e);
1082             throw new DirectoryUnavailableException();
1083         }
1084 
1085         // Log user access.
1086         accessLogger.logUser(AccessStatusType.SUCCESSFUL_DIRECT_AUTHENTICATION, servicePrincipal, userId, null, null);
1087 
1088         // Return the attributes (if any).d
1089         return attributes;
1090     }
1091 
1092 
1093     /***
1094      * Performs a ticket based proxy authentication. A proxy ticket and a set of
1095      * requested attributes are used to retrieve user data. Only cached userdata
1096      * can be retrieved.
1097      * @param requestedAttributes
1098      *            The requested attributes to retrieve.
1099      * @param proxyTicketId
1100      *            The proxy ticket connected with the cached user data.
1101      * @param servicePrincipal
1102      *            The principal of the requesting service.
1103      * @return Map containing user attributes with <code>String</code>
1104      *         (attribute name) as key and <code>String[]</code> (user
1105      *         attributes) as value.
1106      * @throws AuthorizationException
1107      *             If the service is not allowed to perform this operation, or
1108      *             if the user's organization does not allow the use of this
1109      *             service.
1110      * @throws IllegalInputException
1111      *             If <code>requestedAttributes</code> is null, or
1112      *             <code>proxyTicketId</code> or <code>servicePrincipal</code>
1113      *             is <code>null</code> or an empty string.
1114      * @throws InoperableStateException
1115      *             If the controller is not currently ready to use.
1116      * @throws UnknownTicketException
1117      *             If the proxy ticket is invalid or does not exist.
1118      */
1119     public static Map proxyAuthentication(final String[] requestedAttributes,
1120                                           final String proxyTicketId,
1121                                           final String servicePrincipal)
1122     throws AuthorizationException, IllegalInputException,
1123     InoperableStateException, UnknownTicketException {
1124 
1125         // Check controller state.
1126         if (!ready) { throw new InoperableStateException(NOT_READY); }
1127 
1128         // Sanity checks.
1129         if (requestedAttributes == null)
1130             throw new IllegalInputException("Requested attributes cannot be null");
1131         if (proxyTicketId == null || proxyTicketId.equals(""))
1132             throw new IllegalInputException("'proxyTicket' must be a non-empty string.");
1133         if (servicePrincipal == null || servicePrincipal.equals(""))
1134             throw new IllegalInputException("Service principal must be a non-empty string.");
1135 
1136         // Check service authorization.
1137         authorizationCheck(servicePrincipal, requestedAttributes, PROXY_AUTH_OPER);
1138 
1139         // Retrieve cached attributes.
1140         final HashMap result = new HashMap();
1141         final HashMap userData;
1142         final HashSet cachedAttributes = authzManager.getCachableAttributes();
1143 
1144         try {
1145 
1146             // Chech whether the user's organization allows this service.
1147             final String userOrg = store.getTicketUserorg(proxyTicketId, MoriaTicketType.PROXY_TICKET);
1148             if (userOrg == null)
1149                 throw new InvalidTicketException("User organization is not set in ticket");
1150             organizationCheck(servicePrincipal, userOrg);
1151 
1152             // Read user data.
1153             userData = store.getUserData(proxyTicketId, servicePrincipal).getAttributes();
1154 
1155         } catch (InvalidTicketException e) {
1156 
1157             // Invalid ticket.
1158             accessLogger.logService(AccessStatusType.INVALID_PROXY_TICKET, servicePrincipal, proxyTicketId, null);
1159             messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1160             throw new UnknownTicketException(NONEXISTENT_TICKET);
1161 
1162         } catch (NonExistentTicketException e) {
1163 
1164             // Non-existent ticket.
1165             accessLogger.logService(AccessStatusType.NONEXISTENT_PROXY_TICKET, servicePrincipal, proxyTicketId, null);
1166             messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET, e);
1167             throw new UnknownTicketException(NONEXISTENT_TICKET);
1168 
1169         } catch (MoriaStoreException e) {
1170 
1171             // Store problem.
1172             messageLogger.logCritical(CAUGHT_STORE, e);
1173             throw new InoperableStateException(STORE_DOWN);
1174 
1175         }
1176 
1177         /* Get requested, cached attributes */
1178         for (int i = 0; i < requestedAttributes.length; i++) {
1179             final String attr = requestedAttributes[i];
1180             if (!cachedAttributes.contains(attr)) {
1181                 accessLogger.logService(AccessStatusType.PROXY_AUTH_DENIED_UNCACHED_ATTRIBUTES, servicePrincipal, proxyTicketId, null);
1182                 messageLogger.logInfo("Service (proxy authentication)'" + servicePrincipal + "' requested '" + new HashSet(Arrays.asList(requestedAttributes)) + "', but only the following are cached: '" + cachedAttributes);
1183                 throw new AuthorizationException("Requested attributes is not cached: '" + attr + "'");
1184             }
1185             result.put(attr, userData.get(attr));
1186         }
1187 
1188         accessLogger.logService(AccessStatusType.SUCCESSFUL_PROXY_AUTHENTICATION, servicePrincipal, proxyTicketId, null);
1189         return result;
1190     }
1191 
1192 
1193     /***
1194      * Generates a proxy ticket based on a TGT. A new proxy ticket is created,
1195      * referring to the same cached user data as the TGT does. The proxy ticket
1196      * will be owned by the target service, not the one that requested its
1197      * creation.
1198      * @param ticketGrantingTicket
1199      *            The TGT to generate a proxy ticket for.
1200      * @param proxyServicePrincipal
1201      *            The principal of the service that the proxy ticket is created
1202      *            for.
1203      * @param servicePrincipal
1204      *            The principal of the service requesting the ticket generation.
1205      * @return A <code>String</code> containing the proxy ticket.
1206      * @throws AuthorizationException
1207      *             If the requesting service is not allowed to perform the
1208      *             operation, or if the user's organization does not allow the
1209      *             use of this service.
1210      * @throws IllegalInputException
1211      *             If <code>ticketGrantingTicket</code>,
1212      *             <code>proxyServicePrincipal</code> or
1213      *             <code>servicePrincipal</code> is <code>null</code> or an
1214      *             empty string.
1215      * @throws InoperableStateException
1216      *             If Moria is not currently ready for use.
1217      * @throws UnknownTicketException
1218      *             If the <code>ticketGrantingTicket</code> is invalid or does
1219      *             not exist, or <code>userorg</code> is not set in ticket.
1220      */
1221     public static String getProxyTicket(final String ticketGrantingTicket,
1222                                         final String proxyServicePrincipal,
1223                                         final String servicePrincipal)
1224     throws AuthorizationException, IllegalInputException,
1225     InoperableStateException, UnknownTicketException {
1226 
1227         // Check controller state.
1228         if (!ready)
1229             throw new InoperableStateException(NOT_READY);
1230 
1231         // Sanity checks.
1232         if (ticketGrantingTicket == null || ticketGrantingTicket.equals(""))
1233             throw new IllegalInputException("Ticket granting ticket must be a non-empty string");
1234         if (proxyServicePrincipal == null || proxyServicePrincipal.equals(""))
1235             throw new IllegalInputException("Proxy service principal must be a non-empty string.");
1236         if (servicePrincipal == null || servicePrincipal.equals(""))
1237             throw new IllegalInputException("Service principal must be a non-empty string");
1238 
1239         // Is this service allowed to create proxy tickets?
1240         authorizationCheck(servicePrincipal, new String[] {}, PROXY_AUTH_OPER);
1241 
1242         /* Return proxyTicket */
1243         final String proxyTicketId;
1244         try {
1245 
1246             // Check whether user's organization allows the use of this service.
1247             final String userOrg = store.getTicketUserorg(ticketGrantingTicket, MoriaTicketType.TICKET_GRANTING_TICKET);
1248             if (userOrg == null)
1249                 throw new UnknownTicketException("User organization is not set in ticket");
1250             organizationCheck(servicePrincipal, userOrg);
1251 
1252             try {
1253                 if (!authzManager.getSubsystems(servicePrincipal).contains(proxyServicePrincipal)) {
1254                     accessLogger.logService(AccessStatusType.PROXY_TICKET_GENERATION_DENIED_UNAUTHORIZED, servicePrincipal, ticketGrantingTicket, null);
1255                     throw new AuthorizationException("Request for proxy ticket denied.");
1256                 }
1257             } catch (UnknownServicePrincipalException e) {
1258                 accessLogger.logService(AccessStatusType.PROXY_TICKET_GENERATION_DENIED_INVALID_PRINCIPAL, servicePrincipal, ticketGrantingTicket, null);
1259                 messageLogger.logInfo("UnknownServicePrincipalException caught", e);
1260                 throw new AuthorizationException("Unknown service principal: " + servicePrincipal);
1261             }
1262             proxyTicketId = store.createProxyTicket(ticketGrantingTicket, servicePrincipal, proxyServicePrincipal);
1263         } catch (InvalidTicketException e) {
1264             accessLogger.logService(AccessStatusType.INVALID_TGT, servicePrincipal, ticketGrantingTicket, null);
1265             messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1266             throw new UnknownTicketException(NONEXISTENT_TICKET);
1267         } catch (NonExistentTicketException e) {
1268             accessLogger.logService(AccessStatusType.NONEXISTENT_TGT, servicePrincipal, ticketGrantingTicket, null);
1269             messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, e);
1270             throw new UnknownTicketException(NONEXISTENT_TICKET);
1271         } catch (MoriaStoreException e) {
1272             messageLogger.logCritical(CAUGHT_STORE, e);
1273             throw new InoperableStateException(STORE_DOWN);
1274         }
1275 
1276         accessLogger.logService(AccessStatusType.SUCCESSFUL_GET_PROXY_TICKET, servicePrincipal, ticketGrantingTicket, proxyTicketId);
1277         return proxyTicketId;
1278     }
1279 
1280 
1281     /***
1282      * Verifies the existence of a user.
1283      * @param userId
1284      *            The username to verify.
1285      * @param servicePrincipal
1286      *            The principal of the requesting service.
1287      * @return <code>true</code> if the user exists, otherwise
1288      *         <code>false</code>.
1289      * @throws AuthorizationException
1290      *             If the requesting service is not allowed to perform the
1291      *             operation, or if the user's organization does not allow the
1292      *             use of this service.
1293      * @throws IllegalInputException
1294      *             If <code>userId</code> or <code>servicePrincipal</code>
1295      *             is <code>null</code> or an empty string.
1296      * @throws InoperableStateException
1297      *             If the controller is not currently ready to use.
1298      * @throws DirectoryUnavailableException
1299      *             If the directory for the user is not available.
1300      */
1301     public static boolean verifyUserExistence(final String userId,
1302                                               final String servicePrincipal)
1303     throws AuthorizationException, IllegalInputException,
1304     InoperableStateException, DirectoryUnavailableException {
1305 
1306         // Check controller state.
1307         if (!ready) { throw new InoperableStateException(NOT_READY); }
1308 
1309         // Sanity checks.
1310         if (userId == null || userId.equals(""))
1311             throw new IllegalInputException("Username must be non-empty string");
1312         if (servicePrincipal == null || servicePrincipal.equals(""))
1313             throw new IllegalInputException("Service principal must be non-empty string");
1314 
1315         // Is the service allowed to check for user existence?
1316         authorizationCheck(servicePrincipal, new String[] {}, VERIFY_USER_EXISTENCE_OPER);
1317 
1318         // Does the user's organization allow the use of this service?
1319         String userOrganization = null;
1320         if (userId.indexOf("@") != -1)
1321             userOrganization = userId.substring(userId.indexOf("@") + 1, userId.length());
1322         if (userOrganization == null)
1323             throw new AuthorizationException("User organization is unknown");
1324         organizationCheck(servicePrincipal, userOrganization);
1325 
1326         /* Verify user (call DM) */
1327         final boolean userExistence;
1328         try {
1329             userExistence = directoryManager.userExists(null, userId);
1330         } catch (BackendException e) {
1331             messageLogger.logWarn("BackendException caught", e);
1332             throw new DirectoryUnavailableException();
1333         }
1334 
1335         /* Log */
1336         final String resultString;
1337         if (userExistence) {
1338             resultString = "was verified.";
1339         } else {
1340             resultString = "does not exist.";
1341         }
1342         accessLogger.logService(AccessStatusType.SUCCESSFUL_VERIFY_USER, servicePrincipal, null, null);
1343         messageLogger.logInfo("User verification (by " + servicePrincipal + "): " + userId + " " + resultString);
1344 
1345         return userExistence;
1346     }
1347 
1348 
1349     /***
1350      * Sets config for a module. A supplied configuration is transferred to the
1351      * correct module. When all modules have received their config, the
1352      * controller's status becomes ready.
1353      * @param module
1354      *            Name of the module to set config for.
1355      * @param properties
1356      *            The configuration to transfer to the module.
1357      * @see ConfigurationManager#MODULE_AM
1358      * @see ConfigurationManager#MODULE_DM
1359      * @see ConfigurationManager#MODULE_SM
1360      * @see ConfigurationManager#MODULE_WEB
1361      */
1362     public static synchronized void setConfig(final String module,
1363                                               final Properties properties) {
1364 
1365         if (module.equals(ConfigurationManager.MODULE_AM)) {
1366             if (authzManager != null) {
1367                 authzManager.setConfig(properties);
1368                 amReady = true;
1369                 messageLogger.logInfo("Config set for AM.");
1370             } else {
1371                 messageLogger.logCritical("Received authorization config before AM is initialized.");
1372             }
1373         } else if (module.equals(ConfigurationManager.MODULE_DM)) {
1374             if (directoryManager != null) {
1375                 directoryManager.setConfig(properties);
1376                 dmReady = true;
1377                 messageLogger.logInfo("Config set for DM.");
1378             } else {
1379                 messageLogger.logCritical("Received directory config before DM is initialized.");
1380             }
1381         } else if (module.equals(ConfigurationManager.MODULE_SM)) {
1382             if (store != null) {
1383                 try {
1384                     store.setConfig(properties);
1385                     smReady = true;
1386                     messageLogger.logInfo("Config set for SM.");
1387                 } catch (MoriaStoreConfigurationException msce) {
1388                     smReady = false;
1389                     messageLogger.logCritical("Unable to set config for SM", msce);
1390                 }
1391             } else {
1392                 messageLogger.logCritical("Received store config before SM is initialized.");
1393             }
1394         } else if (module.equals(ConfigurationManager.MODULE_WEB)) {
1395             if (servletContext != null) {
1396                 servletContext.setAttribute("no.feide.moria.web.config", properties);
1397                 messageLogger.logInfo("Config set for WEB.");
1398             } else {
1399                 messageLogger.logCritical("Received web config before the servlet context is available.");
1400             }
1401         }
1402 
1403         /* If all modules are ready, the controller is ready */
1404         if (isInitialized.booleanValue() && amReady && dmReady && smReady) {
1405             ready = true;
1406             messageLogger.logInfo("All config is set. Moria is READY for use.");
1407         }
1408     }
1409 
1410 
1411     /***
1412      * Starts the controller. The controller expects to be started from a web
1413      * application. The supplied ServletContext will be used to transfer config
1414      * from the configuration manager to the servlets.
1415      * @param sc
1416      *            The servletContext from the caller.
1417      * @throws InoperableStateException
1418      *             if Moria is not ready for use.
1419      */
1420     public static void initController(final ServletContext sc)
1421     throws InoperableStateException {
1422 
1423         if (isInitialized.booleanValue()) {
1424             messageLogger.logDebug("Controller has already been initialized");
1425             return;
1426         }
1427 
1428         /* Store servlet context for web module configuration. */
1429         servletContext = sc;
1430         init();
1431 
1432         messageLogger.logInfo("Controller initialized");
1433     }
1434 
1435 
1436     /***
1437      * Stops the controller.
1438      */
1439     public static void stopController() {
1440 
1441         if (!isInitialized.booleanValue()) {
1442             messageLogger = new MessageLogger(MoriaController.class);
1443             messageLogger.logInfo("Attempt to stop uninitialized controller, ignoring");
1444         } else {
1445             stop();
1446             messageLogger.logInfo("Controller stopped");
1447         }
1448     }
1449 
1450 
1451     /***
1452      * Validates a URL.
1453      * @param url
1454      *            The URL to validate.
1455      * @return <code>true</code> if the URL is valid, else <code>false</code>.
1456      * @throws IllegalArgumentException
1457      *             if <code>url</code> is <code>null</code> or an empty
1458      *             string.
1459      * @see URI
1460      */
1461     static boolean isLegalURL(final String url) {
1462 
1463         // Sanity checks.
1464         if (url == null || url.equals(""))
1465             throw new IllegalArgumentException("URL must be a non-empty string");
1466 
1467         try {
1468             URI validator = new URI(url);
1469             final String protocol = validator.getScheme();
1470             if (protocol == null || (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https"))) {
1471                 messageLogger.logWarn("Illegal URL protocol '" + url + "'");
1472                 return false;
1473             }
1474         } catch (URISyntaxException e) {
1475             messageLogger.logWarn("Illegal URL '" + url + "'");
1476             return false;
1477         }
1478         return true;
1479 
1480     }
1481 
1482 
1483     /***
1484      * Returns the service configuration for the service that created the
1485      * authentication attempt.
1486      * @param loginTicketId
1487      *            The login ticket associated with the authentication attempt.
1488      *            Cannot be <code>null</code> or an empty string.
1489      * @return A HashMap containing service configuration.
1490      * @throws UnknownTicketException
1491      *             If the ticket does not exist in the store, if the ticket is
1492      *             invalid, or if the ticket does not correspond to a service.
1493      * @throws InoperableStateException
1494      *             If the controller or the store is not ready to use.
1495      * @throws IllegalInputException
1496      *             If <code>loginTicketId</code> is <code>null</code> or an
1497      *             empty string.
1498      */
1499     public static HashMap getServiceProperties(final String loginTicketId)
1500     throws UnknownTicketException, InoperableStateException,
1501     IllegalInputException {
1502 
1503         // Sanity checks.
1504         if (!ready)
1505             throw new InoperableStateException(NOT_READY);
1506         if (loginTicketId == null || loginTicketId.equals(""))
1507             throw new IllegalInputException("Login ticket ID must be a non-empty string");
1508 
1509         // Retrieve authentication attempt.
1510         final MoriaAuthnAttempt authnAttempt;
1511         try {
1512             authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
1513         } catch (NonExistentTicketException e) {
1514             accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, null, loginTicketId, null);
1515             messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET, e);
1516             throw new UnknownTicketException(NONEXISTENT_TICKET);
1517         } catch (InvalidTicketException e) {
1518             accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, null, loginTicketId, null);
1519             messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1520             throw new UnknownTicketException(NONEXISTENT_TICKET);
1521         } catch (MoriaStoreException e) {
1522             messageLogger.logCritical(CAUGHT_STORE, e);
1523             throw new InoperableStateException(STORE_DOWN);
1524         }
1525 
1526         // Return service properties.
1527         try {
1528             return authzManager.getServiceProperties(authnAttempt.getServicePrincipal());
1529         } catch (UnknownServicePrincipalException e) {
1530             messageLogger.logWarn("Service '" + authnAttempt.getServicePrincipal() + "' is unknown", loginTicketId, e);
1531             throw new UnknownTicketException("Ticket '" + loginTicketId + "' does not correspond to a known service");
1532         }
1533     }
1534 
1535 
1536     /***
1537      * Gets the security level of an authentication attempt.
1538      * @param loginTicketId
1539      *            The ticket associated with the authentication attempt.
1540      * @return int describing the security level for the requested attributes in
1541      *         The authentication attempt.
1542      * @throws UnknownTicketException
1543      *             If the ticket does not exist, is invalid, or is not
1544      *             associated with a service.
1545      * @throws InoperableStateException
1546      *             If Moria is not usable.
1547      * @throws IllegalArgumentException
1548      *             If loginTicketId is null or empty.
1549      */
1550     public static int getSecLevel(final String loginTicketId)
1551     throws UnknownTicketException, InoperableStateException {
1552 
1553         /* Check controller state */
1554         if (!ready) { throw new InoperableStateException(NOT_READY); }
1555 
1556         /* Validate argument */
1557         if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalArgumentException("'loginTicketId' must be a non-empty string, was: " + loginTicketId); }
1558 
1559         final MoriaAuthnAttempt authnAttempt;
1560         try {
1561             authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
1562         } catch (NonExistentTicketException e) {
1563             accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, null, loginTicketId, null);
1564             messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, e);
1565             throw new UnknownTicketException(NONEXISTENT_TICKET);
1566         } catch (InvalidTicketException e) {
1567             accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, null, loginTicketId, null);
1568             throw new UnknownTicketException(NONEXISTENT_TICKET);
1569         } catch (MoriaStoreException e) {
1570             messageLogger.logCritical(CAUGHT_STORE, e);
1571             throw new InoperableStateException(STORE_DOWN);
1572         }
1573 
1574         try {
1575             return authzManager.getSecLevel(authnAttempt.getServicePrincipal(), authnAttempt.getRequestedAttributes());
1576         } catch (UnknownServicePrincipalException e) {
1577             messageLogger.logWarn("UnknownServicePrincipalException caught, has the service been removed from the authorization database?", e);
1578             throw new UnknownTicketException("Ticket is no longer connected to a service.");
1579         } catch (UnknownAttributeException e) {
1580             messageLogger.logWarn("UnknownAttributeException caught, has the attribute been removed from the authorization database?", e);
1581             throw new InoperableStateException("The authentication attempt is unusable.");
1582         }
1583     }
1584 
1585 
1586     /***
1587      * Invalidates an SSO ticket. After the invalidation, the ticket cannot be
1588      * used any more.
1589      * @param ssoTicketId
1590      *            The ticket to be invalidated.
1591      * @throws IllegalInputException
1592      *             If <code>ssoTicketId</code> is null or empty.
1593      * @throws InoperableStateException
1594      *             If Moria is not ready to use.
1595      */
1596     public static void invalidateSSOTicket(final String ssoTicketId)
1597     throws IllegalInputException, InoperableStateException {
1598 
1599         /* Check controller state */
1600         if (!ready) { throw new InoperableStateException(NOT_READY); }
1601 
1602         /* Validate argument */
1603         if (ssoTicketId == null || ssoTicketId.equals("")) { throw new IllegalInputException("'ssoTicketId' must be a non-empty string."); }
1604 
1605         try {
1606             store.removeSSOTicket(ssoTicketId);
1607         } catch (NonExistentTicketException e) {
1608             /* We don't care, it's just removal of a ticket */
1609             messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET + ", OK since we tried to remove");
1610         } catch (MoriaStoreException e) {
1611             messageLogger.logCritical(CAUGHT_STORE, e);
1612             throw new InoperableStateException(STORE_DOWN);
1613         }
1614 
1615         accessLogger.logUser(AccessStatusType.SSO_TICKET_INVALIDATED, null, null, ssoTicketId, null);
1616     }
1617 
1618 
1619     /***
1620      * Creates a redirect URL for redirecting user back to web service. The URL
1621      * is created by concatenating the URL prefix with the service ticket and
1622      * the URL postfix.
1623      * @param serviceTicketId
1624      *            The service ticket to generate redirect URL for.
1625      * @return A <code>String</code> containing the URL.
1626      * @throws InoperableStateException
1627      *             If Moria is not ready for use.
1628      * @throws IllegalInputException
1629      *             If <code>serviceTicketId</code> is null or empty.
1630      * @throws UnknownTicketException
1631      *             If the service ticket is invalid or does not exist.
1632      */
1633     public static String getRedirectURL(final String serviceTicketId)
1634     throws InoperableStateException, IllegalInputException,
1635     UnknownTicketException {
1636 
1637         /* Check controller state */
1638         if (!ready) { throw new InoperableStateException(NOT_READY); }
1639 
1640         /* Validate argument */
1641         if (serviceTicketId == null || serviceTicketId.equals("")) { throw new IllegalInputException("serviceTicketId must be a non-empty string."); }
1642 
1643         final MoriaAuthnAttempt authnAttempt;
1644         try {
1645             authnAttempt = store.getAuthnAttempt(serviceTicketId, true, null);
1646         } catch (InvalidTicketException e) {
1647             accessLogger.logUser(AccessStatusType.INVALID_SERVICE_TICKET, null, null, serviceTicketId, null);
1648             messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1649             throw new UnknownTicketException(NONEXISTENT_TICKET);
1650         } catch (NonExistentTicketException e) {
1651             accessLogger.logUser(AccessStatusType.NONEXISTENT_SERVICE_TICKET, null, null, serviceTicketId, null);
1652             messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, e);
1653             throw new UnknownTicketException(NONEXISTENT_TICKET);
1654         } catch (MoriaStoreException e) {
1655             messageLogger.logCritical(CAUGHT_STORE, e);
1656             throw new InoperableStateException(STORE_DOWN);
1657         }
1658 
1659         return authnAttempt.getReturnURLPrefix() + serviceTicketId + authnAttempt.getReturnURLPostfix();
1660     }
1661 
1662 
1663     /***
1664      * Resolves a user's home organization through the Directory Manager.
1665      * @param username
1666      *            The full username of a user. Must be a non-empty string.
1667      * @return A <code>String</code> containing the user's organization.
1668      * @throws AuthenticationException
1669      *             If the user's organization is not found.
1670      * @throws IllegalArgumentException
1671      *             If <code>username</code> is <code>null</code> or an empty
1672      *             string.
1673      */
1674     public static String getUserOrg(final String username)
1675     throws AuthenticationException {
1676 
1677         // Sanity check.
1678         if ((username == null) || (username.length() == 0))
1679             throw new IllegalArgumentException("User name must be a non-empty string");
1680 
1681         String org = directoryManager.getRealm(username);
1682         if (org == null)
1683             throw new AuthenticationException();
1684         return org;
1685     }
1686 
1687 
1688     /***
1689      * Convenience method to assure certain pre-authentication checks.
1690      * @param sessionTicket
1691      *            The session ticket.
1692      * @param userCredentials
1693      *            The user's credentials.
1694      * @param attributeRequest
1695      *            The attribute request.
1696      * @return The returned attributes.
1697      * @throws AuthenticationFailedException
1698      *             If authentication fails.
1699      * @throws BackendException
1700      *             If the backend fails to authenticate/retrieve attributes.
1701      * @throws IllegalStateException
1702      *             If Moria2 is in an illegal state.
1703      * @see DirectoryManager#authenticate(java.lang.String,
1704      *      no.feide.moria.directory.Credentials, java.lang.String[])
1705      */
1706     private static final HashMap authenticate(final String sessionTicket,
1707                                        final Credentials userCredentials,
1708                                        final String[] attributeRequest)
1709     throws AuthenticationFailedException, BackendException,
1710     IllegalStateException {
1711 
1712         // User password should not contain any character other than a-z, A-Z or
1713         // 0-9. Anything other will be logged as a warning.
1714         if (messageLogger.isEnabledFor(Level.WARN)) {
1715             String checkPassword = userCredentials.getPassword().replaceAll("[b-zA-Z0-9]", "a");
1716             checkPassword = checkPassword.replaceAll("[^a]", "A");
1717             if (checkPassword.contains("A"))
1718                 messageLogger.logWarn("User '" + userCredentials.getUsername() + "' attempts login with suspicious password (should only contain [a-zA-Z0-9])", sessionTicket);
1719         }
1720 
1721         return directoryManager.authenticate(sessionTicket, userCredentials, attributeRequest);
1722 
1723     }
1724 
1725 }