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