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     */
020    package org.apache.directory.server.ldap.handlers.bind.plain;
021    
022    
023    import java.io.IOException;
024    
025    import org.apache.directory.server.core.CoreSession;
026    import org.apache.directory.server.core.interceptor.context.BindOperationContext;
027    import org.apache.directory.server.ldap.LdapSession;
028    import org.apache.directory.server.ldap.handlers.bind.AbstractSaslServer;
029    import org.apache.directory.shared.ldap.constants.SupportedSaslMechanisms;
030    import org.apache.directory.shared.ldap.message.InternalBindRequest;
031    import org.apache.directory.shared.ldap.name.LdapDN;
032    import org.apache.directory.shared.ldap.schema.PrepareString;
033    import org.apache.directory.shared.ldap.util.StringTools;
034    
035    import javax.naming.InvalidNameException;
036    import javax.security.sasl.SaslException;
037    
038    
039    /**
040     * A SaslServer implementation for PLAIN based SASL mechanism.  This is
041     * required unfortunately because the JDK's SASL provider does not support
042     * this mechanism.
043     *
044     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
045     * @version $$Rev$$
046     */
047    public class PlainSaslServer extends AbstractSaslServer
048    {
049        /** The authzid property stored into the LdapSession instance */
050        public static final String SASL_PLAIN_AUTHZID = "authzid";
051        
052        /** The authcid property stored into the LdapSession instance */
053        public static final String SASL_PLAIN_AUTHCID = "authcid";
054    
055        /** The password property stored into the LdapSession instance */
056        public static final String SASL_PLAIN_PASSWORD = "password";
057        
058        
059        /**
060         * The possible states for the negotiation of a PLAIN mechanism. 
061         */
062        private enum NegotiationState 
063        {
064            INITIALIZED,    // Negotiation has just started 
065            MECH_RECEIVED,  // We have received the PLAIN mechanism
066            COMPLETED       // The user/password have been received
067        }
068        
069        
070        /**
071         * The different state used by the iInitialResponse decoding
072         */
073        private enum InitialResponse
074        {
075            AUTHZID_EXPECTED,    // We are expecting a authzid element
076            AUTHCID_EXPECTED,    // We are expecting a authcid element 
077            PASSWORD_EXPECTED    // We are expecting a password element
078        }
079    
080        /** The current negotiation state */
081        private NegotiationState state;
082        
083        
084        /**
085         * 
086         * Creates a new instance of PlainSaslServer.
087         *
088         * @param bindRequest The associated BindRequest object
089         * @param ldapSession The associated LdapSession instance 
090         */
091        public PlainSaslServer( LdapSession ldapSession, CoreSession adminSession, InternalBindRequest bindRequest )
092        {
093            super( ldapSession, adminSession, bindRequest );
094            state = NegotiationState.INITIALIZED;
095            
096            // Reinitialize the SASL properties
097            getLdapSession().removeSaslProperty( SASL_PLAIN_AUTHZID );
098            getLdapSession().removeSaslProperty( SASL_PLAIN_AUTHCID );
099            getLdapSession().removeSaslProperty( SASL_PLAIN_PASSWORD );
100        }
101    
102    
103        /**
104         * {@inheritDoc}
105         */
106        public String getMechanismName()
107        {
108            return SupportedSaslMechanisms.PLAIN;
109        }
110    
111    
112        /**
113         * {@inheritDoc}
114         */
115        public byte[] evaluateResponse( byte[] initialResponse ) throws SaslException
116        {
117            if ( StringTools.isEmpty( initialResponse ) )
118            {
119                state = NegotiationState.MECH_RECEIVED;
120                return null;
121            }
122            else
123            {
124                // Split the credentials in three parts :
125                // - the optional authzId
126                // - the authId
127                // - the password
128                InitialResponse element = InitialResponse.AUTHZID_EXPECTED;
129                String authzId = null;
130                String authcId = null;
131                String password = null;
132                
133                int start = 0;
134                int end = 0;
135                
136                try
137                {
138                    for ( byte b:initialResponse )
139                    {
140                        if ( b == '\0' )
141                        {
142                            if ( start - end == 0 )
143                            {
144                                // We don't have any value
145                                if ( element == InitialResponse.AUTHZID_EXPECTED )
146                                {
147                                    // This is optional : do nothing, but change
148                                    // the element type
149                                    element = InitialResponse.AUTHCID_EXPECTED;
150                                    continue;
151                                }
152                                else
153                                {
154                                    // This not allowed
155                                    throw new IllegalArgumentException( "response with no auhcid or no password" );
156                                }
157                            }
158                            else
159                            {
160                                start++;
161                                String value = new String( initialResponse, start, end - start + 1, "UTF-8" );
162                                
163                                switch ( element )
164                                {
165                                    case AUTHZID_EXPECTED :
166                                        element = InitialResponse.AUTHCID_EXPECTED;
167                                        authzId = PrepareString.normalize( value, PrepareString.StringType.CASE_EXACT_IA5 );
168                                        end++;
169                                        start = end;
170                                        break;
171                                        
172                                    case AUTHCID_EXPECTED :
173                                        element = InitialResponse.PASSWORD_EXPECTED;
174                                        authcId = PrepareString.normalize( value, PrepareString.StringType.DIRECTORY_STRING );
175                                        end++;
176                                        start = end;
177                                        break;
178                                        
179                                        
180                                    default :
181                                        // This is an error !
182                                        throw new IllegalArgumentException( "'\0' chars are not allowed in authcid or no password" );
183                                }
184                            }
185                        }
186                        else
187                        {
188                            end++;
189                        }
190                    }
191                
192                    if ( start == end )
193                    {
194                        throw new IllegalArgumentException( "response with no auhcid or no password" );
195                    }
196                    
197                    start++;
198                    String value = StringTools.utf8ToString( initialResponse, start, end - start + 1 );
199                    
200                    password = PrepareString.normalize( value, PrepareString.StringType.CASE_EXACT_IA5 );
201                    
202                    if ( ( authcId == null ) || ( password == null ) )
203                    {
204                        throw new IllegalArgumentException( "response with no auhcid or no password" );
205                    }
206                    
207                    // Now that we have the authcid and password, try to authenticate.
208                    CoreSession userSession = authenticate( authcId, password );
209                    
210                    getLdapSession().setCoreSession( userSession );
211                    
212                    state = NegotiationState.COMPLETED;
213                }
214                catch ( IOException ioe )
215                {
216                    throw new IllegalArgumentException( "The given InitialReponse is incorrect" );
217                }
218                catch ( InvalidNameException ine )
219                {
220                    throw new IllegalArgumentException( "Cannot authenticate an invalid authcid DN" );
221                }
222                catch ( Exception e )
223                {
224                    throw new SaslException( "Cannot authenticate the user " + authcId );
225                }
226            }
227    
228            return StringTools.EMPTY_BYTES;
229        }
230    
231    
232        public boolean isComplete()
233        {
234            return state == NegotiationState.COMPLETED;
235        }
236        
237        
238        /**
239         * Try to authenticate the usr against the underlying LDAP server.
240         */
241        private CoreSession authenticate( String user, String password ) throws InvalidNameException, Exception
242        {
243            BindOperationContext bindContext = new BindOperationContext( getLdapSession().getCoreSession() );
244            bindContext.setDn( new LdapDN( user ) );
245            bindContext.setCredentials( StringTools.getBytesUtf8( password ) );
246            
247            getAdminSession().getDirectoryService().getOperationManager().bind( bindContext );
248            
249            return bindContext.getSession();
250        }
251    }