001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.shiro.realm.ldap; 020 021import org.apache.shiro.authc.AuthenticationException; 022import org.apache.shiro.authc.AuthenticationInfo; 023import org.apache.shiro.authc.AuthenticationToken; 024import org.apache.shiro.authc.SimpleAuthenticationInfo; 025import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; 026import org.apache.shiro.authz.AuthorizationException; 027import org.apache.shiro.authz.AuthorizationInfo; 028import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException; 029import org.apache.shiro.realm.AuthorizingRealm; 030import org.apache.shiro.subject.PrincipalCollection; 031import org.apache.shiro.lang.util.StringUtils; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import javax.naming.AuthenticationNotSupportedException; 036import javax.naming.NamingException; 037import javax.naming.ldap.LdapContext; 038 039/** 040 * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's 041 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>. This is 042 * Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP 043 * support for many years. 044 * <p/> 045 * This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP 046 * authentication and authorization needs. However, if it does not suit your needs, you might want to look into 047 * creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the 048 * <a href="http://directory.apache.org/api/">Apache LDAP API</a>. 049 * <h2>Authentication</h2> 050 * During an authentication attempt, if the submitted {@code AuthenticationToken}'s 051 * {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the 052 * LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the 053 * {@link #setUserDnTemplate(String) userDnTemplate} property must be configured. If not configured, 054 * the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP 055 * environments (maybe Microsoft ActiveDirectory being the exception). 056 * <h2>Authorization</h2> 057 * By default, authorization is effectively disabled due to the default 058 * {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} implementation returning {@code null}. 059 * If you wish to perform authorization based on an LDAP schema, you must subclass this one 060 * and override that method to reflect your organization's data model. 061 * <h2>Configuration</h2> 062 * This class primarily provides the {@link #setUserDnTemplate(String) userDnTemplate} property to allow you to specify 063 * the your LDAP server's User DN format. Most other configuration is performed via the nested 064 * {@link LdapContextFactory contextFactory} property. 065 * <p/> 066 * For example, defining this realm in Shiro .ini: 067 * <pre> 068 * [main] 069 * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm 070 * ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com 071 * ldapRealm.contextFactory.url = ldap://ldapHost:389 072 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 073 * ldapRealm.contextFactory.environment[some.obscure.jndi.key] = some value 074 * ... 075 * </pre> 076 * The default {@link #setContextFactory contextFactory} instance is a {@link JndiLdapContextFactory}. See that 077 * class's JavaDoc for more information on configuring the LDAP connection as well as specifying JNDI environment 078 * properties as necessary. 079 * 080 * @see JndiLdapContextFactory 081 * @since 1.3 082 */ 083public class DefaultLdapRealm extends AuthorizingRealm { 084 085 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultLdapRealm.class); 086 087 //The zero index currently means nothing, but could be utilized in the future for other substitution techniques. 088 private static final String USERDN_SUBSTITUTION_TOKEN = "{0}"; 089 090 private String userDnPrefix; 091 private String userDnSuffix; 092 093 /*-------------------------------------------- 094 | I N S T A N C E V A R I A B L E S | 095 ============================================*/ 096 /** 097 * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime 098 * to acquire connections to the LDAP directory to perform authentication attempts and authorization queries. 099 */ 100 private LdapContextFactory contextFactory; 101 102 /*-------------------------------------------- 103 | C O N S T R U C T O R S | 104 ============================================*/ 105 106 /** 107 * Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a 108 * {@link JndiLdapContextFactory}. 109 */ 110 public DefaultLdapRealm() { 111 //Credentials Matching is not necessary - the LDAP directory will do it automatically: 112 setCredentialsMatcher(new AllowAllCredentialsMatcher()); 113 //Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token: 114 setAuthenticationTokenClass(AuthenticationToken.class); 115 this.contextFactory = new JndiLdapContextFactory(); 116 } 117 118 /*-------------------------------------------- 119 | A C C E S S O R S / M O D I F I E R S | 120 ============================================*/ 121 122 /** 123 * Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no 124 * {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that 125 * occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. 126 * 127 * @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no 128 * {@link #getUserDnTemplate() userDnTemplate} has been configured. 129 */ 130 protected String getUserDnPrefix() { 131 return userDnPrefix; 132 } 133 134 /** 135 * Returns the User DN suffix to use when building a runtime User DN value. or {@code null} if no 136 * {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that 137 * occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. 138 * 139 * @return the User DN suffix to use when building a runtime User DN value or {@code null} if no 140 * {@link #getUserDnTemplate() userDnTemplate} has been configured. 141 */ 142 protected String getUserDnSuffix() { 143 return userDnSuffix; 144 } 145 146 /*-------------------------------------------- 147 | M E T H O D S | 148 ============================================*/ 149 150 /** 151 * Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime. A User DN is an LDAP 152 * fully-qualified unique user identifier which is required to establish a connection with the LDAP 153 * directory to authenticate users and query for authorization information. 154 * <h2>Usage</h2> 155 * User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to 156 * specify the format corresponding to your directory. You do this by specifying the full User DN as normal, but 157 * but you use a <b>{@code {0}}</b> placeholder token in the string representing the location where the 158 * user's submitted principal (usually a username or uid) will be substituted at runtime. 159 * <p/> 160 * For example, if your directory 161 * uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like 162 * this: 163 * <p/> 164 * <pre>uid=jsmith,ou=users,dc=mycompany,dc=com</pre> 165 * <p/> 166 * in which case you would set this property with the following template value: 167 * <p/> 168 * <pre>uid=<b>{0}</b>,ou=users,dc=mycompany,dc=com</pre> 169 * <p/> 170 * If no template is configured, the raw {@code AuthenticationToken} 171 * {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal. This is likely 172 * incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username. So, 173 * ensure you set this property to match your environment! 174 * 175 * @param template the User Distinguished Name template to use for runtime substitution 176 * @throws IllegalArgumentException if the template is null, empty, or does not contain the 177 * {@code {0}} substitution token. 178 * @see LdapContextFactory#getLdapContext(Object, Object) 179 */ 180 public void setUserDnTemplate(String template) throws IllegalArgumentException { 181 if (!StringUtils.hasText(template)) { 182 String msg = "User DN template cannot be null or empty."; 183 throw new IllegalArgumentException(msg); 184 } 185 int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN); 186 if (index < 0) { 187 String msg = "User DN template must contain the '" 188 + USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " 189 + "insert the runtime authentication principal."; 190 throw new IllegalArgumentException(msg); 191 } 192 String prefix = template.substring(0, index); 193 String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length()); 194 if (LOGGER.isDebugEnabled()) { 195 LOGGER.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix); 196 } 197 this.userDnPrefix = prefix; 198 this.userDnSuffix = suffix; 199 } 200 201 /** 202 * Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the 203 * {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation. 204 * 205 * @return the User Distinguished Name (DN) template to use when creating User DNs at runtime. 206 */ 207 public String getUserDnTemplate() { 208 return getUserDn(USERDN_SUBSTITUTION_TOKEN); 209 } 210 211 /** 212 * Returns the LDAP User Distinguished Name (DN) to use when acquiring an 213 * {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}. 214 * <p/> 215 * If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct 216 * the User DN by substituting the specified {@code principal} into the configured template. If the 217 * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly 218 * (indicating that the submitted authentication token principal <em>is</em> the User DN). 219 * 220 * @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}. 221 * @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}. 222 * @throws IllegalArgumentException if the method argument is null or empty 223 * @throws IllegalStateException if the {@link #getUserDnTemplate userDnTemplate} has not been set. 224 * @see LdapContextFactory#getLdapContext(Object, Object) 225 */ 226 protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException { 227 if (!StringUtils.hasText(principal)) { 228 throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction."); 229 } 230 String prefix = getUserDnPrefix(); 231 String suffix = getUserDnSuffix(); 232 if (prefix == null && suffix == null) { 233 LOGGER.debug("userDnTemplate property has not been configured, indicating the submitted " 234 + "AuthenticationToken's principal is the same as the User DN. Returning the method argument " 235 + "as is."); 236 return principal; 237 } 238 239 int prefixLength = prefix != null ? prefix.length() : 0; 240 int suffixLength = suffix != null ? suffix.length() : 0; 241 StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength); 242 if (prefixLength > 0) { 243 sb.append(prefix); 244 } 245 sb.append(principal); 246 if (suffixLength > 0) { 247 sb.append(suffix); 248 } 249 return sb.toString(); 250 } 251 252 /** 253 * Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication 254 * attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} 255 * instance. 256 * 257 * @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during 258 * authentication attempts and authorization queries 259 */ 260 @SuppressWarnings({"UnusedDeclaration"}) 261 public void setContextFactory(LdapContextFactory contextFactory) { 262 this.contextFactory = contextFactory; 263 } 264 265 /** 266 * Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication 267 * attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} 268 * instance. 269 * 270 * @return the LdapContextFactory instance used to acquire connections to the LDAP directory during 271 * authentication attempts and authorization queries 272 */ 273 public LdapContextFactory getContextFactory() { 274 return this.contextFactory; 275 } 276 277 /*-------------------------------------------- 278 | M E T H O D S | 279 ============================================*/ 280 281 /** 282 * Delegates to {@link #queryForAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, LdapContextFactory)}, 283 * wrapping any {@link NamingException}s in a Shiro {@link AuthenticationException} to satisfy the parent method 284 * signature. 285 * 286 * @param token the authentication token containing the user's principal and credentials. 287 * @return the {@link AuthenticationInfo} acquired after a successful authentication attempt 288 * @throws AuthenticationException if the authentication attempt fails or if a 289 * {@link NamingException} occurs. 290 */ 291 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 292 AuthenticationInfo info; 293 try { 294 info = queryForAuthenticationInfo(token, getContextFactory()); 295 } catch (AuthenticationNotSupportedException e) { 296 String msg = "Unsupported configured authentication mechanism"; 297 throw new UnsupportedAuthenticationMechanismException(msg, e); 298 } catch (javax.naming.AuthenticationException e) { 299 throw new AuthenticationException("LDAP authentication failed.", e); 300 } catch (NamingException e) { 301 String msg = "LDAP naming error while attempting to authenticate user."; 302 throw new AuthenticationException(msg, e); 303 } 304 305 return info; 306 } 307 308 309 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 310 AuthorizationInfo info; 311 try { 312 info = queryForAuthorizationInfo(principals, getContextFactory()); 313 } catch (NamingException e) { 314 String msg = "LDAP naming error while attempting to retrieve authorization for user [" + principals + "]."; 315 throw new AuthorizationException(msg, e); 316 } 317 318 return info; 319 } 320 321 /** 322 * Returns the principal to use when creating the LDAP connection for an authentication attempt. 323 * <p/> 324 * This implementation uses a heuristic: it checks to see if the specified token's 325 * {@link AuthenticationToken#getPrincipal() principal} is a {@code String}, and if so, 326 * {@link #getUserDn(String) converts it} from what is 327 * assumed to be a raw uid or username {@code String} into a User DN {@code String}. Almost all LDAP directories 328 * expect the authentication connection to present a User DN and not an unqualified username or uid. 329 * <p/> 330 * If the token's {@code principal} is not a String, it is assumed to already be in the format supported by the 331 * underlying {@link LdapContextFactory} implementation and the raw principal is returned directly. 332 * 333 * @param token the {@link AuthenticationToken} submitted during the authentication process 334 * @return the User DN or raw principal to use to acquire the LdapContext. 335 * @see LdapContextFactory#getLdapContext(Object, Object) 336 */ 337 protected Object getLdapPrincipal(AuthenticationToken token) { 338 Object principal = token.getPrincipal(); 339 if (principal instanceof String) { 340 String sPrincipal = (String) principal; 341 return getUserDn(sPrincipal); 342 } 343 return principal; 344 } 345 346 /** 347 * This implementation opens an LDAP connection using the token's 348 * {@link #getLdapPrincipal(org.apache.shiro.authc.AuthenticationToken) discovered principal} and provided 349 * {@link AuthenticationToken#getCredentials() credentials}. If the connection opens successfully, the 350 * authentication attempt is immediately considered successful and a new 351 * {@link AuthenticationInfo} instance is 352 * {@link #createAuthenticationInfo(AuthenticationToken, Object, Object, LdapContext) created} 353 * and returned. If the connection cannot be opened, either because LDAP authentication failed or some other 354 * JNDI problem, an {@link NamingException} will be thrown. 355 * 356 * @param token the submitted authentication token that triggered the authentication attempt. 357 * @param ldapContextFactory factory used to retrieve LDAP connections. 358 * @return an {@link AuthenticationInfo} instance representing the authenticated user's information. 359 * @throws NamingException if any LDAP errors occur. 360 */ 361 protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, 362 LdapContextFactory ldapContextFactory) 363 throws NamingException { 364 365 Object principal = token.getPrincipal(); 366 Object credentials = token.getCredentials(); 367 368 LOGGER.debug("Authenticating user '{}' through LDAP", principal); 369 370 principal = getLdapPrincipal(token); 371 372 LdapContext ctx = null; 373 try { 374 ctx = ldapContextFactory.getLdapContext(principal, credentials); 375 //context was opened successfully, which means their credentials were valid. Return the AuthenticationInfo: 376 return createAuthenticationInfo(token, principal, credentials, ctx); 377 } finally { 378 LdapUtils.closeContext(ctx); 379 } 380 } 381 382 /** 383 * Returns the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt. 384 * <p/> 385 * This implementation ignores the {@code ldapPrincipal}, {@code ldapCredentials}, and the opened 386 * {@code ldapContext} arguments and merely returns an {@code AuthenticationInfo} instance mirroring the 387 * submitted token's principal and credentials. This is acceptable because this method is only ever invoked after 388 * a successful authentication attempt, which means the provided principal and credentials were correct, and can 389 * be used directly to populate the (now verified) {@code AuthenticationInfo}. 390 * <p/> 391 * Subclasses however are free to override this method for more advanced construction logic. 392 * 393 * @param token the submitted {@code AuthenticationToken} that resulted in a successful authentication 394 * @param ldapPrincipal the LDAP principal used when creating the LDAP connection. Unlike the token's 395 * {@link AuthenticationToken#getPrincipal() principal}, this value is usually a constructed 396 * User DN and not a simple username or uid. The exact value is depending on the 397 * configured 398 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> 399 * LDAP authentication mechanism</a> in use. 400 * @param ldapCredentials the LDAP credentials used when creating the LDAP connection. 401 * @param ldapContext the LdapContext created that resulted in a successful authentication. It can be used 402 * further by subclasses for more complex operations. It does not need to be closed - 403 * it will be closed automatically after this method returns. 404 * @return the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt. 405 * @throws NamingException if there was any problem using the {@code LdapContext} 406 */ 407 @SuppressWarnings({"UnusedDeclaration"}) 408 protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, Object ldapPrincipal, 409 Object ldapCredentials, LdapContext ldapContext) 410 throws NamingException { 411 return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName()); 412 } 413 414 415 /** 416 * Method that should be implemented by subclasses to build an 417 * {@link AuthorizationInfo} object by querying the LDAP context for the 418 * specified principal.</p> 419 * 420 * @param principals the principals of the Subject whose AuthenticationInfo should be queried from the LDAP server. 421 * @param ldapContextFactory factory used to retrieve LDAP connections. 422 * @return an {@link AuthorizationInfo} instance containing information retrieved from the LDAP server. 423 * @throws NamingException if any LDAP errors occur during the search. 424 */ 425 protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, 426 LdapContextFactory ldapContextFactory) throws NamingException { 427 return null; 428 } 429}