Implementation of Gerrit OAuthServiceProvider

The OAUTH authentication has been finally merged and we can start
using the new OAuthServiceProvider interface to plugin the GitHub
plugin as official authentication instead of relying of a generic HTTP
auth.

Not everything works though (e.g. Group backend, profile import,
repositories, pull-requests) and thus when choosing OAUTH during
init those features are actually disabled.

As soon as the Gerrit OAUTH provider will be mature enough to support
them the HTTP auth style can be removed safely. In the meantime it is
possible to choose HTTP / OAUTH implementation during init:

    Gerrit OAuth implementation    [HTTP/?]: ?
       Supported options are:
         http
         oauth
    Gerrit OAuth implementation    [HTTP/?]: oauth

Change-Id: If2620a2eea333a2ae6ec1b4fcd03f4f2f79ee41a
diff --git a/github-oauth/pom.xml b/github-oauth/pom.xml
index 6a55e54..b900a3a 100644
--- a/github-oauth/pom.xml
+++ b/github-oauth/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.googlesource.gerrit.plugins.github</groupId>
     <artifactId>github-parent</artifactId>
-    <version>2.10-SNAPSHOT</version>
+    <version>2.10.3</version>
   </parent>
   <artifactId>github-oauth</artifactId>
   <name>Gerrit Code Review - GitHub OAuth login</name>
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
index 61f4890..74712bd 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
@@ -31,10 +31,11 @@
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
 
 @Singleton
 public class OAuthProtocol {
-
   public static enum Scope {
     DEFAULT(""),
     USER("user"),
@@ -71,6 +72,7 @@
     public String error;
     public String errorDescription;
     public String errorUri;
+    public String raw;
 
     public AccessToken() {
     }
@@ -79,7 +81,7 @@
       this(token, "");
     }
 
-    public AccessToken(String token, String type, Scope... scopes) {
+    public AccessToken(String token, String type) {
       this();
       this.accessToken = token;
       this.tokenType = type;
@@ -126,6 +128,18 @@
     public boolean isError() {
       return !Strings.isNullOrEmpty(error);
     }
+
+    public String getRaw() {
+      return raw;
+    }
+
+    public void setRaw(String raw) {
+      this.raw = raw;
+    }
+
+    public OAuthToken toOAuthToken() {
+      return new OAuthToken(accessToken, null, getRaw());
+    }
   }
 
   @Inject
@@ -139,23 +153,27 @@
     this.gson = gsonProvider.get();
   }
 
+  public String getAuthorizationUrl(String scopesString, String state) {
+    return config.gitHubOAuthUrl + "?client_id=" + config.gitHubClientId
+        + getURLEncodedParameter("&scope=", scopesString)
+        + getURLEncodedParameter("&redirect_uri=", config.oAuthFinalRedirectUrl)
+        + getURLEncodedParameter("&state=", state);
+  }
+
   public void loginPhase1(HttpServletRequest request,
       HttpServletResponse response, Set<Scope> scopes) throws IOException {
 
     String scopesString = getScope(scopes);
     LOG.debug("Initiating GitHub Login for ClientId=" + config.gitHubClientId + " Scopes=" + scopesString);
-    response.sendRedirect(String.format(
-        "%s?client_id=%s%s&redirect_uri=%s&state=%s%s", config.gitHubOAuthUrl,
-        config.gitHubClientId, scopesString,
-        getURLEncoded(config.oAuthFinalRedirectUrl),
-        me(), getURLEncoded(request.getRequestURI().toString())));
+    response.sendRedirect(getAuthorizationUrl(scopesString, 
+        me() + request.getRequestURI().toString()));
   }
 
-  private String getScope(Set<Scope> scopes) {
+  public String getScope(Set<Scope> scopes) {
     if(scopes.size() <= 0) {
       return "";
     }
-    
+
     StringBuilder out = new StringBuilder();
     for (Scope scope : scopes) {
       if(out.length() > 0) {
@@ -163,13 +181,13 @@
       }
       out.append(scope.getValue());
     }
-    return "&" + "scope=" + out.toString();
+    return out.toString();
   }
 
   public static boolean isOAuthFinal(HttpServletRequest request) {
     return Strings.emptyToNull(request.getParameter("code")) != null;
   }
-  
+
   public static boolean isOAuthFinalForOthers(HttpServletRequest request) {
     String targetUrl = getTargetUrl(request);
     if(targetUrl.equals(request.getRequestURI())) {
@@ -197,47 +215,46 @@
 
   public AccessToken loginPhase2(HttpServletRequest request,
       HttpServletResponse response) throws IOException {
+    return getAccessToken(new OAuthVerifier(request.getParameter("code")));
+  }
 
-    HttpPost post = null;
-
-    post = new HttpPost(config.gitHubOAuthAccessTokenUrl);
+  public AccessToken getAccessToken(OAuthVerifier code) throws IOException {
+    HttpPost post = new HttpPost(config.gitHubOAuthAccessTokenUrl);
     post.setHeader("Accept", "application/json");
     List<NameValuePair> nvps = new ArrayList<NameValuePair>();
     nvps.add(new BasicNameValuePair("client_id", config.gitHubClientId));
     nvps.add(new BasicNameValuePair("client_secret", config.gitHubClientSecret));
-    nvps.add(new BasicNameValuePair("code", request.getParameter("code")));
+    nvps.add(new BasicNameValuePair("code", code.getValue()));
     post.setEntity(new UrlEncodedFormEntity(nvps, Charsets.UTF_8));
 
-    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());
-        EntityUtils.consume(postResponse.getEntity());
-        return null;
-      }
-
-      InputStream content = postResponse.getEntity().getContent();
-      String tokenJsonString =
-          CharStreams.toString(new InputStreamReader(content,
-              StandardCharsets.UTF_8));
-      AccessToken token = gson.fromJson(tokenJsonString, AccessToken.class);
-      if (token.isError()) {
-        LOG.error("POST " + config.gitHubOAuthAccessTokenUrl
-            + " returned an error token: " + token);
-      }
-      return token;
-    } catch (IOException e) {
+    HttpResponse postResponse = http.execute(post);
+    if (postResponse.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) {
       LOG.error("POST " + config.gitHubOAuthAccessTokenUrl
-          + " request for access token failed", e);
+          + " request for access token failed with status "
+          + postResponse.getStatusLine());
+      EntityUtils.consume(postResponse.getEntity());
       return null;
     }
+
+    InputStream content = postResponse.getEntity().getContent();
+    String tokenJsonString =
+        CharStreams.toString(new InputStreamReader(content,
+            StandardCharsets.UTF_8));
+    AccessToken token = gson.fromJson(tokenJsonString, AccessToken.class);
+    token.setRaw(tokenJsonString);
+    if (token.isError()) {
+      LOG.error("POST " + config.gitHubOAuthAccessTokenUrl
+          + " returned an error token: " + token);
+      throw new IOException("Invalid GitHub OAuth token");
+    }
+    
+    return token;
   }
 
-  private static String getURLEncoded(String url) {
+  private static String getURLEncodedParameter(String prefix, String url) {
     try {
-      return URLEncoder.encode(url, "UTF-8");
+      return Strings.isNullOrEmpty(url) ? 
+          "" : (prefix + URLEncoder.encode(url,"UTF-8"));
     } catch (UnsupportedEncodingException e) {
       // UTF-8 is hardcoded, cannot fail
       return null;
@@ -259,10 +276,9 @@
 
   public static String getTargetOAuthFinal(HttpServletRequest httpRequest) {
     String targetUrl = getTargetUrl(httpRequest);
-    String code = getURLEncoded(httpRequest.getParameter("code"));
-    String state = getURLEncoded(httpRequest.getParameter("state"));
-    return targetUrl + (targetUrl.indexOf('?') < 0 ? '?' : '&') + "code="
-        + code + "&state=" + state;
+    String code = getURLEncodedParameter("code=", httpRequest.getParameter("code"));
+    String state = getURLEncodedParameter("&state=", httpRequest.getParameter("state"));
+    return targetUrl + (targetUrl.indexOf('?') < 0 ? '?' : '&') + code + state;
   }
 
   @Override
diff --git a/github-plugin/pom.xml b/github-plugin/pom.xml
index 9f61ef8..2a923b0 100644
--- a/github-plugin/pom.xml
+++ b/github-plugin/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <artifactId>github-parent</artifactId>
     <groupId>com.googlesource.gerrit.plugins.github</groupId>
-    <version>2.10-SNAPSHOT</version>
+    <version>2.10.3</version>
   </parent>
 
   <artifactId>github-plugin</artifactId>
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubOAuthServiceProvider.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubOAuthServiceProvider.java
new file mode 100644
index 0000000..9e54c60
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubOAuthServiceProvider.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2015 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.googlesource.gerrit.plugins.github;
+
+import java.io.IOException;
+
+import org.kohsuke.github.GHMyself;
+import org.kohsuke.github.GitHub;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.github.oauth.GitHubOAuthConfig;
+import com.googlesource.gerrit.plugins.github.oauth.OAuthProtocol;
+
+public class GitHubOAuthServiceProvider implements OAuthServiceProvider {
+  public static final String VERSION = "2.10.3";
+  private static final Logger log = LoggerFactory
+      .getLogger(GitHubOAuthServiceProvider.class);
+
+  private final GitHubOAuthConfig config;
+  private final OAuthProtocol oauth;
+  private final String name;
+
+  @Inject
+  public GitHubOAuthServiceProvider(GitHubOAuthConfig config,
+      OAuthProtocol oauth,
+      @PluginName String name) {
+    this.config = config;
+    this.oauth = oauth;
+    this.name = name;
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    return oauth.getAuthorizationUrl(
+        oauth.getScope(Sets.newHashSet(config.getDefaultScopes())), null);
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier verifier) {
+    try {
+      return oauth.getAccessToken(verifier).toOAuthToken();
+    } catch (IOException e) {
+      log.error("Invalid OAuth access verifier" + verifier.getValue(), e);
+      return null;
+    }
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    String oauthToken = token.getToken();
+    GitHub hub = GitHub.connectUsingOAuth(oauthToken);
+    GHMyself myself = hub.getMyself();
+    String login = myself.getLogin();
+    return new OAuthUserInfo(AccountExternalId.SCHEME_GERRIT + login, login,
+        myself.getEmail(), myself.getName(), null);
+  }
+
+  @Override
+  public String getVersion() {
+    return VERSION;
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java
index 34c4d73..4b97fad 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java
@@ -20,8 +20,10 @@
 import com.google.gerrit.extensions.annotations.Listen;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -29,12 +31,14 @@
 @Listen
 @Singleton
 public class GitHubTopMenu implements TopMenu {
-  private List<MenuEntry> menuEntries;
-  private Provider<CurrentUser> userProvider;
+  private final List<MenuEntry> menuEntries;
+  private final Provider<CurrentUser> userProvider;
+  private final AuthConfig authConfig;
 
   @Inject
-  public GitHubTopMenu(final @PluginName String pluginName,
-      final Provider<CurrentUser> userProvider) {
+  public GitHubTopMenu(@PluginName String pluginName,
+      Provider<CurrentUser> userProvider,
+      AuthConfig authConfig) {
     String baseUrl = "/plugins/" + pluginName;
     this.menuEntries =
         Arrays.asList(new MenuEntry("GitHub", Arrays.asList(
@@ -42,6 +46,7 @@
             getItem("Repositories", baseUrl + "/static/repositories.html"),
             getItem("Pull Requests", baseUrl + "/static/pullrequests.html"))));
     this.userProvider = userProvider;
+    this.authConfig = authConfig;
   }
 
   private MenuItem getItem(String anchorName, String urlPath) {
@@ -50,7 +55,9 @@
 
   @Override
   public List<MenuEntry> getEntries() {
-    if (userProvider.get() instanceof IdentifiedUser) {
+    if (userProvider.get() instanceof IdentifiedUser &&
+        // Only with HTTP authentication we can transparently trigger OAuth if needed
+        authConfig.getAuthType().equals(AuthType.HTTP)) {
       return menuEntries;
     } else {
       return Collections.emptyList();
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
index 1b4bc0b..00f8201 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
@@ -16,6 +16,8 @@
 import org.apache.http.client.HttpClient;
 import org.apache.velocity.runtime.RuntimeInstance;
 
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.name.Names;
@@ -65,6 +67,9 @@
     bind(String.class).annotatedWith(GitHubURL.class).toProvider(
         GitHubURLProvider.class);
 
+    bind(OAuthServiceProvider.class).annotatedWith(
+        Exports.named("github")).to(GitHubOAuthServiceProvider.class);
+
     serve("*.css", "*.js", "*.png", "*.jpg", "*.woff", "*.gif", "*.ttf").with(
         VelocityStaticServlet.class);
     serve("*.html").with(VelocityViewServlet.class);
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/InitGitHub.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/InitGitHub.java
index 5e56cf9..cb48f34 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/InitGitHub.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/InitGitHub.java
@@ -22,7 +22,15 @@
   private final ConsoleUI ui;
   private final Section auth;
   private final Section httpd;
-  private Section github;
+  private final Section github;
+  
+  public enum OAuthType {
+    /* Legacy Gerrit/HTTP authentication for GitHub through HTTP Header enrichment */
+    HTTP,
+    
+    /* New native Gerrit/OAuth authentication provider */
+    OAUTH
+  }
 
   @Inject
   InitGitHub(final ConsoleUI ui, final Section.Factory sections) {
@@ -45,12 +53,18 @@
   }
 
   private void configureAuth() {
-    github.string("ClientId", "clientId", null);
-    github.string("ClientSecret", "clientSecret", null);
-
-    auth.string("HTTP Authentication Header", "httpHeader", "GITHUB_USER");
-    auth.set("type", "HTTP");
-    httpd.set("filterClass", "com.googlesource.gerrit.plugins.github.oauth.OAuthFilter");
+    github.string("GitHub Client ID", "clientId", null);
+    github.passwordForKey("GitHub Client Secret", "clientSecret");
+    
+    OAuthType authType = auth.select("Gerrit OAuth implementation", "type", OAuthType.HTTP);
+    if (authType.equals(OAuthType.HTTP)) {
+      auth.string("HTTP Authentication Header", "httpHeader", "GITHUB_USER");
+      httpd.set("filterClass",
+          "com.googlesource.gerrit.plugins.github.oauth.OAuthFilter");
+    } else {
+      httpd.unset("filterClass");
+      httpd.unset("httpHeader");
+    }
   }
 
   @Override
diff --git a/pom.xml b/pom.xml
index e65c897..1467223 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.googlesource.gerrit.plugins.github</groupId>
   <artifactId>github-parent</artifactId>
-  <version>2.10-SNAPSHOT</version>
+  <version>2.10.3</version>
   <name>Gerrit Code Review - GitHub integration</name>
   <url>http://www.gerritforge.com</url>
   <packaging>pom</packaging>