You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2015/07/20 17:52:54 UTC

incubator-tinkerpop git commit: Add HTTP Basic authentication to REST based requests.

Repository: incubator-tinkerpop
Updated Branches:
  refs/heads/auth b583fb9f1 -> a686fda18


Add HTTP Basic authentication to REST based requests.


Project: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/commit/a686fda1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/tree/a686fda1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/diff/a686fda1

Branch: refs/heads/auth
Commit: a686fda18226497c9be7e1fcbea7a80464bbc537
Parents: b583fb9
Author: Stephen Mallette <sp...@genoprime.com>
Authored: Mon Jul 20 11:52:16 2015 -0400
Committer: Stephen Mallette <sp...@genoprime.com>
Committed: Mon Jul 20 11:52:16 2015 -0400

----------------------------------------------------------------------
 .../gremlin/server/AbstractChannelizer.java     |  12 +-
 .../server/auth/AuthenticationHandler.java      | 102 ---------------
 .../gremlin/server/channel/HttpChannelizer.java |  15 ++-
 .../gremlin/server/channel/NioChannelizer.java  |  11 ++
 .../server/channel/WebSocketChannelizer.java    |  11 ++
 .../handler/HttpBasicAuthenticationHandler.java |  83 ++++++++++++
 .../handler/SaslAuthenticationHandler.java      | 105 +++++++++++++++
 .../server/GremlinServerHttpIntegrateTest.java  | 131 +++++++++++++++++++
 8 files changed, 358 insertions(+), 112 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
index e5589e4..41d909e 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
@@ -26,7 +26,7 @@ import io.netty.handler.ssl.util.SelfSignedCertificate;
 import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
 import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor;
 import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
-import org.apache.tinkerpop.gremlin.server.auth.AuthenticationHandler;
+import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler;
 import org.apache.tinkerpop.gremlin.server.auth.Authenticator;
 import org.apache.tinkerpop.gremlin.server.handler.IteratorHandler;
 import org.apache.tinkerpop.gremlin.server.handler.OpExecutorHandler;
@@ -81,7 +81,7 @@ public abstract class AbstractChannelizer extends ChannelInitializer<SocketChann
     private OpExecutorHandler opExecutorHandler;
     private IteratorHandler iteratorHandler;
 
-    private AuthenticationHandler authenticationHandler;
+    protected Authenticator authenticator;
 
     /**
      * This method is called from within {@link #initChannel(io.netty.channel.socket.SocketChannel)} just after
@@ -114,10 +114,7 @@ public abstract class AbstractChannelizer extends ChannelInitializer<SocketChann
                 Optional.ofNullable(createSSLContext(settings)) : Optional.empty();
         if (sslContext.isPresent()) logger.info("SSL enabled");
 
-        // configure authentication - null means don't bother to add authentication to the pipeline
-        final Authenticator authenticator = createAuthenticator(settings.authentication);
-        authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
-                null : new AuthenticationHandler(authenticator);
+        authenticator = createAuthenticator(settings.authentication);
 
         // these handlers don't share any state and can thus be initialized once per pipeline
         opSelectorHandler = new OpSelectorHandler(settings, graphManager, gremlinExecutor, scheduledExecutorService);
@@ -136,9 +133,6 @@ public abstract class AbstractChannelizer extends ChannelInitializer<SocketChann
         // instance
         configure(pipeline);
 
-        if (authenticationHandler != null)
-            pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
-
         pipeline.addLast(PIPELINE_OP_SELECTOR, opSelectorHandler);
         pipeline.addLast(PIPELINE_RESULT_ITERATOR_HANDLER, iteratorHandler);
         pipeline.addLast(PIPELINE_OP_EXECUTOR, opExecutorHandler);

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java
deleted file mode 100644
index d5bef4b..0000000
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.tinkerpop.gremlin.server.auth;
-
-import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.util.Attribute;
-import io.netty.util.AttributeKey;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * @author Stephen Mallette (http://stephen.genoprime.com)
- */
-@ChannelHandler.Sharable
-public class AuthenticationHandler extends ChannelInboundHandlerAdapter {
-    private static final Logger logger = LoggerFactory.getLogger(AuthenticationHandler.class);
-
-    private static final AttributeKey<Authenticator.SaslNegotiator> negotiatorKey = AttributeKey.valueOf("negotiator");
-    private static final AttributeKey<RequestMessage> requestKey = AttributeKey.valueOf("request");
-
-    private final Authenticator authenticator;
-
-    public AuthenticationHandler(final Authenticator authenticator) {
-        this.authenticator = authenticator;
-    }
-
-    @Override
-    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
-        if (msg instanceof RequestMessage){
-            final RequestMessage requestMessage = (RequestMessage) msg;
-
-            final Attribute<Authenticator.SaslNegotiator> negotiator = ctx.attr(negotiatorKey);
-            final Attribute<RequestMessage> request = ctx.attr(requestKey);
-            if (negotiator.get() == null) {
-                // First time through so save the request and send an AUTHENTICATE challenge with no data
-                negotiator.set(authenticator.newSaslNegotiator());
-                request.set(requestMessage);
-                final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
-                        .code(ResponseStatusCode.AUTHENTICATE).create();
-                ctx.writeAndFlush(authenticate);
-            } else {
-                if (requestMessage.getOp().equals("authentication") && requestMessage.getArgs().containsKey("sasl")) {
-                    final byte[] saslResponse = (byte[]) requestMessage.getArgs().get("sasl");
-                    try {
-                        byte[] saslMessage = negotiator.get().evaluateResponse(saslResponse);
-                        if (negotiator.get().isComplete()) {
-                            // todo: do something with this user
-                            final AuthenticatedUser user = negotiator.get().getAuthenticatedUser();
-
-                            // If we have got here we are authenticated so remove the handler and pass
-                            // the original message down the pipeline for processing
-                            ctx.pipeline().remove(this);
-                            final RequestMessage original = request.get();
-                            ctx.fireChannelRead(original);
-                        } else {
-                            // not done here - send back the sasl message for next challenge
-                            final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
-                                    .code(ResponseStatusCode.AUTHENTICATE).result(saslMessage).create();
-                            ctx.writeAndFlush(authenticate);
-                        }
-                    } catch (AuthenticationException ae) {
-                        final ResponseMessage error = ResponseMessage.build(request.get())
-                                .statusMessage(ae.getMessage())
-                                .code(ResponseStatusCode.UNAUTHORIZED).create();
-                        ctx.writeAndFlush(error);
-                    }
-                } else {
-                    final ResponseMessage error = ResponseMessage.build(requestMessage)
-                            .statusMessage("Failed to authenticate")
-                            .code(ResponseStatusCode.UNAUTHORIZED).create();
-                    ctx.writeAndFlush(error);
-                }
-            }
-        }
-        else {
-            logger.warn("{} only processes RequestMessage instances - received {} - channel closing",
-                    this.getClass().getSimpleName(), msg.getClass());
-            ctx.close();
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
index 28df014..d0a88e5 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
@@ -20,6 +20,9 @@ package org.apache.tinkerpop.gremlin.server.channel;
 
 import io.netty.channel.EventLoopGroup;
 import org.apache.tinkerpop.gremlin.server.AbstractChannelizer;
+import org.apache.tinkerpop.gremlin.server.Channelizer;
+import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
+import org.apache.tinkerpop.gremlin.server.handler.HttpBasicAuthenticationHandler;
 import org.apache.tinkerpop.gremlin.server.handler.HttpGremlinEndpointHandler;
 import io.netty.channel.ChannelPipeline;
 import io.netty.handler.codec.http.HttpObjectAggregator;
@@ -31,7 +34,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Constructs a {@link org.apache.tinkerpop.gremlin.server.Channelizer} that exposes an HTTP/REST endpoint in Gremlin Server.
+ * Constructs a {@link Channelizer} that exposes an HTTP/REST endpoint in Gremlin Server.
  *
  * @author Stephen Mallette (http://stephen.genoprime.com)
  */
@@ -39,11 +42,17 @@ public class HttpChannelizer extends AbstractChannelizer {
     private static final Logger logger = LoggerFactory.getLogger(HttpChannelizer.class);
 
     private HttpGremlinEndpointHandler httpGremlinEndpointHandler;
+    private HttpBasicAuthenticationHandler authenticationHandler;
 
     @Override
     public void init(final ServerGremlinExecutor<EventLoopGroup> serverGremlinExecutor) {
         super.init(serverGremlinExecutor);
         httpGremlinEndpointHandler = new HttpGremlinEndpointHandler(serializers, gremlinExecutor, graphManager);
+
+        // configure authentication - null means don't bother to add authentication to the pipeline
+        if (authenticator != null)
+            authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
+                    null : new HttpBasicAuthenticationHandler(authenticator);
     }
 
     @Override
@@ -57,6 +66,10 @@ public class HttpChannelizer extends AbstractChannelizer {
             pipeline.addLast(new LoggingHandler("http-io", LogLevel.DEBUG));
 
         pipeline.addLast(new HttpObjectAggregator(settings.maxContentLength));
+
+        if (authenticationHandler != null)
+            pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
+
         pipeline.addLast("http-gremlin-handler", httpGremlinEndpointHandler);
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
index 0b95e81..fba75f9 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
@@ -20,11 +20,13 @@ package org.apache.tinkerpop.gremlin.server.channel;
 
 import io.netty.channel.EventLoopGroup;
 import org.apache.tinkerpop.gremlin.server.AbstractChannelizer;
+import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
 import org.apache.tinkerpop.gremlin.server.handler.NioGremlinBinaryRequestDecoder;
 import org.apache.tinkerpop.gremlin.server.handler.NioGremlinResponseEncoder;
 import io.netty.channel.ChannelPipeline;
 import io.netty.handler.logging.LogLevel;
 import io.netty.handler.logging.LoggingHandler;
+import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler;
 import org.apache.tinkerpop.gremlin.server.util.ServerGremlinExecutor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -39,11 +41,17 @@ public class NioChannelizer extends AbstractChannelizer {
     private static final Logger logger = LoggerFactory.getLogger(NioChannelizer.class);
 
     private NioGremlinBinaryRequestDecoder nioGremlinBinaryRequestDecoder;
+    private SaslAuthenticationHandler authenticationHandler;
 
     @Override
     public void init(final ServerGremlinExecutor<EventLoopGroup> serverGremlinExecutor) {
         super.init(serverGremlinExecutor);
         nioGremlinBinaryRequestDecoder = new NioGremlinBinaryRequestDecoder(serializers);
+
+        // configure authentication - null means don't bother to add authentication to the pipeline
+        if (authenticator != null)
+            authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
+                    null : new SaslAuthenticationHandler(authenticator);
     }
 
     @Override
@@ -56,5 +64,8 @@ public class NioChannelizer extends AbstractChannelizer {
 
         if (logger.isDebugEnabled())
             pipeline.addLast(new LoggingHandler("log-codec", LogLevel.DEBUG));
+
+        if (authenticationHandler != null)
+            pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
     }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
index 23d7873..603a239 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
@@ -20,6 +20,8 @@ package org.apache.tinkerpop.gremlin.server.channel;
 
 import io.netty.channel.EventLoopGroup;
 import org.apache.tinkerpop.gremlin.server.AbstractChannelizer;
+import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
+import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler;
 import org.apache.tinkerpop.gremlin.server.handler.WsGremlinBinaryRequestDecoder;
 import org.apache.tinkerpop.gremlin.server.handler.WsGremlinResponseEncoder;
 import org.apache.tinkerpop.gremlin.server.handler.WsGremlinTextRequestDecoder;
@@ -46,6 +48,7 @@ public class WebSocketChannelizer extends AbstractChannelizer {
     private WsGremlinResponseEncoder wsGremlinResponseEncoder;
     private WsGremlinTextRequestDecoder wsGremlinTextRequestDecoder;
     private WsGremlinBinaryRequestDecoder wsGremlinBinaryRequestDecoder;
+    private SaslAuthenticationHandler authenticationHandler;
 
     @Override
     public void init(final ServerGremlinExecutor<EventLoopGroup> serverGremlinExecutor) {
@@ -54,6 +57,11 @@ public class WebSocketChannelizer extends AbstractChannelizer {
         wsGremlinResponseEncoder = new WsGremlinResponseEncoder();
         wsGremlinTextRequestDecoder = new WsGremlinTextRequestDecoder(serializers);
         wsGremlinBinaryRequestDecoder = new WsGremlinBinaryRequestDecoder(serializers);
+
+        // configure authentication - null means don't bother to add authentication to the pipeline
+        if (authenticator != null)
+            authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
+                    null : new SaslAuthenticationHandler(authenticator);
     }
 
     @Override
@@ -89,5 +97,8 @@ public class WebSocketChannelizer extends AbstractChannelizer {
 
         if (logger.isDebugEnabled())
             pipeline.addLast(new LoggingHandler("log-aggregator-encoder", LogLevel.DEBUG));
+
+        if (authenticationHandler != null)
+            pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
     }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java
new file mode 100644
index 0000000..ccfd5fd
--- /dev/null
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.server.handler;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpMessage;
+import io.netty.util.ReferenceCountUtil;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException;
+import org.apache.tinkerpop.gremlin.server.auth.Authenticator;
+
+import java.nio.charset.Charset;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static org.apache.tinkerpop.gremlin.groovy.plugin.dsl.credential.CredentialGraphTokens.PROPERTY_PASSWORD;
+import static org.apache.tinkerpop.gremlin.groovy.plugin.dsl.credential.CredentialGraphTokens.PROPERTY_USERNAME;
+
+/**
+ * @author Stephen Mallette (http://stephen.genoprime.com)
+ */
+public class HttpBasicAuthenticationHandler extends ChannelInboundHandlerAdapter {
+    private final Authenticator authenticator;
+
+    private final Base64.Decoder decoder = Base64.getUrlDecoder();
+
+    public HttpBasicAuthenticationHandler(final Authenticator authenticator) {
+        this.authenticator = authenticator;
+    }
+
+    @Override
+    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+        if (msg instanceof FullHttpMessage) {
+            final FullHttpMessage request = (FullHttpMessage) msg;
+            if (!request.headers().contains("Authorization")) {
+                ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED));
+                ReferenceCountUtil.release(msg);
+                return;
+            }
+
+            final String authorizationHeader = request.headers().get("Authorization");
+            final String authorization = new String(decoder.decode(authorizationHeader), Charset.forName("UTF-8"));
+            final String[] split = authorization.split(":");
+            if (split.length != 2) {
+                ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED));
+                ReferenceCountUtil.release(msg);
+                return;
+            }
+
+            final Map<String,String> credentials = new HashMap<>();
+            credentials.put(PROPERTY_USERNAME, split[0]);
+            credentials.put(PROPERTY_PASSWORD, split[1]);
+
+            try {
+                authenticator.authenticate(credentials);
+                ctx.fireChannelRead(request);
+            } catch (AuthenticationException ae) {
+                ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED));
+                ReferenceCountUtil.release(msg);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java
new file mode 100644
index 0000000..9db34a1
--- /dev/null
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.server.handler;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.util.Attribute;
+import io.netty.util.AttributeKey;
+import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
+import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException;
+import org.apache.tinkerpop.gremlin.server.auth.Authenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Stephen Mallette (http://stephen.genoprime.com)
+ */
+@ChannelHandler.Sharable
+public class SaslAuthenticationHandler extends ChannelInboundHandlerAdapter {
+    private static final Logger logger = LoggerFactory.getLogger(SaslAuthenticationHandler.class);
+
+    private static final AttributeKey<Authenticator.SaslNegotiator> negotiatorKey = AttributeKey.valueOf("negotiator");
+    private static final AttributeKey<RequestMessage> requestKey = AttributeKey.valueOf("request");
+
+    private final Authenticator authenticator;
+
+    public SaslAuthenticationHandler(final Authenticator authenticator) {
+        this.authenticator = authenticator;
+    }
+
+    @Override
+    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+        if (msg instanceof RequestMessage){
+            final RequestMessage requestMessage = (RequestMessage) msg;
+
+            final Attribute<Authenticator.SaslNegotiator> negotiator = ctx.attr(negotiatorKey);
+            final Attribute<RequestMessage> request = ctx.attr(requestKey);
+            if (negotiator.get() == null) {
+                // First time through so save the request and send an AUTHENTICATE challenge with no data
+                negotiator.set(authenticator.newSaslNegotiator());
+                request.set(requestMessage);
+                final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
+                        .code(ResponseStatusCode.AUTHENTICATE).create();
+                ctx.writeAndFlush(authenticate);
+            } else {
+                if (requestMessage.getOp().equals("authentication") && requestMessage.getArgs().containsKey("sasl")) {
+                    final byte[] saslResponse = (byte[]) requestMessage.getArgs().get("sasl");
+                    try {
+                        byte[] saslMessage = negotiator.get().evaluateResponse(saslResponse);
+                        if (negotiator.get().isComplete()) {
+                            // todo: do something with this user
+                            final AuthenticatedUser user = negotiator.get().getAuthenticatedUser();
+
+                            // If we have got here we are authenticated so remove the handler and pass
+                            // the original message down the pipeline for processing
+                            ctx.pipeline().remove(this);
+                            final RequestMessage original = request.get();
+                            ctx.fireChannelRead(original);
+                        } else {
+                            // not done here - send back the sasl message for next challenge
+                            final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
+                                    .code(ResponseStatusCode.AUTHENTICATE).result(saslMessage).create();
+                            ctx.writeAndFlush(authenticate);
+                        }
+                    } catch (AuthenticationException ae) {
+                        final ResponseMessage error = ResponseMessage.build(request.get())
+                                .statusMessage(ae.getMessage())
+                                .code(ResponseStatusCode.UNAUTHORIZED).create();
+                        ctx.writeAndFlush(error);
+                    }
+                } else {
+                    final ResponseMessage error = ResponseMessage.build(requestMessage)
+                            .statusMessage("Failed to authenticate")
+                            .code(ResponseStatusCode.UNAUTHORIZED).create();
+                    ctx.writeAndFlush(error);
+                }
+            }
+        }
+        else {
+            logger.warn("{} only processes RequestMessage instances - received {} - channel closing",
+                    this.getClass().getSimpleName(), msg.getClass());
+            ctx.close();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
index 73e56f4..11ec3a6 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
@@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.server;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.tinkerpop.gremlin.server.auth.SimpleAuthenticator;
 import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
 import org.apache.http.Consts;
 import org.apache.http.client.methods.CloseableHttpResponse;
@@ -34,6 +35,9 @@ import org.junit.Test;
 
 import java.io.File;
 import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
 
@@ -45,6 +49,8 @@ import static org.junit.Assert.assertEquals;
 public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegrationTest {
     private final ObjectMapper mapper = new ObjectMapper();
 
+    private final Base64.Encoder encoder = Base64.getUrlEncoder();
+
     /**
      * Configure specific Gremlin Server settings for specific tests.
      */
@@ -63,10 +69,135 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
                 deleteDirectory(new File("/tmp/neo4j"));
                 settings.graphs.put("graph", "conf/neo4j-empty.properties");
                 break;
+            case "should401OnGETWithNoAuthorizationHeader":
+            case "should401OnPOSTWithNoAuthorizationHeader":
+            case "should401OnGETWithBadAuthorizationHeader":
+            case "should401OnPOSTWithBadAuthorizationHeader":
+            case "should401OnGETWithInvalidPasswordAuthorizationHeader":
+            case "should401OnPOSTWithInvalidPasswordAuthorizationHeader":
+            case "should200OnGETWithAuthorizationHeader":
+            case "should200OnPOSTWithAuthorizationHeader":
+                configureForAuthentication(settings);
+                break;
         }
         return settings;
     }
 
+    private void configureForAuthentication(final Settings settings) {
+        final Settings.AuthenticationSettings authSettings = new Settings.AuthenticationSettings();
+        authSettings.className = SimpleAuthenticator.class.getName();
+
+        // use a credentials graph with one user in it: stephen/password
+        final Map<String,Object> authConfig = new HashMap<>();
+        authConfig.put(SimpleAuthenticator.CONFIG_CREDENTIALS_DB, "conf/tinkergraph-empty.properties");
+        authConfig.put(SimpleAuthenticator.CONFIG_CREDENTIALS_LOCATION, "data/credentials.kryo");
+
+        authSettings.config = authConfig;
+        settings.authentication = authSettings;
+    }
+
+    @Test
+    public void should401OnGETWithNoAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+
+        try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+            assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void should401OnPOSTWithNoAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpPost httppost = new HttpPost("http://localhost:8182");
+        httppost.addHeader("Content-Type", "application/json");
+        httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+        try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+            assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void should401OnGETWithBadAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+        httpget.addHeader("Authorization", "not-base-64-encoded");
+
+        try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+            assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void should401OnPOSTWithBadAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpPost httppost = new HttpPost("http://localhost:8182");
+        httppost.addHeader("Content-Type", "application/json");
+        httppost.addHeader("Authorization", "not-base-64-encoded");
+        httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+        try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+            assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void should401OnGETWithInvalidPasswordAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+        httpget.addHeader("Authorization", encoder.encodeToString("stephen:not-my-password".getBytes()));
+
+        try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+            assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void should401OnPOSTWithInvalidPasswordAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpPost httppost = new HttpPost("http://localhost:8182");
+        httppost.addHeader("Content-Type", "application/json");
+        httppost.addHeader("Authorization", encoder.encodeToString("stephen:not-my-password".getBytes()));
+        httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+        try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+            assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void should200OnGETWithAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+        httpget.addHeader("Authorization", encoder.encodeToString("stephen:password".getBytes()));
+
+        try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+            assertEquals(200, response.getStatusLine().getStatusCode());
+            assertEquals("application/json", response.getEntity().getContentType().getValue());
+            final String json = EntityUtils.toString(response.getEntity());
+            final JsonNode node = mapper.readTree(json);
+            assertEquals(0, node.get("result").get("data").get(0).intValue());
+        }
+    }
+
+    @Test
+    public void should200OnPOSTWithAuthorizationHeader() throws Exception {
+        final CloseableHttpClient httpclient = HttpClients.createDefault();
+        final HttpPost httppost = new HttpPost("http://localhost:8182");
+        httppost.addHeader("Content-Type", "application/json");
+        httppost.addHeader("Authorization", encoder.encodeToString("stephen:password".getBytes()));
+        httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+        try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+            assertEquals(200, response.getStatusLine().getStatusCode());
+            assertEquals("application/json", response.getEntity().getContentType().getValue());
+            final String json = EntityUtils.toString(response.getEntity());
+            final JsonNode node = mapper.readTree(json);
+            assertEquals(0, node.get("result").get("data").get(0).intValue());
+        }
+    }
+
     @Test
     public void should200OnGETWithGremlinQueryStringArgumentWithBindingsAndFunction() throws Exception {
         final CloseableHttpClient httpclient = HttpClients.createDefault();