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.realm.ldap; 20 21 import org.apache.shiro.authc.AuthenticationException; 22 import org.apache.shiro.authc.AuthenticationInfo; 23 import org.apache.shiro.authc.AuthenticationToken; 24 import org.apache.shiro.authc.SimpleAuthenticationInfo; 25 import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; 26 import org.apache.shiro.authz.AuthorizationException; 27 import org.apache.shiro.authz.AuthorizationInfo; 28 import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException; 29 import org.apache.shiro.realm.AuthorizingRealm; 30 import org.apache.shiro.subject.PrincipalCollection; 31 import org.apache.shiro.util.StringUtils; 32 import org.slf4j.Logger; 33 import org.slf4j.LoggerFactory; 34 35 import javax.naming.AuthenticationNotSupportedException; 36 import javax.naming.NamingException; 37 import javax.naming.ldap.LdapContext; 38 39 /** 40 * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's 41 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>. This is 42 * Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP 43 * support for many years. 44 * <p/> 45 * This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP 46 * authentication and authorization needs. However, if it does not suit your needs, you might want to look into 47 * creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the 48 * <a href="http://directory.apache.org/api/">Apache LDAP API</a>. 49 * <h2>Authentication</h2> 50 * During an authentication attempt, if the submitted {@code AuthenticationToken}'s 51 * {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the 52 * LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the 53 * {@link #setUserDnTemplate(String) userDnTemplate} property must be configured. If not configured, 54 * the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP 55 * environments (maybe Microsoft ActiveDirectory being the exception). 56 * <h2>Authorization</h2> 57 * By default, authorization is effectively disabled due to the default 58 * {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} implementation returning {@code null}. 59 * If you wish to perform authorization based on an LDAP schema, you must subclass this one 60 * and override that method to reflect your organization's data model. 61 * <h2>Configuration</h2> 62 * This class primarily provides the {@link #setUserDnTemplate(String) userDnTemplate} property to allow you to specify 63 * the your LDAP server's User DN format. Most other configuration is performed via the nested 64 * {@link LdapContextFactory contextFactory} property. 65 * <p/> 66 * For example, defining this realm in Shiro .ini: 67 * <pre> 68 * [main] 69 * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm 70 * ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com 71 * ldapRealm.contextFactory.url = ldap://ldapHost:389 72 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 73 * ldapRealm.contextFactory.environment[some.obscure.jndi.key] = some value 74 * ... 75 * </pre> 76 * The default {@link #setContextFactory contextFactory} instance is a {@link JndiLdapContextFactory}. See that 77 * class's JavaDoc for more information on configuring the LDAP connection as well as specifying JNDI environment 78 * properties as necessary. 79 * 80 * @see JndiLdapContextFactory 81 * 82 * @since 1.3 83 */ 84 public class DefaultLdapRealm extends AuthorizingRealm { 85 86 private static final Logger log = LoggerFactory.getLogger(DefaultLdapRealm.class); 87 88 //The zero index currently means nothing, but could be utilized in the future for other substitution techniques. 89 private static final String USERDN_SUBSTITUTION_TOKEN = "{0}"; 90 91 private String userDnPrefix; 92 private String userDnSuffix; 93 94 /*-------------------------------------------- 95 | I N S T A N C E V A R I A B L E S | 96 ============================================*/ 97 /** 98 * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime 99 * to acquire connections to the LDAP directory to perform authentication attempts and authorizatino queries. 100 */ 101 private LdapContextFactory contextFactory; 102 103 /*-------------------------------------------- 104 | C O N S T R U C T O R S | 105 ============================================*/ 106 107 /** 108 * Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a 109 * {@link JndiLdapContextFactory}. 110 */ 111 public DefaultLdapRealm() { 112 //Credentials Matching is not necessary - the LDAP directory will do it automatically: 113 setCredentialsMatcher(new AllowAllCredentialsMatcher()); 114 //Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token: 115 setAuthenticationTokenClass(AuthenticationToken.class); 116 this.contextFactory = new JndiLdapContextFactory(); 117 } 118 119 /*-------------------------------------------- 120 | A C C E S S O R S / M O D I F I E R S | 121 ============================================*/ 122 123 /** 124 * Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no 125 * {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that 126 * occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. 127 * 128 * @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no 129 * {@link #getUserDnTemplate() userDnTemplate} has been configured. 130 */ 131 protected String getUserDnPrefix() { 132 return userDnPrefix; 133 } 134 135 /** 136 * Returns the User DN suffix to use when building a runtime User DN value. or {@code null} if no 137 * {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that 138 * occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. 139 * 140 * @return the User DN suffix to use when building a runtime User DN value or {@code null} if no 141 * {@link #getUserDnTemplate() userDnTemplate} has been configured. 142 */ 143 protected String getUserDnSuffix() { 144 return userDnSuffix; 145 } 146 147 /*-------------------------------------------- 148 | M E T H O D S | 149 ============================================*/ 150 151 /** 152 * Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime. A User DN is an LDAP 153 * fully-qualified unique user identifier which is required to establish a connection with the LDAP 154 * directory to authenticate users and query for authorization information. 155 * <h2>Usage</h2> 156 * User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to 157 * specify the format corresponding to your directory. You do this by specifying the full User DN as normal, but 158 * but you use a <b>{@code {0}}</b> placeholder token in the string representing the location where the 159 * user's submitted principal (usually a username or uid) will be substituted at runtime. 160 * <p/> 161 * For example, if your directory 162 * uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like 163 * this: 164 * <p/> 165 * <pre>uid=jsmith,ou=users,dc=mycompany,dc=com</pre> 166 * <p/> 167 * in which case you would set this property with the following template value: 168 * <p/> 169 * <pre>uid=<b>{0}</b>,ou=users,dc=mycompany,dc=com</pre> 170 * <p/> 171 * If no template is configured, the raw {@code AuthenticationToken} 172 * {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal. This is likely 173 * incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username. So, 174 * ensure you set this property to match your environment! 175 * 176 * @param template the User Distinguished Name template to use for runtime substitution 177 * @throws IllegalArgumentException if the template is null, empty, or does not contain the 178 * {@code {0}} substitution token. 179 * @see LdapContextFactory#getLdapContext(Object,Object) 180 */ 181 public void setUserDnTemplate(String template) throws IllegalArgumentException { 182 if (!StringUtils.hasText(template)) { 183 String msg = "User DN template cannot be null or empty."; 184 throw new IllegalArgumentException(msg); 185 } 186 int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN); 187 if (index < 0) { 188 String msg = "User DN template must contain the '" + 189 USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " + 190 "insert the runtime authentication principal."; 191 throw new IllegalArgumentException(msg); 192 } 193 String prefix = template.substring(0, index); 194 String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length()); 195 if (log.isDebugEnabled()) { 196 log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix); 197 } 198 this.userDnPrefix = prefix; 199 this.userDnSuffix = suffix; 200 } 201 202 /** 203 * Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the 204 * {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation. 205 * 206 * @return the User Distinguished Name (DN) template to use when creating User DNs at runtime. 207 */ 208 public String getUserDnTemplate() { 209 return getUserDn(USERDN_SUBSTITUTION_TOKEN); 210 } 211 212 /** 213 * Returns the LDAP User Distinguished Name (DN) to use when acquiring an 214 * {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}. 215 * <p/> 216 * If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct 217 * the User DN by substituting the specified {@code principal} into the configured template. If the 218 * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly 219 * (indicating that the submitted authentication token principal <em>is</em> the User DN). 220 * 221 * @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}. 222 * @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}. 223 * @throws IllegalArgumentException if the method argument is null or empty 224 * @throws IllegalStateException if the {@link #getUserDnTemplate userDnTemplate} has not been set. 225 * @see LdapContextFactory#getLdapContext(Object, Object) 226 */ 227 protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException { 228 if (!StringUtils.hasText(principal)) { 229 throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction."); 230 } 231 String prefix = getUserDnPrefix(); 232 String suffix = getUserDnSuffix(); 233 if (prefix == null && suffix == null) { 234 log.debug("userDnTemplate property has not been configured, indicating the submitted " + 235 "AuthenticationToken's principal is the same as the User DN. Returning the method argument " + 236 "as is."); 237 return principal; 238 } 239 240 int prefixLength = prefix != null ? prefix.length() : 0; 241 int suffixLength = suffix != null ? suffix.length() : 0; 242 StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength); 243 if (prefixLength > 0) { 244 sb.append(prefix); 245 } 246 sb.append(principal); 247 if (suffixLength > 0) { 248 sb.append(suffix); 249 } 250 return sb.toString(); 251 } 252 253 /** 254 * Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication 255 * attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} 256 * instance. 257 * 258 * @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during 259 * authentication attempts and authorization queries 260 */ 261 @SuppressWarnings({"UnusedDeclaration"}) 262 public void setContextFactory(LdapContextFactory contextFactory) { 263 this.contextFactory = contextFactory; 264 } 265 266 /** 267 * Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication 268 * attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} 269 * instance. 270 * 271 * @return the LdapContextFactory instance used to acquire connections to the LDAP directory during 272 * authentication attempts and authorization queries 273 */ 274 public LdapContextFactory getContextFactory() { 275 return this.contextFactory; 276 } 277 278 /*-------------------------------------------- 279 | M E T H O D S | 280 ============================================*/ 281 282 /** 283 * Delegates to {@link #queryForAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, LdapContextFactory)}, 284 * wrapping any {@link NamingException}s in a Shiro {@link AuthenticationException} to satisfy the parent method 285 * signature. 286 * 287 * @param token the authentication token containing the user's principal and credentials. 288 * @return the {@link AuthenticationInfo} acquired after a successful authentication attempt 289 * @throws AuthenticationException if the authentication attempt fails or if a 290 * {@link NamingException} occurs. 291 */ 292 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 293 AuthenticationInfo info; 294 try { 295 info = queryForAuthenticationInfo(token, getContextFactory()); 296 } catch (AuthenticationNotSupportedException e) { 297 String msg = "Unsupported configured authentication mechanism"; 298 throw new UnsupportedAuthenticationMechanismException(msg, e); 299 } catch (javax.naming.AuthenticationException e) { 300 throw new AuthenticationException("LDAP authentication failed.", e); 301 } catch (NamingException e) { 302 String msg = "LDAP naming error while attempting to authenticate user."; 303 throw new AuthenticationException(msg, e); 304 } 305 306 return info; 307 } 308 309 310 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 311 AuthorizationInfo info; 312 try { 313 info = queryForAuthorizationInfo(principals, getContextFactory()); 314 } catch (NamingException e) { 315 String msg = "LDAP naming error while attempting to retrieve authorization for user [" + principals + "]."; 316 throw new AuthorizationException(msg, e); 317 } 318 319 return info; 320 } 321 322 /** 323 * Returns the principal to use when creating the LDAP connection for an authentication attempt. 324 * <p/> 325 * This implementation uses a heuristic: it checks to see if the specified token's 326 * {@link AuthenticationToken#getPrincipal() principal} is a {@code String}, and if so, 327 * {@link #getUserDn(String) converts it} from what is 328 * assumed to be a raw uid or username {@code String} into a User DN {@code String}. Almost all LDAP directories 329 * expect the authentication connection to present a User DN and not an unqualified username or uid. 330 * <p/> 331 * If the token's {@code principal} is not a String, it is assumed to already be in the format supported by the 332 * underlying {@link LdapContextFactory} implementation and the raw principal is returned directly. 333 * 334 * @param token the {@link AuthenticationToken} submitted during the authentication process 335 * @return the User DN or raw principal to use to acquire the LdapContext. 336 * @see LdapContextFactory#getLdapContext(Object, Object) 337 */ 338 protected Object getLdapPrincipal(AuthenticationToken token) { 339 Object principal = token.getPrincipal(); 340 if (principal instanceof String) { 341 String sPrincipal = (String) principal; 342 return getUserDn(sPrincipal); 343 } 344 return principal; 345 } 346 347 /** 348 * This implementation opens an LDAP connection using the token's 349 * {@link #getLdapPrincipal(org.apache.shiro.authc.AuthenticationToken) discovered principal} and provided 350 * {@link AuthenticationToken#getCredentials() credentials}. If the connection opens successfully, the 351 * authentication attempt is immediately considered successful and a new 352 * {@link AuthenticationInfo} instance is 353 * {@link #createAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, Object, Object, javax.naming.ldap.LdapContext) created} 354 * and returned. If the connection cannot be opened, either because LDAP authentication failed or some other 355 * JNDI problem, an {@link NamingException} will be thrown. 356 * 357 * @param token the submitted authentication token that triggered the authentication attempt. 358 * @param ldapContextFactory factory used to retrieve LDAP connections. 359 * @return an {@link AuthenticationInfo} instance representing the authenticated user's information. 360 * @throws NamingException if any LDAP errors occur. 361 */ 362 protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, 363 LdapContextFactory ldapContextFactory) 364 throws NamingException { 365 366 Object principal = token.getPrincipal(); 367 Object credentials = token.getCredentials(); 368 369 log.debug("Authenticating user '{}' through LDAP", principal); 370 371 principal = getLdapPrincipal(token); 372 373 LdapContext ctx = null; 374 try { 375 ctx = ldapContextFactory.getLdapContext(principal, credentials); 376 //context was opened successfully, which means their credentials were valid. Return the AuthenticationInfo: 377 return createAuthenticationInfo(token, principal, credentials, ctx); 378 } finally { 379 LdapUtils.closeContext(ctx); 380 } 381 } 382 383 /** 384 * Returns the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt. 385 * <p/> 386 * This implementation ignores the {@code ldapPrincipal}, {@code ldapCredentials}, and the opened 387 * {@code ldapContext} arguments and merely returns an {@code AuthenticationInfo} instance mirroring the 388 * submitted token's principal and credentials. This is acceptable because this method is only ever invoked after 389 * a successful authentication attempt, which means the provided principal and credentials were correct, and can 390 * be used directly to populate the (now verified) {@code AuthenticationInfo}. 391 * <p/> 392 * Subclasses however are free to override this method for more advanced construction logic. 393 * 394 * @param token the submitted {@code AuthenticationToken} that resulted in a successful authentication 395 * @param ldapPrincipal the LDAP principal used when creating the LDAP connection. Unlike the token's 396 * {@link AuthenticationToken#getPrincipal() principal}, this value is usually a constructed 397 * User DN and not a simple username or uid. The exact value is depending on the 398 * configured 399 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> 400 * LDAP authentication mechanism</a> in use. 401 * @param ldapCredentials the LDAP credentials used when creating the LDAP connection. 402 * @param ldapContext the LdapContext created that resulted in a successful authentication. It can be used 403 * further by subclasses for more complex operations. It does not need to be closed - 404 * it will be closed automatically after this method returns. 405 * @return the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt. 406 * @throws NamingException if there was any problem using the {@code LdapContext} 407 */ 408 @SuppressWarnings({"UnusedDeclaration"}) 409 protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, Object ldapPrincipal, 410 Object ldapCredentials, LdapContext ldapContext) 411 throws NamingException { 412 return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName()); 413 } 414 415 416 /** 417 * Method that should be implemented by subclasses to build an 418 * {@link AuthorizationInfo} object by querying the LDAP context for the 419 * specified principal.</p> 420 * 421 * @param principals the principals of the Subject whose AuthenticationInfo should be queried from the LDAP server. 422 * @param ldapContextFactory factory used to retrieve LDAP connections. 423 * @return an {@link AuthorizationInfo} instance containing information retrieved from the LDAP server. 424 * @throws NamingException if any LDAP errors occur during the search. 425 */ 426 protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, 427 LdapContextFactory ldapContextFactory) throws NamingException { 428 return null; 429 } 430 }