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 * 019 */ 020package org.apache.directory.server.ldap.handlers.sasl.plain; 021 022 023import java.io.IOException; 024 025import javax.naming.InvalidNameException; 026import javax.security.sasl.SaslException; 027 028import org.apache.directory.api.ldap.model.constants.SchemaConstants; 029import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms; 030import org.apache.directory.api.ldap.model.entry.Entry; 031import org.apache.directory.api.ldap.model.entry.Value; 032import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException; 033import org.apache.directory.api.ldap.model.filter.EqualityNode; 034import org.apache.directory.api.ldap.model.message.BindRequest; 035import org.apache.directory.api.ldap.model.message.SearchScope; 036import org.apache.directory.api.ldap.model.schema.PrepareString; 037import org.apache.directory.api.util.Strings; 038import org.apache.directory.server.core.api.CoreSession; 039import org.apache.directory.server.core.api.DirectoryService; 040import org.apache.directory.server.core.api.OperationEnum; 041import org.apache.directory.server.core.api.OperationManager; 042import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; 043import org.apache.directory.server.core.api.interceptor.context.BindOperationContext; 044import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; 045import org.apache.directory.server.i18n.I18n; 046import org.apache.directory.server.ldap.LdapServer; 047import org.apache.directory.server.ldap.LdapSession; 048import org.apache.directory.server.ldap.handlers.sasl.AbstractSaslServer; 049 050 051/** 052 * A SaslServer implementation for PLAIN based SASL mechanism. This is 053 * required unfortunately because the JDK's SASL provider does not support 054 * this mechanism. 055 * 056 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 057 */ 058public final class PlainSaslServer extends AbstractSaslServer 059{ 060 /** The authzid property stored into the LdapSession instance */ 061 public static final String SASL_PLAIN_AUTHZID = "authzid"; 062 063 /** The authcid property stored into the LdapSession instance */ 064 public static final String SASL_PLAIN_AUTHCID = "authcid"; 065 066 /** The password property stored into the LdapSession instance */ 067 public static final String SASL_PLAIN_PASSWORD = "password"; 068 069 /** 070 * The possible states for the negotiation of a PLAIN mechanism. 071 */ 072 private enum NegotiationState 073 { 074 INITIALIZED, // Negotiation has just started 075 MECH_RECEIVED, // We have received the PLAIN mechanism 076 COMPLETED // The user/password have been received 077 } 078 079 /** 080 * The different state used by the iInitialResponse decoding 081 */ 082 private enum InitialResponse 083 { 084 AUTHZID_EXPECTED, // We are expecting a authzid element 085 AUTHCID_EXPECTED, // We are expecting a authcid element 086 PASSWORD_EXPECTED // We are expecting a password element 087 } 088 089 /** The current negotiation state */ 090 private NegotiationState state; 091 092 /** 093 * 094 * Creates a new instance of PlainSaslServer. 095 * 096 * @param ldapSession The associated LdapSession instance 097 * @param adminSession The Administrator session 098 * @param bindRequest The associated BindRequest object 099 */ 100 public PlainSaslServer( LdapSession ldapSession, CoreSession adminSession, BindRequest bindRequest ) 101 { 102 super( ldapSession, adminSession, bindRequest ); 103 state = NegotiationState.INITIALIZED; 104 105 // Reinitialize the SASL properties 106 getLdapSession().removeSaslProperty( SASL_PLAIN_AUTHZID ); 107 getLdapSession().removeSaslProperty( SASL_PLAIN_AUTHCID ); 108 getLdapSession().removeSaslProperty( SASL_PLAIN_PASSWORD ); 109 } 110 111 112 /** 113 * {@inheritDoc} 114 */ 115 public String getMechanismName() 116 { 117 return SupportedSaslMechanisms.PLAIN; 118 } 119 120 121 /** 122 * {@inheritDoc} 123 */ 124 public byte[] evaluateResponse( byte[] initialResponse ) throws SaslException 125 { 126 if ( Strings.isEmpty( initialResponse ) ) 127 { 128 state = NegotiationState.MECH_RECEIVED; 129 return null; 130 } 131 else 132 { 133 // Split the credentials in three parts : 134 // - the optional authzId 135 // - the authId 136 // - the password 137 // The message should have this structure : 138 // message = [authzid] '0x00' authcid '0x00' passwd 139 // authzid = 1*SAFE ; MUST accept up to 255 octets 140 // authcid = 1*SAFE ; MUST accept up to 255 octets 141 // passwd = 1*SAFE ; MUST accept up to 255 octets 142 InitialResponse element = InitialResponse.AUTHZID_EXPECTED; 143 String authzId = null; 144 String authcId = null; 145 String password = null; 146 147 int start = 0; 148 int end = 0; 149 150 try 151 { 152 for ( byte b : initialResponse ) 153 { 154 if ( b == '\0' ) 155 { 156 if ( start - end == 0 ) 157 { 158 // We don't have any value 159 if ( element == InitialResponse.AUTHZID_EXPECTED ) 160 { 161 // This is optional : do nothing, but change 162 // the element type 163 element = InitialResponse.AUTHCID_EXPECTED; 164 } 165 else 166 { 167 // This not allowed 168 throw new IllegalArgumentException( I18n.err( I18n.ERR_671 ) ); 169 } 170 } 171 else 172 { 173 start++; 174 String value = new String( initialResponse, start, end - start + 1, "UTF-8" ); 175 176 switch ( element ) 177 { 178 case AUTHZID_EXPECTED: 179 element = InitialResponse.AUTHCID_EXPECTED; 180 authzId = PrepareString.normalize( value ); 181 end++; 182 start = end; 183 break; 184 185 case AUTHCID_EXPECTED: 186 element = InitialResponse.PASSWORD_EXPECTED; 187 authcId = PrepareString 188 .normalize( value ); 189 end++; 190 start = end; 191 break; 192 193 default: 194 // This is an error ! 195 throw new IllegalArgumentException( I18n.err( I18n.ERR_672 ) ); 196 } 197 } 198 } 199 else 200 { 201 end++; 202 } 203 } 204 205 if ( start == end ) 206 { 207 throw new IllegalArgumentException( I18n.err( I18n.ERR_671 ) ); 208 } 209 210 start++; 211 String value = Strings.utf8ToString( initialResponse, start, end - start + 1 ); 212 213 password = PrepareString.normalize( value ); 214 215 if ( ( authcId == null ) || ( password == null ) ) 216 { 217 throw new IllegalArgumentException( I18n.err( I18n.ERR_671 ) ); 218 } 219 220 // Now that we have the authcid and password, try to authenticate. 221 CoreSession userSession = authenticate( authcId, password ); 222 223 getLdapSession().setCoreSession( userSession ); 224 225 state = NegotiationState.COMPLETED; 226 } 227 catch ( IOException ioe ) 228 { 229 throw new IllegalArgumentException( I18n.err( I18n.ERR_674 ) ); 230 } 231 catch ( InvalidNameException ine ) 232 { 233 throw new IllegalArgumentException( I18n.err( I18n.ERR_675 ) ); 234 } 235 catch ( Exception e ) 236 { 237 throw new SaslException( I18n.err( I18n.ERR_676, authcId ) ); 238 } 239 } 240 241 return Strings.EMPTY_BYTES; 242 } 243 244 245 public boolean isComplete() 246 { 247 return state == NegotiationState.COMPLETED; 248 } 249 250 251 /** 252 * Try to authenticate the user against the underlying LDAP server. The SASL PLAIN 253 * authentication is based on the entry which uid is equal to the user name we received. 254 */ 255 private CoreSession authenticate( String user, String password ) throws Exception 256 { 257 LdapSession ldapSession = getLdapSession(); 258 CoreSession adminSession = getAdminSession(); 259 DirectoryService directoryService = adminSession.getDirectoryService(); 260 LdapServer ldapServer = ldapSession.getLdapServer(); 261 OperationManager operationManager = directoryService.getOperationManager(); 262 263 // first, we have to find the entries which has the uid value 264 EqualityNode<String> filter = new EqualityNode<>( 265 directoryService.getSchemaManager().getAttributeType( SchemaConstants.UID_AT ), new Value( user ) ); 266 267 SearchOperationContext searchContext = new SearchOperationContext( directoryService.getAdminSession() ); 268 searchContext.setDn( directoryService.getDnFactory().create( ldapServer.getSearchBaseDn() ) ); 269 searchContext.setScope( SearchScope.SUBTREE ); 270 searchContext.setFilter( filter ); 271 searchContext.setNoAttributes( true ); 272 273 EntryFilteringCursor cursor = operationManager.search( searchContext ); 274 Exception bindException = new LdapAuthenticationException( "Cannot authenticate user uid=" + user ); 275 276 while ( cursor.next() ) 277 { 278 Entry entry = cursor.get(); 279 280 try 281 { 282 BindOperationContext bindContext = new BindOperationContext( ldapSession.getCoreSession() ); 283 bindContext.setDn( entry.getDn() ); 284 bindContext.setCredentials( Strings.getBytesUtf8( password ) ); 285 bindContext.setIoSession( ldapSession.getIoSession() ); 286 bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) ); 287 288 operationManager.bind( bindContext ); 289 290 cursor.close(); 291 292 return bindContext.getSession(); 293 } 294 catch ( Exception e ) 295 { 296 bindException = e;// Nothing to do here : we will try to bind with the next user 297 } 298 } 299 300 cursor.close(); 301 302 throw bindException; 303 } 304}