Resilience against GerritOAuth and JSESSIONID cookies lost

GitHub plugin uses two main cookies to cache OAuth
information:
- GerritOAuth to enrich HTTP requests with auth headers used by
  Gerrit HTTP authentication
- JSESSIONID to get session objects with GitHub client objects

When GerritOAuth or JSESSIONID expire, a transparent GitHub OAuth
renegotiation is needed to recreate them through a new authentication
challenge. OAuth scope is now stored in the GerritOAuth cookie in order
to re-create the same scope-level previously own.

NOTE: GerritOAuth is typically short lived to avoid
      security leak of the OAuth information. JSESSIONID could be lost
      because of workload balancing or failover.

Change-Id: I874c4ae75f2b238dccb0ec8c164628c28f1b5f01
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
index 459b9da..da86a02 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
@@ -15,9 +15,16 @@
 package com.googlesource.gerrit.plugins.github.oauth;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -27,6 +34,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -42,6 +50,13 @@
     public Provider(com.google.inject.Provider<GitHubLogin> provider) {
       super(provider);
     }
+
+    @Override
+    public GitHubLogin get(HttpServletRequest req) {
+      GitHubLogin login = super.get(req);
+      login.initOAuthCookie(req);
+      return login;
+    }
   }
 
   public AccessToken token;
@@ -50,7 +65,10 @@
   private transient OAuthProtocol oauth;
 
   private GHMyself myself;
+  private SortedSet<Scope> loginScopes;
   private final OAuthCookieProvider cookieProvider;
+  private final GitHubOAuthConfig config;
+  private OAuthCookie oAuthCookie;
 
   public GHMyself getMyself() {
     if (isLoggedIn()) {
@@ -61,9 +79,10 @@
   }
 
   @Inject
-  public GitHubLogin(OAuthProtocol oauth) {
+  public GitHubLogin(final OAuthProtocol oauth, final GitHubOAuthConfig config) {
     this.oauth = oauth;
     this.cookieProvider = new OAuthCookieProvider(TokenCipher.get());
+    this.config = config;
   }
 
   public boolean isLoggedIn() {
@@ -80,6 +99,25 @@
     return loggedIn;
   }
 
+  private void initOAuthCookie(HttpServletRequest request) {
+    for (Cookie cookie : getCookies(request)) {
+      if (cookie.getName().equalsIgnoreCase(OAuthCookie.OAUTH_COOKIE_NAME)
+          && !Strings.isNullOrEmpty(cookie.getValue())) {
+        try {
+          oAuthCookie = cookieProvider.getFromCookie(cookie);
+          loginScopes = oAuthCookie.scopes;
+        } catch (OAuthTokenException e) {
+          LOG.warn("Invalid cookie detected", e);
+        }
+      }
+    }
+  }
+
+  private Cookie[] getCookies(HttpServletRequest httpRequest) {
+    Cookie[] cookies = httpRequest.getCookies();
+    return cookies == null ? new Cookie[0] : cookies;
+  }
+
   public boolean login(ServletRequest request, ServletResponse response,
       Scope... scopes) throws IOException {
     return login((HttpServletRequest) request, (HttpServletResponse) response,
@@ -106,7 +144,7 @@
                 .getName();
 
         OAuthCookie userCookie =
-            cookieProvider.getFromUser(user, email, fullName);
+            cookieProvider.getFromUser(user, email, fullName, loginScopes);
         response.addCookie(userCookie);
 
         response.sendRedirect(OAuthProtocol.getTargetUrl(request));
@@ -116,8 +154,9 @@
         return false;
       }
     } else {
+      this.loginScopes = getScopes(getScopesKey(request), scopes);
       LOG.debug("Login-PHASE1 " + this);
-      oauth.loginPhase1(request, response, scopes);
+      oauth.loginPhase1(request, response, loginScopes);
       return false;
     }
   }
@@ -139,7 +178,23 @@
 
   @Override
   public String toString() {
-    return "GitHubLogin [token=" + token + ", myself=" + myself + "]";
+    return "GitHubLogin [token=" + token + ", myself=" + myself + ", scopes="
+        + loginScopes + "]";
+  }
+
+  private String getScopesKey(HttpServletRequest request) {
+    String scopeRequested = request.getParameter("scope");
+    return Objects.firstNonNull(scopeRequested, "scopes");
+  }
+
+  private SortedSet<Scope> getScopes(String baseScopeKey, Scope... scopes) {
+    HashSet<Scope> fullScopes =
+        oAuthCookie == null ? new HashSet<Scope>(
+            config.scopes.get(baseScopeKey)) : new HashSet<Scope>(
+            oAuthCookie.scopes);
+    fullScopes.addAll(Arrays.asList(scopes));
+
+    return new TreeSet<Scope>(fullScopes);
   }
 
 }
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java
index d886b82..54fee4f 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java
@@ -13,8 +13,13 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.github.oauth;
 
+import java.util.SortedSet;
+import java.util.TreeSet;
+
 import javax.servlet.http.Cookie;
 
+import com.googlesource.gerrit.plugins.github.oauth.OAuthProtocol.Scope;
+
 public class OAuthCookie extends Cookie {
   private static final long serialVersionUID = 2771690299147135167L;
   public static final String OAUTH_COOKIE_NAME = "GerritOAuth";
@@ -22,16 +27,34 @@
   public final String user;
   public final String email;
   public final String fullName;
+  public final SortedSet<Scope> scopes;
 
   public OAuthCookie(TokenCipher cipher, final String user, final String email,
-      final String fullName) throws OAuthTokenException {
-    super(OAUTH_COOKIE_NAME, cipher.encode(String.format("%s\n%s\n%s", user,
-        email, fullName)));
+      final String fullName, final SortedSet<Scope> scopes) throws OAuthTokenException {
+    super(OAUTH_COOKIE_NAME, cipher.encode(getClearTextCookie(user, email, fullName, scopes)));
     this.user = user;
     this.email = email;
     this.fullName = fullName;
+    this.scopes = scopes;
     setMaxAge((int) (TokenCipher.COOKIE_TIMEOUT/1000L));
     setHttpOnly(true);
+    setPath("/");
+  }
+
+  private static String getClearTextCookie(final String user,
+      final String email, final String fullName, final SortedSet<Scope> scopes) {
+    StringBuilder clearTextCookie = new StringBuilder();
+    clearTextCookie.append(user);
+    clearTextCookie.append("\n");
+    clearTextCookie.append(email);
+    clearTextCookie.append("\n");
+    clearTextCookie.append(fullName);
+
+    for (Scope scope : scopes) {
+      clearTextCookie.append("\n");
+      clearTextCookie.append(scope);
+    }
+    return clearTextCookie.toString();
   }
 
   public OAuthCookie(TokenCipher cipher, Cookie cookie)
@@ -42,5 +65,11 @@
     user = clearText.length > 0 ? clearText[0]:null;
     email = clearText.length > 1 ? clearText[1]:null;
     fullName = clearText.length > 2 ? clearText[2]:null;
+
+    this.scopes = new TreeSet<Scope>();
+    for (int i = 3; i < clearText.length; i++) {
+      Scope scope = Enum.valueOf(Scope.class, clearText[i]);
+      this.scopes.add(scope);
+    }
   }
 }
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java
index 65cd93e..185002c 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java
@@ -13,17 +13,11 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.github.oauth;
 
-import java.net.URLEncoder;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
+import java.util.SortedSet;
 
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
 import javax.servlet.http.Cookie;
 
-import org.slf4j.Logger;
+import com.googlesource.gerrit.plugins.github.oauth.OAuthProtocol.Scope;
 
 public class OAuthCookieProvider {
 
@@ -33,9 +27,9 @@
     this.cipher = cipher;
   }
 
-  public OAuthCookie getFromUser(String username, String email, String fullName) {
+  public OAuthCookie getFromUser(String username, String email, String fullName, SortedSet<Scope> scopes) {
     try {
-      return new OAuthCookie(cipher, username, email, fullName);
+      return new OAuthCookie(cipher, username, email, fullName, scopes);
     } catch (OAuthTokenException e) {
       return null;
     }
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
index ba057ff..fa604db 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
@@ -89,27 +89,16 @@
       OAuthCookie authCookie =
           getOAuthCookie(httpRequest, (HttpServletResponse) response);
 
-      if(OAuthProtocol.isOAuthLogout((HttpServletRequest) request)) {
+      if (OAuthProtocol.isOAuthLogout((HttpServletRequest) request)) {
         logout(request, response, chain, httpRequest);
-      } else if ((OAuthProtocol.isOAuthLogin(httpRequest) || OAuthProtocol.isOAuthFinal(httpRequest)) && !ghLogin.isLoggedIn()) {        
+      } else if (OAuthProtocol.isOAuthRequest(httpRequest)) {
         login(request, httpRequest, httpResponse, ghLogin);
       } else {
-        if (gerritCookie != null && !OAuthProtocol.isOAuthLogin(httpRequest)) {
-          if (authCookie != null) {
-            authCookie.setMaxAge(0);
-            authCookie.setValue("");
-            httpResponse.addCookie(authCookie);
-          }
-        } else if (authCookie != null) {
-          httpRequest =
-              new AuthenticatedHttpRequest(httpRequest, config.httpHeader,
-                  authCookie.user, config.httpDisplaynameHeader,
-                  authCookie.fullName, config.httpEmailHeader, authCookie.email);
-        }
+        httpRequest = enrichAuthenticatedRequest(httpRequest, authCookie);
 
         if (OAuthProtocol.isOAuthFinalForOthers(httpRequest)) {
-          httpResponse.sendRedirect(OAuthProtocol.getTargetOAuthFinal(httpRequest));
-          return;
+          httpResponse.sendRedirect(OAuthProtocol
+              .getTargetOAuthFinal(httpRequest));
         } else {
           chain.doFilter(httpRequest, response);
         }
@@ -130,16 +119,24 @@
     }
   }
 
-  private void login(ServletRequest request,
-      HttpServletRequest httpRequest, HttpServletResponse httpResponse,
-      GitHubLogin ghLogin) throws IOException {
-    if(ghLogin.login(httpRequest, httpResponse)) {
-      GHMyself myself =
-          ghLogin.getMyself();
+  private HttpServletRequest enrichAuthenticatedRequest(
+      HttpServletRequest httpRequest, OAuthCookie authCookie) {
+    httpRequest =
+        authCookie == null ? httpRequest : new AuthenticatedHttpRequest(
+            httpRequest, config.httpHeader, authCookie.user,
+            config.httpDisplaynameHeader, authCookie.fullName,
+            config.httpEmailHeader, authCookie.email);
+    return httpRequest;
+  }
+
+  private void login(ServletRequest request, HttpServletRequest httpRequest,
+      HttpServletResponse httpResponse, GitHubLogin ghLogin) throws IOException {
+    if (ghLogin.login(httpRequest, httpResponse)) {
+      GHMyself myself = ghLogin.getMyself();
       String user = myself.getLogin();
 
-      updateSecureConfigWithRetry(ghLogin.hub.getMyOrganizations().keySet(), user,
-          ghLogin.token.access_token);
+      updateSecureConfigWithRetry(ghLogin.hub.getMyOrganizations().keySet(),
+          user, ghLogin.token.access_token);
     }
   }
 
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 5fd96b5..d0d705f 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
@@ -9,6 +9,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
@@ -56,8 +57,7 @@
     }
   }
   private static final String ME_SEPARATOR = ",";
-
-  private static final Logger log = LoggerFactory
+  private static final Logger LOG = LoggerFactory
       .getLogger(OAuthProtocol.class);
 
   private final GitHubOAuthConfig config;
@@ -91,31 +91,24 @@
   }
 
   public void loginPhase1(HttpServletRequest request,
-      HttpServletResponse response, Scope... scopes) throws IOException {
-    String scopeRequested = request.getParameter("scope");
-    String baseScopeKey = Objects.firstNonNull(scopeRequested, "scopes");
+      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, getScope(baseScopeKey, scopes),
+        config.gitHubClientId, scopesString,
         getURLEncoded(config.oAuthFinalRedirectUrl),
         me(), getURLEncoded(request.getRequestURI().toString())));
   }
 
-  private String getScope(String baseScopeKey, Scope[] scopes) {
-    List<Scope> baseScopes = config.scopes.get(baseScopeKey);
-    if(baseScopes == null) {
-      throw new IllegalArgumentException("Requested OAuth base scope id " + baseScopeKey + " is not configured in gerrit.config");
-    }
-
-    HashSet<Scope> fullScopes = new HashSet<OAuthProtocol.Scope>(baseScopes);
-    fullScopes.addAll(Arrays.asList(scopes));
-    
-    if(fullScopes.size() <= 0) {
+  private String getScope(Set<Scope> scopes) {
+    if(scopes.size() <= 0) {
       return "";
     }
     
     StringBuilder out = new StringBuilder();
-    for (Scope scope : fullScopes) {
+    for (Scope scope : scopes) {
       if(out.length() > 0) {
         out.append(",");
       }
@@ -149,6 +142,10 @@
     return request.getRequestURI().indexOf(GitHubOAuthConfig.OAUTH_LOGOUT) >= 0;
   }
 
+  public static boolean isOAuthRequest(HttpServletRequest httpRequest) {
+    return OAuthProtocol.isOAuthLogin(httpRequest) || OAuthProtocol.isOAuthFinal(httpRequest);
+  }
+
   public AccessToken loginPhase2(HttpServletRequest request,
       HttpServletResponse response) throws IOException {
 
@@ -169,7 +166,7 @@
     try {
       HttpResponse postResponse = http.execute(post);
       if (postResponse.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) {
-        log.error("POST " + config.gitHubOAuthAccessTokenUrl
+        LOG.error("POST " + config.gitHubOAuthAccessTokenUrl
             + " request for access token failed with status "
             + postResponse.getStatusLine());
         response.sendError(HttpURLConnection.HTTP_UNAUTHORIZED,
@@ -183,7 +180,7 @@
               .getContent(), "UTF-8"), AccessToken.class);
       return token;
     } catch (IOException e) {
-      log.error("POST " + config.gitHubOAuthAccessTokenUrl
+      LOG.error("POST " + config.gitHubOAuthAccessTokenUrl
           + " request for access token failed", e);
       response.sendError(HttpURLConnection.HTTP_UNAUTHORIZED,
           "Request for access token not authorised");