1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
|
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.authenticator;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Manager;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.SessionListener;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.tomcat.util.res.StringManager;
/**
* A <strong>Valve</strong> that supports a "single sign on" user experience, where the security identity of a user who
* successfully authenticates to one web application is propagated to other web applications in the same security
* domain. For successful use, the following requirements must be met:
* <ul>
* <li>This Valve must be configured on the Container that represents a virtual host (typically an implementation of
* <code>Host</code>).</li>
* <li>The <code>Realm</code> that contains the shared user and role information must be configured on the same
* Container (or a higher one), and not overridden at the web application level.</li>
* <li>The web applications themselves must use one of the standard Authenticators found in the
* <code>org.apache.catalina.authenticator</code> package.</li>
* </ul>
*
* @author Craig R. McClanahan
*/
public class SingleSignOn extends ValveBase {
private static final StringManager sm = StringManager.getManager(SingleSignOn.class);
/*
* The engine at the top of the container hierarchy in which this SSO Valve has been placed. It is used to get back
* to a session object from a SingleSignOnSessionKey and is updated when the Valve starts and stops.
*/
private Engine engine;
// ------------------------------------------------------ Constructor
public SingleSignOn() {
super(true);
}
// ----------------------------------------------------- Instance Variables
/**
* The cache of SingleSignOnEntry instances for authenticated Principals, keyed by the cookie value that is used to
* select them.
*/
protected Map<String,SingleSignOnEntry> cache = new ConcurrentHashMap<>();
/**
* Indicates whether this valve should require a downstream Authenticator to reauthenticate each request, or if it
* itself can bind a UserPrincipal and AuthType object to the request.
*/
private boolean requireReauthentication = false;
/**
* Optional SSO cookie domain.
*/
private String cookieDomain;
/**
* SSO cookie name, the default value is <code>JSESSIONIDSSO</code>.
*/
private String cookieName = Constants.SINGLE_SIGN_ON_COOKIE;
// ------------------------------------------------------------- Properties
/**
* Returns the optional cookie domain. May return null.
*
* @return The cookie domain
*/
public String getCookieDomain() {
return cookieDomain;
}
/**
* Sets the domain to be used for sso cookies.
*
* @param cookieDomain cookie domain name
*/
public void setCookieDomain(String cookieDomain) {
if (cookieDomain != null && cookieDomain.trim().isEmpty()) {
this.cookieDomain = null;
} else {
this.cookieDomain = cookieDomain;
}
}
/**
* @return the cookie name
*/
public String getCookieName() {
return cookieName;
}
/**
* Set the cookie name that will be used for the SSO cookie.
*
* @param cookieName the cookieName to set
*/
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
/**
* Gets whether each request needs to be reauthenticated (by an Authenticator downstream in the pipeline) to the
* security <code>Realm</code>, or if this Valve can itself bind security info to the request based on the presence
* of a valid SSO entry without rechecking with the <code>Realm</code>.
*
* @return <code>true</code> if it is required that a downstream Authenticator reauthenticate each request before
* calls to <code>HttpServletRequest.setUserPrincipal()</code> and
* <code>HttpServletRequest.setAuthType()</code> are made; <code>false</code> if the <code>Valve</code>
* can itself make those calls relying on the presence of a valid SingleSignOn entry associated with the
* request.
*
* @see #setRequireReauthentication
*/
public boolean getRequireReauthentication() {
return requireReauthentication;
}
/**
* Sets whether each request needs to be reauthenticated (by an Authenticator downstream in the pipeline) to the
* security <code>Realm</code>, or if this Valve can itself bind security info to the request, based on the presence
* of a valid SSO entry, without rechecking with the <code>Realm</code>.
* <p>
* If this property is <code>false</code> (the default), this <code>Valve</code> will bind a UserPrincipal and
* AuthType to the request if a valid SSO entry is associated with the request. It will not notify the security
* <code>Realm</code> of the incoming request.
* <p>
* This property should be set to <code>true</code> if the overall server configuration requires that the
* <code>Realm</code> reauthenticate each request thread. An example of such a configuration would be one where the
* <code>Realm</code> implementation provides security for both a web tier and an associated EJB tier, and needs to
* set security credentials on each request thread in order to support EJB access.
* <p>
* If this property is set to <code>true</code>, this Valve will set flags on the request notifying the downstream
* Authenticator that the request is associated with an SSO session. The Authenticator will then call its
* {@link AuthenticatorBase#reauthenticateFromSSO reauthenticateFromSSO} method to attempt to reauthenticate the
* request to the <code>Realm</code>, using any credentials that were cached with this Valve.
* <p>
* The default value of this property is <code>false</code>, in order to maintain backward compatibility with
* previous versions of Tomcat.
*
* @param required <code>true</code> if it is required that a downstream Authenticator reauthenticate each request
* before calls to <code>HttpServletRequest.setUserPrincipal()</code> and
* <code>HttpServletRequest.setAuthType()</code> are made; <code>false</code> if the
* <code>Valve</code> can itself make those calls relying on the presence of a valid
* SingleSignOn entry associated with the request.
*
* @see AuthenticatorBase#reauthenticateFromSSO
*/
public void setRequireReauthentication(boolean required) {
this.requireReauthentication = required;
}
// ---------------------------------------------------------- Valve Methods
/**
* Perform single-sign-on support processing for this request.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet error occurs
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
request.removeNote(Constants.REQ_SSOID_NOTE);
// Has a valid user already been authenticated?
if (containerLog.isTraceEnabled()) {
containerLog.trace(sm.getString("singleSignOn.debug.invoke", request.getRequestURI()));
}
if (request.getUserPrincipal() != null) {
if (containerLog.isDebugEnabled()) {
containerLog
.debug(sm.getString("singleSignOn.debug.hasPrincipal", request.getUserPrincipal().getName()));
}
getNext().invoke(request, response);
return;
}
// Check for the single sign on cookie
if (containerLog.isTraceEnabled()) {
containerLog.trace(sm.getString("singleSignOn.debug.cookieCheck"));
}
Cookie cookie = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie value : cookies) {
if (cookieName.equals(value.getName())) {
cookie = value;
break;
}
}
}
if (cookie == null) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.cookieNotFound"));
}
getNext().invoke(request, response);
return;
}
// Look up the cached Principal associated with this cookie value
if (containerLog.isTraceEnabled()) {
containerLog.trace(sm.getString("singleSignOn.debug.principalCheck", cookie.getValue()));
}
SingleSignOnEntry entry = cache.get(cookie.getValue());
if (entry != null) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.principalFound",
entry.getPrincipal() != null ? entry.getPrincipal().getName() : "", entry.getAuthType()));
}
request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue());
// Only set security elements if reauthentication is not required
if (!getRequireReauthentication()) {
request.setAuthType(entry.getAuthType());
request.setUserPrincipal(entry.getPrincipal());
}
} else {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.principalNotFound", cookie.getValue()));
}
// No need to return a valid SSO session ID
cookie.setValue("REMOVE");
// Age of zero will trigger removal
cookie.setMaxAge(0);
// Domain and path have to match the original cookie to 'replace'
// the original cookie
cookie.setPath("/");
String domain = getCookieDomain();
if (domain != null) {
cookie.setDomain(domain);
}
/*
* This is going to trigger a Set-Cookie header. While the value is not security sensitive, ensure that
* expectations for secure, httpOnly and Partitioned are met.
*/
cookie.setSecure(request.isSecure());
if (request.getServletContext().getSessionCookieConfig().isHttpOnly() ||
request.getContext().getUseHttpOnly()) {
cookie.setHttpOnly(true);
}
cookie.setAttribute(Constants.COOKIE_PARTITIONED_ATTR,
Boolean.toString(request.getContext().getUsePartitioned()));
response.addCookie(cookie);
}
// Invoke the next Valve in our pipeline
getNext().invoke(request, response);
}
// ------------------------------------------------------ Protected Methods
/**
* Process a session destroyed event by removing references to that session from the caches and - if the session
* destruction is the result of a logout - destroy the associated SSO session.
*
* @param ssoId The id of the SSO session with which the destroyed session was associated
* @param session The session that has been destroyed
*/
public void sessionDestroyed(String ssoId, Session session) {
if (!getState().isAvailable()) {
return;
}
// Was the session destroyed as the result of a timeout or context stop?
// If so, we'll just remove the expired session from the SSO. If the
// session was logged out, we'll log out of all session associated with
// the SSO.
if (((session.getMaxInactiveInterval() > 0) &&
(session.getIdleTimeInternal() >= session.getMaxInactiveInterval() * 1000L)) ||
(!session.getManager().getContext().getState().isAvailable())) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.sessionTimeout", ssoId, session));
}
removeSession(ssoId, session);
} else {
// The session was logged out.
// Deregister this single session id, invalidating
// associated sessions
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.sessionLogout", ssoId, session));
}
// First remove the session that we know has expired / been logged
// out since it has already been removed from its Manager and, if
// we don't remove it first, deregister() will log a warning that it
// can't be found
removeSession(ssoId, session);
// If the SSO session was only associated with one web app the call
// above will have removed the SSO session from the cache
if (cache.containsKey(ssoId)) {
deregister(ssoId);
}
}
}
/**
* Associate the specified single sign on identifier with the specified Session.
*
* @param ssoId Single sign on identifier
* @param session Session to be associated
*
* @return <code>true</code> if the session was associated to the given SSO session, otherwise <code>false</code>
*/
protected boolean associate(String ssoId, Session session) {
SingleSignOnEntry sso = cache.get(ssoId);
if (sso == null) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.associateFail", ssoId, session));
}
return false;
} else {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.associate", ssoId, session));
}
sso.addSession(this, ssoId, session);
return true;
}
}
/**
* Deregister the specified single sign on identifier, and invalidate any associated sessions.
*
* @param ssoId Single sign on identifier to deregister
*/
protected void deregister(String ssoId) {
// Look up and remove the corresponding SingleSignOnEntry
SingleSignOnEntry sso = cache.remove(ssoId);
if (sso == null) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.deregisterFail", ssoId));
}
return;
}
// Expire any associated sessions
Set<SingleSignOnSessionKey> ssoKeys = sso.findSessions();
if (ssoKeys.isEmpty()) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.deregisterNone", ssoId));
}
}
for (SingleSignOnSessionKey ssoKey : ssoKeys) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.deregister", ssoKey, ssoId));
}
// Invalidate this session
expire(ssoKey);
}
// NOTE: Clients may still possess the old single sign on cookie,
// but it will be removed on the next request since it is no longer
// in the cache
}
private void expire(SingleSignOnSessionKey key) {
if (engine == null) {
containerLog.warn(sm.getString("singleSignOn.sessionExpire.engineNull", key));
return;
}
Container host = engine.findChild(key.getHostName());
if (host == null) {
containerLog.warn(sm.getString("singleSignOn.sessionExpire.hostNotFound", key));
return;
}
Context context = (Context) host.findChild(key.getContextName());
if (context == null) {
containerLog.warn(sm.getString("singleSignOn.sessionExpire.contextNotFound", key));
return;
}
Manager manager = context.getManager();
if (manager == null) {
containerLog.warn(sm.getString("singleSignOn.sessionExpire.managerNotFound", key));
return;
}
Session session;
try {
session = manager.findSession(key.getSessionId());
} catch (IOException e) {
containerLog.warn(sm.getString("singleSignOn.sessionExpire.managerError", key), e);
return;
}
if (session == null) {
containerLog.warn(sm.getString("singleSignOn.sessionExpire.sessionNotFound", key));
return;
}
session.expire();
}
/**
* Attempts reauthentication to the given <code>Realm</code> using the credentials associated with the single
* sign-on session identified by argument <code>ssoId</code>.
* <p>
* If reauthentication is successful, the <code>Principal</code> and authorization type associated with the SSO
* session will be bound to the given <code>Request</code> object via calls to {@link Request#setAuthType
* Request.setAuthType()} and {@link Request#setUserPrincipal Request.setUserPrincipal()}
* </p>
*
* @param ssoId identifier of SingleSignOn session with which the caller is associated
* @param realm Realm implementation against which the caller is to be authenticated
* @param request the request that needs to be authenticated
*
* @return <code>true</code> if reauthentication was successful, <code>false</code> otherwise.
*/
protected boolean reauthenticate(String ssoId, Realm realm, Request request) {
if (ssoId == null || realm == null) {
return false;
}
boolean reauthenticated = false;
SingleSignOnEntry entry = cache.get(ssoId);
if (entry != null && entry.getCanReauthenticate()) {
String username = entry.getUsername();
if (username != null) {
Principal reauthPrincipal = realm.authenticate(username, entry.getPassword());
if (reauthPrincipal != null) {
reauthenticated = true;
// Bind the authorization credentials to the request
request.setAuthType(entry.getAuthType());
request.setUserPrincipal(reauthPrincipal);
}
}
}
return reauthenticated;
}
/**
* Register the specified Principal as being associated with the specified value for the single sign on identifier.
*
* @param ssoId Single sign on identifier to register
* @param principal Associated user principal that is identified
* @param authType Authentication type used to authenticate this user principal
* @param username Username used to authenticate this user
* @param password Password used to authenticate this user
*/
protected void register(String ssoId, Principal principal, String authType, String username, String password) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.register", ssoId,
principal != null ? principal.getName() : "", authType));
}
cache.put(ssoId, new SingleSignOnEntry(principal, authType, username, password));
}
/**
* Updates any <code>SingleSignOnEntry</code> found under key <code>ssoId</code> with the given authentication data.
* <p>
* The purpose of this method is to allow an SSO entry that was established without a username/password combination
* (i.e. established following DIGEST or CLIENT_CERT authentication) to be updated with a username and password if
* one becomes available through a subsequent BASIC or FORM authentication. The SSO entry will then be usable for
* reauthentication.
* <p>
* <b>NOTE:</b> Only updates the SSO entry if a call to <code>SingleSignOnEntry.getCanReauthenticate()</code>
* returns <code>false</code>; otherwise, it is assumed that the SSO entry already has sufficient information to
* allow reauthentication and that no update is needed.
*
* @param ssoId identifier of Single sign to be updated
* @param principal the <code>Principal</code> returned by the latest call to <code>Realm.authenticate</code>.
* @param authType the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM)
* @param username the username (if any) used for the authentication
* @param password the password (if any) used for the authentication
*
* @return <code>true</code> if the credentials were updated, otherwise <code>false</code>
*/
protected boolean update(String ssoId, Principal principal, String authType, String username, String password) {
SingleSignOnEntry sso = cache.get(ssoId);
if (sso != null && !sso.getCanReauthenticate()) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.update", ssoId, authType));
}
sso.updateCredentials(principal, authType, username, password);
return true;
}
return false;
}
/**
* Remove a single Session from a SingleSignOn. Called when a session is timed out and no longer active.
*
* @param ssoId Single sign on identifier from which to remove the session.
* @param session the session to be removed.
*/
protected void removeSession(String ssoId, Session session) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("singleSignOn.debug.removeSession", session, ssoId));
}
// Get a reference to the SingleSignOn
SingleSignOnEntry entry = cache.get(ssoId);
if (entry == null) {
return;
}
// Remove the inactive session from SingleSignOnEntry
entry.removeSession(session);
// If there are no sessions left in the SingleSignOnEntry,
// deregister the entry.
if (entry.findSessions().isEmpty()) {
deregister(ssoId);
}
}
protected SessionListener getSessionListener(String ssoId) {
return new SingleSignOnListener(ssoId);
}
@Override
protected void startInternal() throws LifecycleException {
Container c = getContainer();
while (c != null && !(c instanceof Engine)) {
c = c.getParent();
}
if (c != null) {
engine = (Engine) c;
}
super.startInternal();
}
@Override
protected void stopInternal() throws LifecycleException {
super.stopInternal();
engine = null;
}
}
|