Coverage Report - com.allanbank.mongodb.client.connection.auth.MongoDbAuthenticator
 
Classes in this File Line Coverage Branch Coverage Complexity
MongoDbAuthenticator
88%
31/35
100%
2/2
2.583
MongoDbAuthenticator$AuthenticateReplyCallback
100%
13/13
83%
5/6
2.583
MongoDbAuthenticator$NonceReplyCallback
94%
33/35
100%
4/4
2.583
 
 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  27
 public class MongoDbAuthenticator implements Authenticator {
 55  
 
 56  
     /** The UTF-8 character encoding. */
 57  1
     public static final Charset ASCII = Charset.forName("US-ASCII");
 58  
 
 59  
     /** The result of the Authentication attempt. */
 60  54
     protected FutureCallback<Boolean> myResults = new FutureCallback<Boolean>();
 61  
 
 62  
     /**
 63  
      * Creates a new MongoDbAuthenticator.
 64  
      */
 65  
     public MongoDbAuthenticator() {
 66  54
         super();
 67  54
     }
 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  27
             final MongoDbAuthenticator newAuth = (MongoDbAuthenticator) super
 79  
                     .clone();
 80  27
             newAuth.myResults = new FutureCallback<Boolean>();
 81  
 
 82  27
             return newAuth;
 83  
         }
 84  0
         catch (final CloneNotSupportedException cannotHappen) {
 85  0
             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  1
         final MessageDigest md5 = MessageDigest.getInstance("MD5");
 101  
 
 102  1
         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  25
         final boolean interrupted = Thread.interrupted();
 116  
         try {
 117  25
             return myResults.get().booleanValue();
 118  
         }
 119  0
         catch (final InterruptedException e) {
 120  0
             throw new MongoDbAuthenticationException(e);
 121  
         }
 122  10
         catch (final ExecutionException e) {
 123  10
             throw new MongoDbAuthenticationException(e);
 124  
         }
 125  
         finally {
 126  25
             if (interrupted) {
 127  1
                 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  27
             final DocumentBuilder builder = BuilderFactory.start();
 145  27
             builder.addInteger("getnonce", 1);
 146  
 
 147  27
             connection.send(new Command(credential.getDatabase(),
 148  
                     Command.COMMAND_COLLECTION, builder.build()),
 149  
                     new NonceReplyCallback(credential, connection));
 150  
         }
 151  1
         catch (final MongoDbException errorOnSend) {
 152  1
             myResults.exception(errorOnSend);
 153  
 
 154  1
             throw errorOnSend;
 155  26
         }
 156  26
     }
 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  22
         final char[] password = credentials.getPassword();
 171  22
         final ByteBuffer bb = ASCII.encode(CharBuffer.wrap(password));
 172  
 
 173  22
         md5.update((credentials.getUserName() + ":mongo:").getBytes(ASCII));
 174  22
         md5.update(bb.array(), 0, bb.limit());
 175  
 
 176  22
         Arrays.fill(password, ' ');
 177  22
         Arrays.fill(bb.array(), (byte) 0);
 178  
 
 179  22
         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  16
     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  21
             super(results);
 198  21
         }
 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  16
             boolean current = false;
 210  16
             final List<Document> results = reply.getResults();
 211  16
             if (results.size() == 1) {
 212  15
                 final Document doc = results.get(0);
 213  15
                 final Element okElem = doc.get("ok");
 214  15
                 if (okElem != null) {
 215  12
                     final int okValue = toInt(okElem);
 216  12
                     if (okValue == 1) {
 217  12
                         current = true;
 218  
                     }
 219  
 
 220  
                 }
 221  
             }
 222  
 
 223  16
             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  27
                 final Connection connection) {
 251  27
             myCredential = credential;
 252  27
             myConnection = connection;
 253  27
         }
 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  7
             myResults.exception(thrown);
 265  7
         }
 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  1
             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  26
             StringElement nonce = null;
 290  26
             if (reply.getResults().size() > 0) {
 291  23
                 final Document doc = reply.getResults().get(0);
 292  23
                 nonce = doc.get(StringElement.class, "nonce");
 293  23
                 if (nonce == null) {
 294  
                     // Bad reply. Try again.
 295  2
                     exception(new MongoDbAuthenticationException(
 296  
                             "Bad response from nonce request."));
 297  2
                     return;
 298  
                 }
 299  21
             }
 300  
             else {
 301  
                 // Bad reply. Try again.
 302  3
                 exception(new MongoDbAuthenticationException(
 303  
                         "Bad response from nonce request."));
 304  3
                 return;
 305  
             }
 306  
 
 307  
             // Send an authenticate request.
 308  
             try {
 309  21
                 final MessageDigest md5 = MessageDigest.getInstance("MD5");
 310  21
                 final String passwordHash = passwordHash(md5, myCredential);
 311  
 
 312  21
                 final String text = nonce.getValue()
 313  
                         + myCredential.getUserName() + passwordHash;
 314  
 
 315  21
                 md5.reset();
 316  21
                 md5.update(text.getBytes(ASCII));
 317  21
                 final byte[] bytes = md5.digest();
 318  
 
 319  21
                 final DocumentBuilder builder = BuilderFactory.start();
 320  21
                 builder.addInteger("authenticate", 1);
 321  21
                 builder.add(nonce);
 322  21
                 builder.addString("user", myCredential.getUserName());
 323  21
                 builder.addString("key", IOUtils.toHex(bytes));
 324  
 
 325  21
                 myConnection.send(new Command(myCredential.getDatabase(),
 326  
                         Command.COMMAND_COLLECTION, builder.build()),
 327  
                         new AuthenticateReplyCallback(myResults));
 328  
             }
 329  0
             catch (final NoSuchAlgorithmException e) {
 330  0
                 exception(new MongoDbAuthenticationException(e));
 331  
             }
 332  1
             catch (final RuntimeException e) {
 333  1
                 exception(new MongoDbAuthenticationException(e));
 334  20
             }
 335  21
         }
 336  
     }
 337  
 }