001/*
002 * Copyright 2011-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2011-2017 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.listener;
022
023
024
025import java.util.Arrays;
026import java.util.List;
027import java.util.Map;
028
029import com.unboundid.asn1.ASN1OctetString;
030import com.unboundid.ldap.protocol.LDAPMessage;
031import com.unboundid.ldap.sdk.BindResult;
032import com.unboundid.ldap.sdk.Control;
033import com.unboundid.ldap.sdk.DN;
034import com.unboundid.ldap.sdk.Entry;
035import com.unboundid.ldap.sdk.LDAPException;
036import com.unboundid.ldap.sdk.ResultCode;
037import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
038import com.unboundid.ldap.sdk.controls.AuthorizationIdentityResponseControl;
039import com.unboundid.util.Debug;
040import com.unboundid.util.NotMutable;
041import com.unboundid.util.StaticUtils;
042import com.unboundid.util.ThreadSafety;
043import com.unboundid.util.ThreadSafetyLevel;
044
045import static com.unboundid.ldap.listener.ListenerMessages.*;
046
047
048
049/**
050 * This class defines a SASL bind handler which may be used to provide support
051 * for the SASL PLAIN mechanism (as defined in RFC 4616) in the in-memory
052 * directory server.
053 */
054@NotMutable()
055@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
056public final class PLAINBindHandler
057       extends InMemorySASLBindHandler
058{
059  /**
060   * Creates a new instance of this SASL bind handler.
061   */
062  public PLAINBindHandler()
063  {
064    // No initialization is required.
065  }
066
067
068
069  /**
070   * {@inheritDoc}
071   */
072  @Override()
073  public String getSASLMechanismName()
074  {
075    return "PLAIN";
076  }
077
078
079
080  /**
081   * {@inheritDoc}
082   */
083  @Override()
084  public BindResult processSASLBind(final InMemoryRequestHandler handler,
085                                    final int messageID, final DN bindDN,
086                                    final ASN1OctetString credentials,
087                                    final List<Control> controls)
088  {
089    // Process the provided request controls.
090    final Map<String,Control> controlMap;
091    try
092    {
093      controlMap = RequestControlPreProcessor.processControls(
094           LDAPMessage.PROTOCOL_OP_TYPE_BIND_REQUEST, controls);
095    }
096    catch (final LDAPException le)
097    {
098      Debug.debugException(le);
099      return  new BindResult(messageID, le.getResultCode(),
100           le.getMessage(), le.getMatchedDN(), le.getReferralURLs(),
101           le.getResponseControls());
102    }
103
104
105    // Parse the credentials, which should be in the form:
106    //      [authzid] UTF8NUL authcid UTF8NUL passwd
107    if (credentials == null)
108    {
109      return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS,
110           ERR_PLAIN_BIND_NO_CREDENTIALS.get(), null, null, null);
111    }
112
113    int firstNullPos  = -1;
114    int secondNullPos = -1;
115    final byte[] credBytes = credentials.getValue();
116    for (int i=0; i < credBytes.length; i++)
117    {
118      if (credBytes[i] == 0x00)
119      {
120        if (firstNullPos < 0)
121        {
122          firstNullPos = i;
123        }
124        else
125        {
126          secondNullPos = i;
127          break;
128        }
129      }
130    }
131
132    if (secondNullPos < 0)
133    {
134      return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS,
135           ERR_PLAIN_BIND_MALFORMED_CREDENTIALS.get(), null, null, null);
136    }
137
138
139    // There must have been at least an authentication identity.  Verify that it
140    // is valid.
141    final String authzID;
142    final String authcID = StaticUtils.toUTF8String(credBytes, (firstNullPos+1),
143         (secondNullPos-firstNullPos-1));
144    if (firstNullPos == 0)
145    {
146      authzID = null;
147    }
148    else
149    {
150      authzID = StaticUtils.toUTF8String(credBytes, 0, firstNullPos);
151    }
152
153    DN authDN;
154    try
155    {
156      authDN = handler.getDNForAuthzID(authcID);
157    }
158    catch (final LDAPException le)
159    {
160      Debug.debugException(le);
161      return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS,
162           le.getMessage(), le.getMatchedDN(), le.getReferralURLs(),
163           le.getResponseControls());
164    }
165
166
167    // Verify that the password is correct.
168    final byte[] bindPWBytes = new byte[credBytes.length - secondNullPos - 1];
169    System.arraycopy(credBytes, secondNullPos+1, bindPWBytes, 0,
170         bindPWBytes.length);
171
172    final boolean passwordValid;
173    if (authDN.isNullDN())
174    {
175      // For an anonymous bind, the password must be empty, and no authorization
176      // ID may have been provided.
177      passwordValid = ((bindPWBytes.length == 0) && (authzID == null));
178    }
179    else
180    {
181      // Determine the password for the target user, which may be an actual
182      // entry or be included in the additional bind credentials.
183      final byte[] userPWBytes;
184      final Entry authEntry = handler.getEntry(authDN);
185      if (authEntry == null)
186      {
187        userPWBytes = handler.getAdditionalBindCredentials(authDN);
188      }
189      else
190      {
191        userPWBytes = authEntry.getAttributeValueBytes("userPassword");
192      }
193
194      passwordValid =  Arrays.equals(bindPWBytes, userPWBytes);
195    }
196
197    if (! passwordValid)
198    {
199      return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS,
200           null, null, null, null);
201    }
202
203
204    // The server doesn't really distinguish between authID and authzID, so
205    // if an authzID was provided then we'll just behave as if the user
206    // specified as the authzID had bound.
207    String authID = authcID;
208    if (authzID != null)
209    {
210      try
211      {
212        authID = authzID;
213        authDN = handler.getDNForAuthzID(authzID);
214      }
215      catch (final LDAPException le)
216      {
217        Debug.debugException(le);
218        return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS,
219             le.getMessage(), le.getMatchedDN(), le.getReferralURLs(),
220             le.getResponseControls());
221      }
222    }
223
224    handler.setAuthenticatedDN(authDN);
225    final Control[] responseControls;
226    if (controlMap.containsKey(AuthorizationIdentityRequestControl.
227             AUTHORIZATION_IDENTITY_REQUEST_OID))
228    {
229      if (authDN == null)
230      {
231        responseControls = new Control[]
232        {
233          new AuthorizationIdentityResponseControl("")
234        };
235      }
236      else
237      {
238        responseControls = new Control[]
239        {
240          new AuthorizationIdentityResponseControl("dn:" + authDN.toString())
241        };
242      }
243    }
244    else
245    {
246      responseControls = null;
247    }
248
249    return new BindResult(messageID, ResultCode.SUCCESS, null, null, null,
250         responseControls);
251  }
252}