2 * Licensed to the University Corporation for Advanced Internet Development,
3 * Inc. (UCAID) under one or more contributor license agreements. See the
4 * NOTICE file distributed with this work for additional information regarding
5 * copyright ownership. The UCAID licenses this file to You under the Apache
6 * License, Version 2.0 (the "License"); you may not use this file except in
7 * compliance with the License. You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 package edu.internet2.middleware.shibboleth.idp.authn;
20 import java.io.IOException;
21 import java.security.GeneralSecurityException;
22 import java.security.MessageDigest;
23 import java.security.Principal;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.List;
31 import java.util.Map.Entry;
34 import javax.security.auth.Subject;
35 import javax.servlet.RequestDispatcher;
36 import javax.servlet.ServletConfig;
37 import javax.servlet.ServletContext;
38 import javax.servlet.ServletException;
39 import javax.servlet.http.Cookie;
40 import javax.servlet.http.HttpServlet;
41 import javax.servlet.http.HttpServletRequest;
42 import javax.servlet.http.HttpServletResponse;
44 import org.joda.time.DateTime;
45 import org.opensaml.saml2.core.AuthnContext;
46 import org.opensaml.util.storage.StorageService;
47 import org.opensaml.ws.transport.http.HTTPTransportUtils;
48 import org.opensaml.xml.util.Base64;
49 import org.opensaml.xml.util.DatatypeHelper;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
53 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
54 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
55 import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
56 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
57 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
58 import edu.internet2.middleware.shibboleth.idp.session.Session;
59 import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
60 import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
61 import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
63 /** Manager responsible for handling authentication requests. */
64 public class AuthenticationEngine extends HttpServlet {
67 * Name of the Servlet config init parameter that indicates whether the public credentials of a {@link Subject} are
68 * retained after authentication.
70 public static final String RETAIN_PUBLIC_CREDENTIALS = "retainSubjectsPublicCredentials";
73 * Name of the Servlet config init parameter that indicates whether the private credentials of a {@link Subject} are
74 * retained after authentication.
76 public static final String RETAIN_PRIVATE_CREDENTIALS = "retainSubjectsPrivateCredentials";
78 /** Name of the Servlet config init parameter that holds the partition name for login contexts. */
79 public static final String LOGIN_CONTEXT_PARTITION_NAME_INIT_PARAM_NAME = "loginContextPartitionName";
81 /** Name of the Servlet config init parameter that holds lifetime of a login context in the storage service. */
82 public static final String LOGIN_CONTEXT_LIFETIME_INIT_PARAM_NAME = "loginContextEntryLifetime";
84 /** Name of the IdP Cookie containing the IdP session ID. */
85 public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
87 /** Name of the key under which to bind the storage service key for a login context. */
88 public static final String LOGIN_CONTEXT_KEY_NAME = "_idp_authn_lc_key";
90 /** Serial version UID. */
91 private static final long serialVersionUID = -8479060989001890156L;
94 private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
96 // TODO remove once HttpServletHelper does redirects
97 private static ServletContext context;
99 /** Storage service used to store {@link LoginContext}s while authentication is in progress. */
100 private static StorageService<String, LoginContextEntry> storageService;
102 /** Whether the public credentials of a {@link Subject} are retained after authentication. */
103 private boolean retainSubjectsPublicCredentials;
105 /** Whether the private credentials of a {@link Subject} are retained after authentication. */
106 private boolean retainSubjectsPrivateCredentials;
108 /** Profile handler manager. */
109 private IdPProfileHandlerManager handlerManager;
111 /** Session manager. */
112 private SessionManager<Session> sessionManager;
115 public void init(ServletConfig config) throws ServletException {
118 String retain = DatatypeHelper.safeTrimOrNullString(config.getInitParameter(RETAIN_PRIVATE_CREDENTIALS));
119 if (retain != null) {
120 retainSubjectsPrivateCredentials = Boolean.parseBoolean(retain);
122 retainSubjectsPrivateCredentials = false;
125 retain = DatatypeHelper.safeTrimOrNullString(config.getInitParameter(RETAIN_PUBLIC_CREDENTIALS));
126 if (retain != null) {
127 retainSubjectsPublicCredentials = Boolean.parseBoolean(retain);
129 retainSubjectsPublicCredentials = false;
131 context = config.getServletContext();
132 handlerManager = HttpServletHelper.getProfileHandlerManager(context);
133 sessionManager = HttpServletHelper.getSessionManager(context);
134 storageService = (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(context);
138 * Returns control back to the authentication engine.
140 * @param httpRequest current HTTP request
141 * @param httpResponse current HTTP response
143 public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
144 LOG.debug("Returning control to authentication engine");
145 LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
146 if (loginContext == null) {
147 LOG.warn("No login context available, unable to return to authentication engine");
148 forwardRequest("/error.jsp", httpRequest, httpResponse);
150 forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
155 * Returns control back to the profile handler that invoked the authentication engine.
157 * @param httpRequest current HTTP request
158 * @param httpResponse current HTTP response
160 public static void returnToProfileHandler(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
161 LOG.debug("Returning control to profile handler");
162 LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
163 if (loginContext == null) {
164 LOG.warn("No login context available, unable to return to profile handler");
165 forwardRequest("/error.jsp", httpRequest, httpResponse);
169 if (loginContext.getProfileHandlerURL() == null) {
170 LOG.warn("Login context did not contain a profile handler path, unable to return to profile handler");
171 forwardRequest("/error.jsp", httpRequest, httpResponse);
175 String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, loginContext.getProfileHandlerURL())
177 LOG.debug("Redirecting user to profile handler at {}", profileUrl);
179 httpResponse.sendRedirect(profileUrl);
180 } catch (IOException e) {
181 LOG.warn("Error sending user back to profile handler at " + profileUrl, e);
186 * Forwards a request to the given path.
188 * @param forwardPath path to forward the request to
189 * @param httpRequest current HTTP request
190 * @param httpResponse current HTTP response
192 protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
193 HttpServletResponse httpResponse) {
195 RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
196 dispatcher.forward(httpRequest, httpResponse);
198 } catch (IOException e) {
199 LOG.error("Unable to return control back to authentication engine", e);
200 } catch (ServletException e) {
201 LOG.error("Unable to return control back to authentication engine", e);
206 @SuppressWarnings("unchecked")
207 protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
209 LOG.debug("Processing incoming request");
211 if (httpResponse.isCommitted()) {
212 LOG.error("HTTP Response already committed");
215 LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, getServletContext(), httpRequest);
216 if (loginContext == null) {
217 LOG.warn("No login context available, unable to proceed with authentication");
218 forwardRequest("/error.jsp", httpRequest, httpResponse);
222 if (!loginContext.getAuthenticationAttempted()) {
223 startUserAuthentication(loginContext, httpRequest, httpResponse);
225 completeAuthentication(loginContext, httpRequest, httpResponse);
230 * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
231 * authentication method is sufficient. Also determines, when authentication is required, which handler to use
232 * depending on whether passive authentication is required.
234 * @param loginContext current login context
235 * @param httpRequest current HTTP request
236 * @param httpResponse current HTTP response
238 protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
239 HttpServletResponse httpResponse) {
240 LOG.debug("Beginning user authentication process.");
242 Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
243 if (idpSession != null) {
244 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
247 Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
249 // Filter out possible candidate login handlers by forced and passive authentication requirements
250 if (loginContext.isForceAuthRequired()) {
251 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
254 if (loginContext.isPassiveAuthRequired()) {
255 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
258 LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
259 loginContext.setAuthenticationAttempted();
260 loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
262 // Send the request to the login handler
263 HttpServletHelper.bindLoginContext(loginContext, storageService, getServletContext(), httpRequest,
265 loginHandler.login(httpRequest, httpResponse);
266 } catch (AuthenticationException e) {
267 loginContext.setAuthenticationFailure(e);
268 returnToProfileHandler(httpRequest, httpResponse);
273 * Determines which configured login handlers will support the requested authentication methods.
275 * @param loginContext current login context
276 * @param idpSession current user's session, or null if they don't have one
278 * @return login methods that may be used to authenticate the user
280 * @throws AuthenticationException thrown if no login handler meets the given requirements
282 protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
283 throws AuthenticationException {
284 Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(
285 handlerManager.getLoginHandlers());
286 LOG.debug("Filtering configured LoginHandlers: {}", supportedLoginHandlers);
288 // First, if the service provider requested a particular authentication method, filter out everything but
289 List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
290 if (requestedMethods != null && !requestedMethods.isEmpty()) {
291 LOG.debug("Filtering possible login handlers by requested authentication methods: {}", requestedMethods);
292 Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet()
294 Entry<String, LoginHandler> supportedLoginHandlerEntry;
295 while (supportedLoginHandlerItr.hasNext()) {
296 supportedLoginHandlerEntry = supportedLoginHandlerItr.next();
297 if (!supportedLoginHandlerEntry.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
298 && !requestedMethods.contains(supportedLoginHandlerEntry.getKey())) {
300 "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
301 supportedLoginHandlerEntry.getKey());
302 supportedLoginHandlerItr.remove();
307 // Next, determine, if present, if the previous session handler can be used
308 filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
310 if (supportedLoginHandlers.isEmpty()) {
311 LOG.warn("No authentication method, requested by the service provider, is supported");
312 throw new AuthenticationException(
313 "No authentication method, requested by the service provider, is supported");
316 return supportedLoginHandlers;
320 * Filters out the previous session login handler if there is no existing IdP session, no active authentication
321 * methods, or if at least one of the active authentication methods do not match the requested authentication
324 * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
325 * @param idpSession current IdP session, may be null if no session currently exists
326 * @param loginContext current login context, never null
328 protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
329 Session idpSession, LoginContext loginContext) {
330 if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
334 if (idpSession == null) {
335 LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
336 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
339 Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
342 Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
343 while (methodItr.hasNext()) {
344 AuthenticationMethodInformation info = methodItr.next();
345 if (info.isExpired()) {
349 if (currentAuthnMethods.isEmpty()) {
350 LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
351 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
355 List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
356 if (requestedMethods != null && !requestedMethods.isEmpty()) {
357 boolean retainPreviousSession = false;
358 for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
359 if (loginContext.getRequestedAuthenticationMethods().contains(
360 currentAuthnMethod.getAuthenticationMethod())) {
361 retainPreviousSession = true;
366 if (!retainPreviousSession) {
367 LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
368 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
375 * Filters out any login handler based on the requirement for forced authentication.
377 * During forced authentication any handler that has not previously been used to authenticate the user or any
378 * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
380 * @param idpSession user's current IdP session
381 * @param loginContext current login context
382 * @param loginHandlers login handlers to filter
384 * @throws ForceAuthenticationException thrown if no handlers remain after filtering
386 protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
387 Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
388 LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
390 ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
391 if (idpSession != null) {
392 activeMethods.addAll(idpSession.getAuthenticationMethods().values());
395 loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
397 LoginHandler loginHandler;
398 for (AuthenticationMethodInformation activeMethod : activeMethods) {
399 loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
400 if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
401 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
402 LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
403 .getClass().getName());
404 loginHandlers.remove(handlerSupportedMethods);
409 LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
412 if (loginHandlers.isEmpty()) {
413 LOG.info("Force authentication requested but no login handlers available to support it");
414 throw new ForceAuthenticationException();
419 * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
420 * authentication is required.
422 * @param idpSession user's current IdP session
423 * @param loginContext current login context
424 * @param loginHandlers login handlers to filter
426 * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
428 protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
429 Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
430 LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
432 if (idpSession == null) {
433 loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
436 LoginHandler loginHandler;
437 Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
438 while (authnMethodItr.hasNext()) {
439 loginHandler = authnMethodItr.next().getValue();
440 if (!loginHandler.supportsPassive()) {
441 authnMethodItr.remove();
445 LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
448 if (loginHandlers.isEmpty()) {
449 LOG.warn("Passive authentication required but no login handlers available to support it");
450 throw new PassiveAuthenticationException();
455 * Selects a login handler from a list of possible login handlers that could be used for the request.
457 * @param possibleLoginHandlers list of possible login handlers that could be used for the request
458 * @param loginContext current login context
459 * @param idpSession current IdP session, if one exists
461 * @return the login handler to use for this request
463 * @throws AuthenticationException thrown if no handler can be used for this request
465 protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
466 LoginContext loginContext, Session idpSession) throws AuthenticationException {
467 LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
468 LoginHandler loginHandler;
469 if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
470 LOG.debug("Authenticating user with previous session LoginHandler");
471 loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
473 for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
474 if (authnMethod.isExpired()) {
478 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
479 || loginContext.getRequestedAuthenticationMethods().contains(
480 authnMethod.getAuthenticationMethod())) {
481 LOG.debug("Basing previous session authentication on active authentication method {}",
482 authnMethod.getAuthenticationMethod());
483 loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
484 loginContext.setAuthenticationMethodInformation(authnMethod);
490 if (loginContext.getDefaultAuthenticationMethod() != null
491 && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
492 loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
493 loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
495 Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
496 loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
497 loginHandler = chosenLoginHandler.getValue();
500 LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
505 * Completes the authentication process.
507 * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
508 * Shibboleth session is created if needed, information indicating that the user has logged into the service is
509 * recorded and finally control is returned back to the profile handler.
511 * @param loginContext current login context
512 * @param httpRequest current HTTP request
513 * @param httpResponse current HTTP response
515 protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
516 HttpServletResponse httpResponse) {
517 LOG.debug("Completing user authentication process");
519 Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
522 // We allow a login handler to override the authentication method in the
523 // event that it supports multiple methods
524 String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
525 .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
526 if (actualAuthnMethod != null) {
527 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
528 && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
529 String msg = "Relying patry required an authentication method of "
530 + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
533 throw new AuthenticationException(msg);
536 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
539 // Check to make sure the login handler did the right thing
540 validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
541 if(loginContext.getAuthenticationFailure() != null){
542 returnToProfileHandler(httpRequest, httpResponse);
545 // Check for an overridden authn instant.
546 DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
548 // Get the Subject from the request. If force authentication was required then make sure the
549 // Subject identifies the same user that authenticated before
550 Subject subject = getLoginHandlerSubject(httpRequest);
551 if (loginContext.isForceAuthRequired()) {
552 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
554 // Reset the authn instant.
555 if (actualAuthnInstant == null) {
556 actualAuthnInstant = new DateTime();
560 loginContext.setPrincipalAuthenticated(true);
561 updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
562 LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
563 loginContext.getAuthenticationMethod());
564 } catch (AuthenticationException e) {
565 LOG.error("Authentication failed with the error:", e);
566 loginContext.setPrincipalAuthenticated(false);
567 loginContext.setAuthenticationFailure(e);
570 returnToProfileHandler(httpRequest, httpResponse);
574 * Validates that the authentication was successfully performed by the login handler. An authentication is
575 * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
576 * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
577 * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
579 * @param loginContext current login context
580 * @param httpRequest current HTTP request
581 * @param authenticationMethod the authentication method used to authenticate the user
583 * @throws AuthenticationException thrown if the authentication was not successful
585 protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
586 String authenticationMethod) throws AuthenticationException {
587 LOG.debug("Validating authentication was performed successfully");
589 if (authenticationMethod == null) {
590 LOG.error("No authentication method reported by login handler.");
591 throw new AuthenticationException("No authentication method reported by login handler.");
594 String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
595 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
596 if (errorMessage != null) {
597 LOG.debug("Error returned from login handler for authentication method {}:\n{}",
598 loginContext.getAttemptedAuthnMethod(), errorMessage);
599 loginContext.setAuthenticationFailure(new AuthenticationException(errorMessage));
600 loginContext.setPrincipalAuthenticated(false);
604 AuthenticationException authnException = (AuthenticationException) httpRequest
605 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
606 if (authnException != null) {
607 LOG.debug("Exception returned from login handler for authentication method {}:\n{}",
608 loginContext.getAttemptedAuthnMethod(), authnException);
609 loginContext.setAuthenticationFailure(authnException);
610 loginContext.setPrincipalAuthenticated(false);
614 Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
615 Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
616 String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
617 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
619 if (subject == null && principal == null && principalName == null) {
620 LOG.error("No user identified by login handler.");
621 throw new AuthenticationException("No user identified by login handler.");
626 * Gets the subject from the request coming back from the login handler.
628 * @param httpRequest request coming back from the login handler
630 * @return the {@link Subject} created from the request
632 * @throws AuthenticationException thrown if no subject can be retrieved from the request
634 protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
635 Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
636 Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
637 String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
638 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
640 if (subject == null && (principal != null || principalName != null)) {
641 subject = new Subject();
642 if (principal == null) {
643 principal = new UsernamePrincipal(principalName);
645 subject.getPrincipals().add(principal);
652 * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
653 * principal name that is equal to the principal name associated with the authentication method. If this is the
654 * first time the subject has authenticated with this method than this check always passes.
656 * @param idpSession user's IdP session
657 * @param authnMethod method used to authenticate the user
658 * @param subject subject that was authenticated
660 * @throws AuthenticationException thrown if this check fails
662 protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
663 throws AuthenticationException {
664 if (idpSession != null) {
665 AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
666 if (authnMethodInfo != null) {
667 boolean princpalMatch = false;
668 for (Principal princpal : subject.getPrincipals()) {
669 if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
670 princpalMatch = true;
675 if (!princpalMatch) {
676 throw new ForceAuthenticationException(
677 "Authenticated principal does not match previously authenticated principal");
684 * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
687 * @param loginContext current login context
688 * @param authenticationSubject subject created from the authentication method
689 * @param authenticationMethod the method used to authenticate the subject
690 * @param authenticationInstant the time of authentication
691 * @param httpRequest current HTTP request
692 * @param httpResponse current HTTP response
694 protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
695 String authenticationMethod, DateTime authenticationInstant, HttpServletRequest httpRequest,
696 HttpServletResponse httpResponse) {
697 Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
698 LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
700 Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
701 if (idpSession == null) {
702 LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
703 idpSession = (Session) sessionManager.createSession();
704 loginContext.setSessionID(idpSession.getSessionID());
705 addSessionCookie(httpRequest, httpResponse, idpSession);
708 // Merge the information in the current session subject with the information from the
709 // login handler subject
710 idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
712 // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
713 // if not record the new information
714 AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
715 authenticationMethod);
716 if (authnMethodInfo == null || authenticationInstant != null) {
717 LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
718 authenticationPrincipal.getName());
719 LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
720 DateTime authnInstant = authenticationInstant;
721 if (authnInstant == null) {
722 authnInstant = new DateTime();
724 authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession.getSubject(), authenticationPrincipal,
725 authenticationMethod, authnInstant, loginHandler.getAuthenticationDuration());
728 loginContext.setAuthenticationMethodInformation(authnMethodInfo);
729 idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
730 sessionManager.indexSession(idpSession, idpSession.getPrincipalName());
732 ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
734 idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
738 * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
739 * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
740 * the private credentials from both subjects, if not the new subject will not contain private credentials. If
741 * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
742 * from both subjects, if not the new subject will not contain public credentials.
744 * @param subject1 first subject to merge, may be null
745 * @param subject2 second subject to merge, may be null
747 * @return subject containing the merged information
749 protected Subject mergeSubjects(Subject subject1, Subject subject2) {
750 if (subject1 == null && subject2 == null) {
751 return new Subject();
754 if (subject1 == null) {
758 if (subject2 == null) {
762 Set<Principal> principals = new HashSet<Principal>(3);
763 principals.addAll(subject1.getPrincipals());
764 principals.addAll(subject2.getPrincipals());
766 Set<Object> publicCredentials = new HashSet<Object>(3);
767 if (retainSubjectsPublicCredentials) {
768 LOG.debug("Merging in subjects public credentials");
769 publicCredentials.addAll(subject1.getPublicCredentials());
770 publicCredentials.addAll(subject2.getPublicCredentials());
773 Set<Object> privateCredentials = new HashSet<Object>(3);
774 if (retainSubjectsPrivateCredentials) {
775 LOG.debug("Merging in subjects private credentials");
776 privateCredentials.addAll(subject1.getPrivateCredentials());
777 privateCredentials.addAll(subject2.getPrivateCredentials());
780 return new Subject(false, principals, publicCredentials, privateCredentials);
784 * Adds an IdP session cookie to the outbound response.
786 * @param httpRequest current request
787 * @param httpResponse current response
788 * @param userSession user's session
790 protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
791 Session userSession) {
792 httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
794 byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
795 byte[] sessionId = userSession.getSessionID().getBytes();
797 String signature = null;
799 MessageDigest digester = MessageDigest.getInstance("SHA");
800 digester.update(userSession.getSessionSecret());
801 digester.update(remoteAddress);
802 digester.update(sessionId);
803 signature = Base64.encodeBytes(digester.digest());
804 } catch (GeneralSecurityException e) {
805 LOG.error("Unable to compute signature over session cookie material", e);
808 LOG.debug("Adding IdP session cookie to HTTP response");
809 StringBuilder cookieValue = new StringBuilder();
810 cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
811 cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
812 cookieValue.append(signature);
814 String cookieDomain = HttpServletHelper.getCookieDomain(context);
816 Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
817 sessionCookie.setVersion(1);
818 if (cookieDomain != null) {
819 sessionCookie.setDomain(cookieDomain);
821 sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
822 sessionCookie.setSecure(httpRequest.isSecure());
823 httpResponse.addCookie(sessionCookie);