GitHub OAuth login for GitBlit
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 279d3c9..15c6048 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -548,7 +548,7 @@
 	}

 

 	public static enum AuthenticationType {

-		PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;

+		PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER, GITHUB_OAUTH;

 

 		public boolean isStandard() {

 			return ordinal() <= COOKIE.ordinal();

diff --git a/src/main/java/com/gitblit/auth/github/AuthenticatedHttpRequest.java b/src/main/java/com/gitblit/auth/github/AuthenticatedHttpRequest.java
new file mode 100644
index 0000000..b2b0e86
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/AuthenticatedHttpRequest.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+import com.google.common.collect.Iterators;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+class AuthenticatedHttpRequest extends HttpServletRequestWrapper {
+  private Map<String, String> headers = new HashMap<>();
+
+  AuthenticatedHttpRequest(HttpServletRequest request,
+      String key, String value) {
+    super(request);
+    headers.put(key, value);
+  }
+
+  @Override
+  public Enumeration<String> getHeaderNames() {
+    return Iterators.asEnumeration(
+        Iterators.concat(Iterators.forEnumeration(super.getHeaderNames()),
+            headers.keySet().iterator()));
+  }
+
+  @Override
+  public String getHeader(String name) {
+    String headerValue = headers.get(name);
+    if (headerValue != null) {
+      return headerValue;
+    } else {
+      return super.getHeader(name);
+    }
+  }
+}
diff --git a/src/main/java/com/gitblit/auth/github/GitHubLogin.java b/src/main/java/com/gitblit/auth/github/GitHubLogin.java
new file mode 100644
index 0000000..0776494
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/GitHubLogin.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+import com.google.inject.Inject;
+import com.google.inject.servlet.SessionScoped;
+
+import org.apache.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SessionScoped
+class GitHubLogin {
+  private static final Logger log = LoggerFactory.getLogger(GitHubLogin.class);
+
+  private final OAuthProtocol oauth;
+  private String token;
+  private String user;
+
+  @Inject
+  GitHubLogin(final OAuthProtocol oauth) {
+    this.oauth = oauth;
+  }
+
+  boolean isLoggedIn() {
+    return token != null && user != null;
+  }
+
+  boolean login(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    if (isLoggedIn()) {
+      return true;
+    }
+
+    log.debug("Login " + this);
+
+    if (OAuthProtocol.isOAuthFinal(request)) {
+      String redirectUrl = oauth.getTargetUrl(request);
+      if (redirectUrl == null) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return false;
+      }
+
+      log.debug("Login-Retrieve-User " + this);
+      retrieveUser(oauth.loginPhase2(request, response));
+      if (isLoggedIn()) {
+        log.debug("Login-SUCCESS " + this);
+        response.sendRedirect(redirectUrl);
+        return true;
+      } else {
+        response.sendError(HttpStatus.SC_UNAUTHORIZED);
+        return false;
+      }
+    } else {
+      log.debug("Login-PHASE1 " + this);
+      oauth.loginPhase1(request, response);
+      return false;
+    }
+  }
+
+  void logout() {
+    token = null;
+    user = null;
+  }
+
+  boolean isLoginRequest(HttpServletRequest httpRequest) {
+    return oauth.isOAuthRequest(httpRequest);
+  }
+
+  String getUsername() {
+    return user;
+  }
+
+  @Override
+  public String toString() {
+    return "GitHubLogin [token=" + token + ", user=" + user + "]";
+  }
+
+  private void retrieveUser(String authToken) throws IOException {
+    this.token = authToken;
+    this.user = oauth.retrieveUser(authToken);
+  }
+}
diff --git a/src/main/java/com/gitblit/auth/github/GitHubOAuthConfig.java b/src/main/java/com/gitblit/auth/github/GitHubOAuthConfig.java
new file mode 100644
index 0000000..1a2fb75
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/GitHubOAuthConfig.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import com.gitblit.IStoredSettings;
+import com.google.common.base.CharMatcher;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class GitHubOAuthConfig {
+  private static final String GITHUB_URL = "https://github.com";
+  private static final String GITHUB_API_URL = "https://api.github.com";
+  protected static final String CONF_SECTION = "github";
+  private static final String GITHUB_OAUTH_AUTHORIZE = "/login/oauth/authorize";
+  public static final String GITHUB_OAUTH_ACCESS_TOKEN =
+      "/login/oauth/access_token";
+  private static final String GITHUB_GET_USER = "/user";
+  //private static final String GITHUB_OAUTH_FINAL = "/oauth";
+  static final String GITHUB_LOGIN = "/login";
+
+  private final String gitHubUrl;
+  private final String gitHubApiUrl;
+  final String gitHubUserUrl;
+  final String gitHubClientId;
+  final String gitHubClientSecret;
+  final String httpHeader;
+  final String gitHubOAuthUrl;
+  final String oAuthFinalRedirectUrl;
+  final String gitHubOAuthAccessTokenUrl;
+  final boolean autoLogin;
+
+  @Inject
+  GitHubOAuthConfig(IStoredSettings settings)
+      throws MalformedURLException {
+    httpHeader = settings.getString("httpHeader", "GITHUB_USER");
+    gitHubUrl = GITHUB_URL;
+    gitHubApiUrl = GITHUB_API_URL;
+    gitHubClientId = settings.getString("clientId", "4711");
+    gitHubClientSecret = settings.getString("clientSecret", "4712");
+
+    gitHubOAuthUrl = getUrl(gitHubUrl, GITHUB_OAUTH_AUTHORIZE);
+    gitHubOAuthAccessTokenUrl = getUrl(gitHubUrl, GITHUB_OAUTH_ACCESS_TOKEN);
+    gitHubUserUrl = getUrl(gitHubApiUrl, GITHUB_GET_USER);
+    oAuthFinalRedirectUrl = settings.getString("canonicalWebUrl",
+        "http://locahost:8080");
+    autoLogin = false;
+  }
+
+  private static String trimTrailingSlash(String url) {
+    return CharMatcher.is('/').trimTrailingFrom(url);
+  }
+
+  private String getUrl(String baseUrl, String path)
+      throws MalformedURLException {
+    if (baseUrl.indexOf("://") > 0) {
+      return new URL(new URL(baseUrl), path).toExternalForm();
+    } else {
+      return baseUrl + trimTrailingSlash(baseUrl) + "/"
+          + CharMatcher.is('/').trimLeadingFrom(path);
+    }
+  }
+}
diff --git a/src/main/java/com/gitblit/auth/github/HttpGitHubOAuthModule.java b/src/main/java/com/gitblit/auth/github/HttpGitHubOAuthModule.java
new file mode 100644
index 0000000..91332b2
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/HttpGitHubOAuthModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+import com.google.inject.servlet.ServletModule;
+
+import org.apache.http.client.HttpClient;
+
+/** Servlets and support related to GitHub OAuth authentication. */
+public class HttpGitHubOAuthModule extends ServletModule {
+
+  @Override
+  protected void configureServlets() {
+      filter("/").through(OAuthWebFilter.class);
+      filter("/login").through(OAuthWebFilter.class);
+      filter("/oauth").through(OAuthWebFilter.class);
+
+      bind(HttpClient.class).toProvider(PooledHttpClientProvider.class);
+  }
+}
diff --git a/src/main/java/com/gitblit/auth/github/OAuthProtocol.java b/src/main/java/com/gitblit/auth/github/OAuthProtocol.java
new file mode 100644
index 0000000..8e6b284
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/OAuthProtocol.java
@@ -0,0 +1,240 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.inject.Inject;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URLEncoder;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class OAuthProtocol {
+  private static final String ME_SEPARATOR = ",";
+  private static final Logger log = LoggerFactory
+      .getLogger(OAuthProtocol.class);
+
+  private final GitHubOAuthConfig config;
+  private final HttpClient http;
+  private final Gson gson;
+  private final String state;
+
+  @Inject
+  OAuthProtocol(GitHubOAuthConfig config, HttpClient http,
+      Gson gson) {
+    this.config = config;
+    this.http = http;
+    this.gson = gson;
+    this.state = generateRandomState();
+  }
+
+  void loginPhase1(HttpServletRequest request,
+      HttpServletResponse response) throws IOException {
+    log.debug("Initiating GitHub Login for ClientId=" + config.gitHubClientId);
+    response.sendRedirect(String.format(
+        "%s?client_id=%s&redirect_uri=%s&state=%s%s", config.gitHubOAuthUrl,
+        config.gitHubClientId, getURLEncoded(config.oAuthFinalRedirectUrl),
+        me(), getURLEncoded(request.getRequestURI().toString())));
+  }
+
+  String loginPhase2(HttpServletRequest request,
+      HttpServletResponse response) throws IOException {
+    HttpPost post = new HttpPost(config.gitHubOAuthAccessTokenUrl);
+    post.setHeader("Accept", "application/json");
+    List<NameValuePair> nvps = new ArrayList<>(3);
+    nvps.add(new BasicNameValuePair("client_id", config.gitHubClientId));
+    nvps.add(new BasicNameValuePair("client_secret",
+        config.gitHubClientSecret));
+    nvps.add(new BasicNameValuePair("code", request.getParameter("code")));
+    post.setEntity(new UrlEncodedFormEntity(nvps));
+
+    try {
+      HttpResponse postResponse = http.execute(post);
+      if (postResponse.getStatusLine().getStatusCode() !=
+          HttpURLConnection.HTTP_OK) {
+        log.error("POST " + config.gitHubOAuthAccessTokenUrl
+            + " request for access token failed with status "
+            + postResponse.getStatusLine());
+        response.sendError(HttpURLConnection.HTTP_UNAUTHORIZED,
+            "Request for access token not authorised");
+        EntityUtils.consume(postResponse.getEntity());
+        return null;
+      }
+
+      return getAccessToken(getAccessTokenJson(postResponse));
+    } catch (IOException e) {
+      log.error("POST " + config.gitHubOAuthAccessTokenUrl
+          + " request for access token failed", e);
+      response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
+          "Request for access token not authorised");
+      return null;
+    }
+  }
+
+  String retrieveUser(String authToken) throws IOException {
+    HttpGet get = new HttpGet(config.gitHubUserUrl);
+    get.setHeader("Authorization", String.format("token %s", authToken));
+    try {
+      return getLogin(getUserJson(httpGetGitHubUserInfo(get)));
+    } catch (IOException e) {
+      log.error("GET {} with authToken {} request failed",
+          config.gitHubUserUrl, config.gitHubOAuthAccessTokenUrl, e);
+      return null;
+    }
+  }
+
+  private InputStream httpGetGitHubUserInfo(HttpGet get) throws IOException,
+      ClientProtocolException {
+    HttpResponse resp = http.execute(get);
+    int statusCode = resp.getStatusLine().getStatusCode();
+    if (statusCode == HttpServletResponse.SC_OK) {
+      return resp.getEntity().getContent();
+    } else {
+      throw new IOException(String.format(
+          "Invalid HTTP status code %s returned from %s", statusCode,
+          get.getURI()));
+    }
+  }
+
+  private String getAccessToken(JsonElement accessTokenJson)
+      throws IOException {
+    JsonElement accessTokenString =
+        accessTokenJson.getAsJsonObject().get("access_token");
+    if (accessTokenString != null) {
+      return accessTokenString.getAsString();
+    } else {
+      throw new IOException(String.format(
+          "Invalid JSON '%s': cannot find access_token field",
+          accessTokenJson));
+    }
+  }
+
+  private JsonObject getAccessTokenJson(HttpResponse postResponse)
+      throws UnsupportedEncodingException, IOException {
+    JsonElement accessTokenJson =
+        gson.fromJson(new InputStreamReader(postResponse.getEntity()
+            .getContent(), Charsets.UTF_8), JsonElement.class);
+    if (accessTokenJson.isJsonObject()) {
+      return accessTokenJson.getAsJsonObject();
+    } else {
+      throw new IOException(String.format(
+          "Invalid JSON '%s': not a JSON Object"));
+    }
+  }
+
+  boolean isOAuthRequest(HttpServletRequest httpRequest) {
+    return OAuthProtocol.isGerritLogin(httpRequest)
+        || OAuthProtocol.isOAuthFinal(httpRequest);
+  }
+
+  String getTargetUrl(ServletRequest request) {
+    String requestState = state(request);
+    int meEnd = requestState.indexOf(ME_SEPARATOR);
+    if (meEnd >= 0 && requestState.subSequence(0, meEnd).equals(state)) {
+      return requestState.substring(meEnd + 1);
+    } else {
+      log.warn("Illegal request state '" + requestState + "' on OAuthProtocol "
+          + this);
+      return null;
+    }
+  }
+
+  private String me() {
+    return state + ME_SEPARATOR;
+  }
+
+  private JsonObject getUserJson(InputStream userContentStream)
+      throws IOException {
+    JsonElement userJson =
+        gson.fromJson(new InputStreamReader(userContentStream,
+            Charsets.UTF_8), JsonElement.class);
+    if (userJson.isJsonObject()) {
+      return userJson.getAsJsonObject();
+    } else {
+      throw new IOException(String.format(
+          "Invalid JSON '%s': not a JSON Object", userJson));
+    }
+  }
+
+  static boolean isOAuthFinal(HttpServletRequest request) {
+    return Strings.emptyToNull(request.getParameter("code")) != null;
+  }
+
+  static boolean isGerritLogin(HttpServletRequest request) {
+    return request.getRequestURI().indexOf(
+        GitHubOAuthConfig.GITHUB_LOGIN) >= 0;
+  }
+
+  private static String getLogin(JsonElement userJson) throws IOException {
+    JsonElement userString = userJson.getAsJsonObject().get("login");
+    if (userString != null) {
+      return userString.getAsString();
+    } else {
+      throw new IOException(String.format(
+          "Invalid JSON '%s': cannot find login field", userJson));
+    }
+  }
+
+  private static String generateRandomState() {
+    byte[] randomState = new byte[32];
+    new SecureRandom().nextBytes(randomState);
+    return Base64.encodeBase64URLSafeString(randomState);
+  }
+
+  private static String getURLEncoded(String url) {
+    try {
+      return URLEncoder.encode(url, Charsets.UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      // UTF-8 is hardcoded, cannot fail
+      return null;
+    }
+  }
+
+  private static String state(ServletRequest request) {
+    return Strings.nullToEmpty(request.getParameter("state"));
+  }
+
+  @Override
+  public String toString() {
+    return "OAuthProtocol/" + state;
+  }
+}
diff --git a/src/main/java/com/gitblit/auth/github/OAuthWebFilter.java b/src/main/java/com/gitblit/auth/github/OAuthWebFilter.java
new file mode 100644
index 0000000..905bdc0
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/OAuthWebFilter.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+
+import com.gitblit.manager.IAuthenticationManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+@Singleton
+class OAuthWebFilter implements Filter {
+  private static final Logger log = LoggerFactory
+      .getLogger(OAuthWebFilter.class);
+
+  private final GitHubOAuthConfig config;
+  private final Provider<GitHubLogin> loginProvider;
+  private final IAuthenticationManager authenticationManager;
+
+  @Inject
+  OAuthWebFilter(GitHubOAuthConfig config,
+      Provider<GitHubLogin> loginProvider,
+      IAuthenticationManager authenticationManager) {
+    this.config = config;
+    this.loginProvider = loginProvider;
+    this.authenticationManager = authenticationManager;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpSession httpSession = ((HttpServletRequest) request).getSession(false);
+    // TODO(davido): FixMe: How to do that on gitblit?
+    //if (authenticationManager.isIdentifiedUser()) {
+    if (false) {
+      if (httpSession != null) {
+        httpSession.invalidate();
+      }
+      chain.doFilter(request, response);
+      return;
+    }
+
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+    HttpServletResponse httpResponse = (HttpServletResponse) response;
+    log.debug("OAuthWebFilter({})", httpRequest.getRequestURL());
+
+    GitHubLogin ghLogin = loginProvider.get();
+
+    if (ghLogin.isLoginRequest(httpRequest) && !ghLogin.isLoggedIn()) {
+      ghLogin.login(httpRequest, httpResponse);
+    } else if (config.autoLogin && !ghLogin.isLoggedIn()) {
+      httpResponse.sendRedirect("/login");
+    } else {
+      if (ghLogin.isLoggedIn()) {
+        httpRequest =
+            new AuthenticatedHttpRequest(httpRequest, config.httpHeader,
+                ghLogin.getUsername());
+      }
+      chain.doFilter(httpRequest, response);
+    }
+  }
+
+  @Override
+  public void destroy() {
+  }
+}
diff --git a/src/main/java/com/gitblit/auth/github/PooledHttpClientProvider.java b/src/main/java/com/gitblit/auth/github/PooledHttpClientProvider.java
new file mode 100644
index 0000000..3eadfb5
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/github/PooledHttpClientProvider.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.gitblit.auth.github;
+
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+class PooledHttpClientProvider implements Provider<HttpClient> {
+
+  @Override
+  public HttpClient get() {
+	  // TODO(davido): handle proxy
+	  // TODO(davido): externalize MaxConnPerRoute && MaxConnTotal values
+      return HttpClientBuilder
+         .create()
+         .setMaxConnPerRoute(100)
+         .setMaxConnTotal(1024)
+         .build();
+  }
+}
diff --git a/src/main/java/com/gitblit/guice/WebModule.java b/src/main/java/com/gitblit/guice/WebModule.java
index 5b56918..35ef572 100644
--- a/src/main/java/com/gitblit/guice/WebModule.java
+++ b/src/main/java/com/gitblit/guice/WebModule.java
@@ -19,6 +19,7 @@
 import java.util.Map;
 
 import com.gitblit.Constants;
+import com.gitblit.auth.github.HttpGitHubOAuthModule;
 import com.gitblit.servlet.BranchGraphServlet;
 import com.gitblit.servlet.DownloadZipFilter;
 import com.gitblit.servlet.DownloadZipServlet;
@@ -92,6 +93,11 @@
 		params.put(GitblitWicketFilter.FILTER_MAPPING_PARAM, ALL);
 		params.put(GitblitWicketFilter.IGNORE_PATHS_PARAM, toIgnore);
 		filter(ALL).through(GitblitWicketFilter.class, params);
+
+		//if (AUTH_METHOD == GITHUB_OAUTH) {
+		if (true) {
+			install(new HttpGitHubOAuthModule());
+		}
 	}
 
 	private String fuzzy(String path) {
diff --git a/src/site/setup_authentication.mkd b/src/site/setup_authentication.mkd
index 02d1be7..10c41ec 100644
--- a/src/site/setup_authentication.mkd
+++ b/src/site/setup_authentication.mkd
@@ -4,6 +4,7 @@
 

 Gitblit supports additional authentication mechanisms aside from it's internal one.

 

+* GitHub OAuth

 * LDAP authentication

 * Windows authentication

 * PAM authentication

@@ -12,6 +13,43 @@
 * Salesforce.com authentication

 * Servlet container authentication

 

+### GitHub OAuth

+*SINCE 1.7.0

+

+OAuth2 is a protocol that lets external apps request authorization to private

+details in a user’s GitHub account without getting their password. This is

+preferred over Basic Authentication because tokens can be limited to specific

+types of data, and can be revoked by users at any time.

++

+Site owners have to register their application before getting started.  For

+more information see

+https://github.com/settings/applications/new[github-register-application].

+A registered OAuth application is assigned a unique `Client ID` and `Client

+Secret`. The `Client Secret` should never be shared.

+

+[[github.url]]github.url::

+

+GitHub URL.

+

+Default is `https://github.com`.

+

+[[github.apiUrl]]github.apiUrl::

+

+GitHub API URL.

+

+Default is `https://api.github.com`.

+

+[[github.clientId]]github.clientId::

+

+The `Client ID`, that was received from GitHub when the application was

+registered. Required.

+

+[[github.clientSecret]]github.clientSecret::

+

+The `Client Secret`, that was received from GitHub when the application was

+registered. Required.

+

+

 ### LDAP Authentication

 *SINCE 1.0.0*