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