1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.shiro.authc.pam; 20 21 import org.apache.shiro.authc.*; 22 import org.apache.shiro.realm.Realm; 23 import org.apache.shiro.subject.PrincipalCollection; 24 import org.apache.shiro.util.CollectionUtils; 25 import org.slf4j.Logger; 26 import org.slf4j.LoggerFactory; 27 28 import java.util.Collection; 29 30 /** 31 * A {@code ModularRealmAuthenticator} delegates account lookups to a pluggable (modular) collection of 32 * {@link Realm}s. This enables PAM (Pluggable Authentication Module) behavior in Shiro. 33 * In addition to authorization duties, a Shiro Realm can also be thought of a PAM 'module'. 34 * <p/> 35 * Using this Authenticator allows you to "plug-in" your own 36 * {@code Realm}s as you see fit. Common realms are those based on accessing 37 * LDAP, relational databases, file systems, etc. 38 * <p/> 39 * If only one realm is configured (this is often the case for most applications), authentication success is naturally 40 * only dependent upon invoking this one Realm's 41 * {@link Realm#getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} method. 42 * <p/> 43 * But if two or more realms are configured, PAM behavior is implemented by iterating over the collection of realms 44 * and interacting with each over the course of the authentication attempt. As this is more complicated, this 45 * authenticator allows customized behavior for interpreting what happens when interacting with multiple realms - for 46 * example, you might require all realms to be successful during the attempt, or perhaps only at least one must be 47 * successful, or some other interpretation. This customized behavior can be performed via the use of a 48 * {@link #setAuthenticationStrategy(AuthenticationStrategy) AuthenticationStrategy}, which 49 * you can inject as a property of this class. 50 * <p/> 51 * The strategy object provides callback methods that allow you to 52 * determine what constitutes a success or failure in a multi-realm (PAM) scenario. And because this only makes sense 53 * in a multi-realm scenario, the strategy object is only utilized when more than one Realm is configured. 54 * <p/> 55 * As most multi-realm applications require at least one Realm authenticates successfully, the default 56 * implementation is the {@link AtLeastOneSuccessfulStrategy}. 57 * 58 * @see #setRealms 59 * @see AtLeastOneSuccessfulStrategy 60 * @see AllSuccessfulStrategy 61 * @see FirstSuccessfulStrategy 62 * @since 0.1 63 */ 64 public class ModularRealmAuthenticator extends AbstractAuthenticator { 65 66 /*-------------------------------------------- 67 | C O N S T A N T S | 68 ============================================*/ 69 private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class); 70 71 /*-------------------------------------------- 72 | I N S T A N C E V A R I A B L E S | 73 ============================================*/ 74 /** 75 * List of realms that will be iterated through when a user authenticates. 76 */ 77 private Collection<Realm> realms; 78 79 /** 80 * The authentication strategy to use during authentication attempts, defaults to a 81 * {@link org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy} instance. 82 */ 83 private AuthenticationStrategy authenticationStrategy; 84 85 /*-------------------------------------------- 86 | C O N S T R U C T O R S | 87 ============================================*/ 88 89 /** 90 * Default no-argument constructor which 91 * {@link #setAuthenticationStrategy(AuthenticationStrategy) enables} an 92 * {@link org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy} by default. 93 */ 94 public ModularRealmAuthenticator() { 95 this.authenticationStrategy = new AtLeastOneSuccessfulStrategy(); 96 } 97 98 /*-------------------------------------------- 99 | A C C E S S O R S / M O D I F I E R S | 100 ============================================*/ 101 102 /** 103 * Sets all realms used by this Authenticator, providing PAM (Pluggable Authentication Module) configuration. 104 * 105 * @param realms the realms to consult during authentication attempts. 106 */ 107 public void setRealms(Collection<Realm> realms) { 108 this.realms = realms; 109 } 110 111 /** 112 * Returns the realm(s) used by this {@code Authenticator} during an authentication attempt. 113 * 114 * @return the realm(s) used by this {@code Authenticator} during an authentication attempt. 115 */ 116 protected Collection<Realm> getRealms() { 117 return this.realms; 118 } 119 120 /** 121 * Returns the {@code AuthenticationStrategy} utilized by this modular authenticator during a multi-realm 122 * log-in attempt. This object is only used when two or more Realms are configured. 123 * <p/> 124 * Unless overridden by 125 * the {@link #setAuthenticationStrategy(AuthenticationStrategy)} method, the default implementation 126 * is the {@link org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy}. 127 * 128 * @return the {@code AuthenticationStrategy} utilized by this modular authenticator during a log-in attempt. 129 * @since 0.2 130 */ 131 public AuthenticationStrategy getAuthenticationStrategy() { 132 return authenticationStrategy; 133 } 134 135 /** 136 * Allows overriding the default {@code AuthenticationStrategy} utilized during multi-realm log-in attempts. 137 * This object is only used when two or more Realms are configured. 138 * 139 * @param authenticationStrategy the strategy implementation to use during log-in attempts. 140 * @since 0.2 141 */ 142 public void setAuthenticationStrategy(AuthenticationStrategy authenticationStrategy) { 143 this.authenticationStrategy = authenticationStrategy; 144 } 145 146 /*-------------------------------------------- 147 | M E T H O D S | 148 149 /** 150 * Used by the internal {@link #doAuthenticate} implementation to ensure that the {@code realms} property 151 * has been set. The default implementation ensures the property is not null and not empty. 152 * 153 * @throws IllegalStateException if the {@code realms} property is configured incorrectly. 154 */ 155 156 protected void assertRealmsConfigured() throws IllegalStateException { 157 Collection<Realm> realms = getRealms(); 158 if (CollectionUtils.isEmpty(realms)) { 159 String msg = "Configuration error: No realms have been configured! One or more realms must be " + 160 "present to execute an authentication attempt."; 161 throw new IllegalStateException(msg); 162 } 163 } 164 165 /** 166 * Performs the authentication attempt by interacting with the single configured realm, which is significantly 167 * simpler than performing multi-realm logic. 168 * 169 * @param realm the realm to consult for AuthenticationInfo. 170 * @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials. 171 * @return the AuthenticationInfo associated with the user account corresponding to the specified {@code token} 172 */ 173 protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { 174 if (!realm.supports(token)) { 175 String msg = "Realm [" + realm + "] does not support authentication token [" + 176 token + "]. Please ensure that the appropriate Realm implementation is " + 177 "configured correctly or that the realm accepts AuthenticationTokens of this type."; 178 throw new UnsupportedTokenException(msg); 179 } 180 AuthenticationInfo info = realm.getAuthenticationInfo(token); 181 if (info == null) { 182 String msg = "Realm [" + realm + "] was unable to find account data for the " + 183 "submitted AuthenticationToken [" + token + "]."; 184 throw new UnknownAccountException(msg); 185 } 186 return info; 187 } 188 189 /** 190 * Performs the multi-realm authentication attempt by calling back to a {@link AuthenticationStrategy} object 191 * as each realm is consulted for {@code AuthenticationInfo} for the specified {@code token}. 192 * 193 * @param realms the multiple realms configured on this Authenticator instance. 194 * @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials. 195 * @return an aggregated AuthenticationInfo instance representing account data across all the successfully 196 * consulted realms. 197 */ 198 protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { 199 200 AuthenticationStrategy strategy = getAuthenticationStrategy(); 201 202 AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); 203 204 if (log.isTraceEnabled()) { 205 log.trace("Iterating through {} realms for PAM authentication", realms.size()); 206 } 207 208 for (Realm realm : realms) { 209 210 try { 211 aggregate = strategy.beforeAttempt(realm, token, aggregate); 212 } catch (ShortCircuitIterationException shortCircuitSignal) { 213 // Break from continuing with subsequnet realms on receiving 214 // short circuit signal from strategy 215 break; 216 } 217 218 if (realm.supports(token)) { 219 220 log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); 221 222 AuthenticationInfo info = null; 223 Throwable t = null; 224 try { 225 info = realm.getAuthenticationInfo(token); 226 } catch (Throwable throwable) { 227 t = throwable; 228 if (log.isDebugEnabled()) { 229 String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; 230 log.debug(msg, t); 231 } 232 } 233 234 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); 235 236 } else { 237 log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); 238 } 239 } 240 241 aggregate = strategy.afterAllAttempts(token, aggregate); 242 243 return aggregate; 244 } 245 246 247 /** 248 * Attempts to authenticate the given token by iterating over the internal collection of 249 * {@link Realm}s. For each realm, first the {@link Realm#supports(org.apache.shiro.authc.AuthenticationToken)} 250 * method will be called to determine if the realm supports the {@code authenticationToken} method argument. 251 * <p/> 252 * If a realm does support 253 * the token, its {@link Realm#getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} 254 * method will be called. If the realm returns a non-null account, the token will be 255 * considered authenticated for that realm and the account data recorded. If the realm returns {@code null}, 256 * the next realm will be consulted. If no realms support the token or all supporting realms return null, 257 * an {@link AuthenticationException} will be thrown to indicate that the user could not be authenticated. 258 * <p/> 259 * After all realms have been consulted, the information from each realm is aggregated into a single 260 * {@link AuthenticationInfo} object and returned. 261 * 262 * @param authenticationToken the token containing the authentication principal and credentials for the 263 * user being authenticated. 264 * @return account information attributed to the authenticated user. 265 * @throws IllegalStateException if no realms have been configured at the time this method is invoked 266 * @throws AuthenticationException if the user could not be authenticated or the user is denied authentication 267 * for the given principal and credentials. 268 */ 269 protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { 270 assertRealmsConfigured(); 271 Collection<Realm> realms = getRealms(); 272 if (realms.size() == 1) { 273 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); 274 } else { 275 return doMultiRealmAuthentication(realms, authenticationToken); 276 } 277 } 278 279 /** 280 * First calls <code>super.onLogout(principals)</code> to ensure a logout notification is issued, and for each 281 * wrapped {@code Realm} that implements the {@link LogoutAware LogoutAware} interface, calls 282 * <code>((LogoutAware)realm).onLogout(principals)</code> to allow each realm the opportunity to perform 283 * logout/cleanup operations during an user-logout. 284 * <p/> 285 * Shiro's Realm implementations all implement the {@code LogoutAware} interface by default and can be 286 * overridden for realm-specific logout logic. 287 * 288 * @param principals the application-specific Subject/user identifier. 289 */ 290 public void onLogout(PrincipalCollection principals) { 291 super.onLogout(principals); 292 Collection<Realm> realms = getRealms(); 293 if (!CollectionUtils.isEmpty(realms)) { 294 for (Realm realm : realms) { 295 if (realm instanceof LogoutAware) { 296 ((LogoutAware) realm).onLogout(principals); 297 } 298 } 299 } 300 } 301 }