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}