View Javadoc
1   /*
2    * #%L
3    * MongoDbAuthenticator.java - mongodb-async-driver - Allanbank Consulting, Inc.
4    * %%
5    * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  
21  package com.allanbank.mongodb.client.connection.auth;
22  
23  import java.nio.ByteBuffer;
24  import java.nio.CharBuffer;
25  import java.nio.charset.Charset;
26  import java.security.MessageDigest;
27  import java.security.NoSuchAlgorithmException;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.concurrent.ExecutionException;
31  
32  import com.allanbank.mongodb.Credential;
33  import com.allanbank.mongodb.MongoDbException;
34  import com.allanbank.mongodb.bson.Document;
35  import com.allanbank.mongodb.bson.Element;
36  import com.allanbank.mongodb.bson.builder.BuilderFactory;
37  import com.allanbank.mongodb.bson.builder.DocumentBuilder;
38  import com.allanbank.mongodb.bson.element.StringElement;
39  import com.allanbank.mongodb.client.FutureCallback;
40  import com.allanbank.mongodb.client.callback.AbstractReplyCallback;
41  import com.allanbank.mongodb.client.callback.AbstractValidatingReplyCallback;
42  import com.allanbank.mongodb.client.connection.Connection;
43  import com.allanbank.mongodb.client.message.Command;
44  import com.allanbank.mongodb.client.message.Reply;
45  import com.allanbank.mongodb.error.MongoDbAuthenticationException;
46  import com.allanbank.mongodb.util.IOUtils;
47  
48  /**
49   * MongoDbAuthenticator provides an authenticator for the legacy, pre-2.4
50   * version, of MongoDB authentication.
51   * 
52   * @copyright 2013-2014, Allanbank Consulting, Inc., All Rights Reserved
53   */
54  public class MongoDbAuthenticator implements Authenticator {
55  
56      /** The UTF-8 character encoding. */
57      public static final Charset ASCII = Charset.forName("US-ASCII");
58  
59      /** The result of the Authentication attempt. */
60      protected FutureCallback<Boolean> myResults = new FutureCallback<Boolean>();
61  
62      /**
63       * Creates a new MongoDbAuthenticator.
64       */
65      public MongoDbAuthenticator() {
66          super();
67      }
68  
69      /**
70       * {@inheritDoc}
71       * <p>
72       * Overridden to return a new authenticator.
73       * </p>
74       */
75      @Override
76      public MongoDbAuthenticator clone() {
77          try {
78              final MongoDbAuthenticator newAuth = (MongoDbAuthenticator) super
79                      .clone();
80              newAuth.myResults = new FutureCallback<Boolean>();
81  
82              return newAuth;
83          }
84          catch (final CloneNotSupportedException cannotHappen) {
85              return new MongoDbAuthenticator();
86          }
87      }
88  
89      /**
90       * Creates the MongoDB authentication hash of the password.
91       * 
92       * @param credentials
93       *            The credentials to hash.
94       * @return The hashed password/myCredential.
95       * @throws NoSuchAlgorithmException
96       *             On a failure to create a MD5 message digest.
97       */
98      public String passwordHash(final Credential credentials)
99              throws NoSuchAlgorithmException {
100         final MessageDigest md5 = MessageDigest.getInstance("MD5");
101 
102         return passwordHash(md5, credentials);
103     }
104 
105     /**
106      * {@inheritDoc}
107      * <p>
108      * Overridden to returns the results of the authentication, once complete.
109      * </p>
110      */
111     @Override
112     public boolean result() throws MongoDbAuthenticationException {
113 
114         // Clear and restore the threads interrupted state for reconnect cases.
115         final boolean interrupted = Thread.interrupted();
116         try {
117             return myResults.get().booleanValue();
118         }
119         catch (final InterruptedException e) {
120             throw new MongoDbAuthenticationException(e);
121         }
122         catch (final ExecutionException e) {
123             throw new MongoDbAuthenticationException(e);
124         }
125         finally {
126             if (interrupted) {
127                 Thread.currentThread().interrupt();
128             }
129         }
130     }
131 
132     /**
133      * {@inheritDoc}
134      * <p>
135      * Overridden to authenticate with MongoDB using the native/legacy
136      * authentication mechanisms.
137      * </p>
138      */
139     @Override
140     public void startAuthentication(final Credential credential,
141             final Connection connection) throws MongoDbAuthenticationException {
142 
143         try {
144             final DocumentBuilder builder = BuilderFactory.start();
145             builder.addInteger("getnonce", 1);
146 
147             connection.send(new Command(credential.getDatabase(),
148                     Command.COMMAND_COLLECTION, builder.build()),
149                     new NonceReplyCallback(credential, connection));
150         }
151         catch (final MongoDbException errorOnSend) {
152             myResults.exception(errorOnSend);
153 
154             throw errorOnSend;
155         }
156     }
157 
158     /**
159      * Creates the MongoDB authentication hash of the password.
160      * 
161      * @param md5
162      *            The MD5 digest to compute the hash.
163      * @param credentials
164      *            The credentials to hash.
165      * @return The hashed password/myCredential.
166      */
167     protected String passwordHash(final MessageDigest md5,
168             final Credential credentials) {
169 
170         final char[] password = credentials.getPassword();
171         final ByteBuffer bb = ASCII.encode(CharBuffer.wrap(password));
172 
173         md5.update((credentials.getUserName() + ":mongo:").getBytes(ASCII));
174         md5.update(bb.array(), 0, bb.limit());
175 
176         Arrays.fill(password, ' ');
177         Arrays.fill(bb.array(), (byte) 0);
178 
179         return IOUtils.toHex(md5.digest());
180     }
181 
182     /**
183      * AuthenticateReplyCallback provides the callback for the second step of
184      * the authentication.
185      * 
186      * @copyright 2013, Allanbank Consulting, Inc., All Rights Reserved
187      */
188     private static class AuthenticateReplyCallback extends
189             AbstractReplyCallback<Boolean> {
190         /**
191          * Creates a new AuthenticateReplyCallback.
192          * 
193          * @param results
194          *            The results to update once the reply is received.
195          */
196         public AuthenticateReplyCallback(final FutureCallback<Boolean> results) {
197             super(results);
198         }
199 
200         /**
201          * {@inheritDoc}
202          * <p>
203          * Overridden to convert the reply to a boolean based on if the
204          * authentication worked.
205          * </p>
206          */
207         @Override
208         protected Boolean convert(final Reply reply) throws MongoDbException {
209             boolean current = false;
210             final List<Document> results = reply.getResults();
211             if (results.size() == 1) {
212                 final Document doc = results.get(0);
213                 final Element okElem = doc.get("ok");
214                 if (okElem != null) {
215                     final int okValue = toInt(okElem);
216                     if (okValue == 1) {
217                         current = true;
218                     }
219 
220                 }
221             }
222 
223             return Boolean.valueOf(current);
224         }
225     }
226 
227     /**
228      * NonceReplyCallback provides the callback for the reply to the nonce
229      * request.
230      * 
231      * @copyright 2013-2014, Allanbank Consulting, Inc., All Rights Reserved
232      */
233     private class NonceReplyCallback extends AbstractValidatingReplyCallback {
234 
235         /** The connection to authenticate on. */
236         private final Connection myConnection;
237 
238         /** The credentials to use in authentication. */
239         private final Credential myCredential;
240 
241         /**
242          * Creates a new NonceReplyCallback.
243          * 
244          * @param credential
245          *            The credentials to use in authentication.
246          * @param connection
247          *            The connection to authenticate on.
248          */
249         public NonceReplyCallback(final Credential credential,
250                 final Connection connection) {
251             myCredential = credential;
252             myConnection = connection;
253         }
254 
255         /**
256          * {@inheritDoc}
257          * <p>
258          * Overridden to set the exception on the results for the
259          * authentication.
260          * </p>
261          */
262         @Override
263         public void exception(final Throwable thrown) {
264             myResults.exception(thrown);
265         }
266 
267         /**
268          * {@inheritDoc}
269          * <p>
270          * Overridden to return true to make sure the authentication is
271          * processed on a thread we control.
272          * </p>
273          */
274         @Override
275         public boolean isLightWeight() {
276             return true;
277         }
278 
279         /**
280          * {@inheritDoc}
281          * <p>
282          * Overridden to retrieve the nonce from the reply and request
283          * authentication.
284          * </p>
285          */
286         @Override
287         protected void handle(final Reply reply) {
288 
289             StringElement nonce = null;
290             if (reply.getResults().size() > 0) {
291                 final Document doc = reply.getResults().get(0);
292                 nonce = doc.get(StringElement.class, "nonce");
293                 if (nonce == null) {
294                     // Bad reply. Try again.
295                     exception(new MongoDbAuthenticationException(
296                             "Bad response from nonce request."));
297                     return;
298                 }
299             }
300             else {
301                 // Bad reply. Try again.
302                 exception(new MongoDbAuthenticationException(
303                         "Bad response from nonce request."));
304                 return;
305             }
306 
307             // Send an authenticate request.
308             try {
309                 final MessageDigest md5 = MessageDigest.getInstance("MD5");
310                 final String passwordHash = passwordHash(md5, myCredential);
311 
312                 final String text = nonce.getValue()
313                         + myCredential.getUserName() + passwordHash;
314 
315                 md5.reset();
316                 md5.update(text.getBytes(ASCII));
317                 final byte[] bytes = md5.digest();
318 
319                 final DocumentBuilder builder = BuilderFactory.start();
320                 builder.addInteger("authenticate", 1);
321                 builder.add(nonce);
322                 builder.addString("user", myCredential.getUserName());
323                 builder.addString("key", IOUtils.toHex(bytes));
324 
325                 myConnection.send(new Command(myCredential.getDatabase(),
326                         Command.COMMAND_COLLECTION, builder.build()),
327                         new AuthenticateReplyCallback(myResults));
328             }
329             catch (final NoSuchAlgorithmException e) {
330                 exception(new MongoDbAuthenticationException(e));
331             }
332             catch (final RuntimeException e) {
333                 exception(new MongoDbAuthenticationException(e));
334             }
335         }
336     }
337 }