summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java')
-rw-r--r--java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java256
1 files changed, 256 insertions, 0 deletions
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
new file mode 100644
index 0000000000..a51a0abedc
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -0,0 +1,256 @@
+// 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.google.gerrit.httpd.auth.openid;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
+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.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.httpd.CanonicalWebUrl;
+import com.google.gerrit.httpd.LoginUrlToken;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.servlet.SessionScoped;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Optional;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.codec.binary.Base64;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** OAuth protocol implementation */
+@SessionScoped
+class OAuthSessionOverOpenID {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ static final String GERRIT_LOGIN = "/login";
+ private static final SecureRandom randomState = newRandomGenerator();
+ private final String state;
+ private final DynamicItem<WebSession> webSession;
+ private final Provider<IdentifiedUser> identifiedUser;
+ private final AccountManager accountManager;
+ private final CanonicalWebUrl urlProvider;
+ private OAuthServiceProvider serviceProvider;
+ private OAuthToken token;
+ private OAuthUserInfo user;
+ private String redirectToken;
+ private boolean linkMode;
+
+ @Inject
+ OAuthSessionOverOpenID(
+ DynamicItem<WebSession> webSession,
+ Provider<IdentifiedUser> identifiedUser,
+ AccountManager accountManager,
+ CanonicalWebUrl urlProvider) {
+ this.state = generateRandomState();
+ this.webSession = webSession;
+ this.identifiedUser = identifiedUser;
+ this.accountManager = accountManager;
+ this.urlProvider = urlProvider;
+ }
+
+ boolean isLoggedIn() {
+ return token != null && user != null;
+ }
+
+ boolean isOAuthFinal(HttpServletRequest request) {
+ return Strings.emptyToNull(request.getParameter("code")) != null;
+ }
+
+ boolean login(
+ HttpServletRequest request, HttpServletResponse response, OAuthServiceProvider oauth)
+ throws IOException {
+ logger.atFine().log("Login %s", this);
+
+ if (isOAuthFinal(request)) {
+ if (!checkState(request)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return false;
+ }
+
+ logger.atFine().log("Login-Retrieve-User %s", this);
+ token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
+ user = oauth.getUserInfo(token);
+
+ if (isLoggedIn()) {
+ logger.atFine().log("Login-SUCCESS %s", this);
+ authenticateAndRedirect(request, response);
+ return true;
+ }
+ response.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+ logger.atFine().log("Login-PHASE1 %s", this);
+ redirectToken = LoginUrlToken.getToken(request);
+ response.sendRedirect(oauth.getAuthorizationUrl() + "&state=" + state);
+ return false;
+ }
+
+ private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp)
+ throws IOException {
+ com.google.gerrit.server.account.AuthRequest areq =
+ new com.google.gerrit.server.account.AuthRequest(
+ ExternalId.Key.parse(user.getExternalId()));
+ AuthResult arsp;
+ try {
+ String claimedIdentifier = user.getClaimedIdentity();
+ Optional<Account.Id> actualId = accountManager.lookup(user.getExternalId());
+ Optional<Account.Id> claimedId = Optional.empty();
+
+ // We try to retrieve claimed identity.
+ // For some reason, for example staging instance
+ // it may deviate from the really old OpenID identity.
+ // What we want to avoid in any event is to create new
+ // account instead of linking to the existing one.
+ // That why we query it here, not to lose linking mode.
+ if (!Strings.isNullOrEmpty(claimedIdentifier)) {
+ claimedId = accountManager.lookup(claimedIdentifier);
+ if (!claimedId.isPresent()) {
+ logger.atFine().log("Claimed identity is unknown");
+ }
+ }
+
+ // Use case 1: claimed identity was provided during handshake phase
+ // and user account exists for this identity
+ if (claimedId.isPresent()) {
+ logger.atFine().log("Claimed identity is set and is known");
+ if (actualId.isPresent()) {
+ if (claimedId.get().equals(actualId.get())) {
+ // Both link to the same account, that's what we expected.
+ logger.atFine().log("Both link to the same account. All is fine.");
+ } else {
+ // This is (for now) a fatal error. There are two records
+ // for what might be the same user. The admin would have to
+ // link the accounts manually.
+ logger.atFine().log(
+ "OAuth accounts disagree over user identity:\n"
+ + " Claimed ID: %s is %s\n"
+ + " Delgate ID: %s is %s",
+ claimedId.get(), claimedIdentifier, actualId.get(), user.getExternalId());
+ rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ } else {
+ // Claimed account already exists: link to it.
+ logger.atFine().log("Claimed account already exists: link to it.");
+ try {
+ accountManager.link(claimedId.get(), areq);
+ } catch (OrmException | ConfigInvalidException e) {
+ logger.atSevere().log(
+ "Cannot link: %s to user identity:\n Claimed ID: %s is %s",
+ user.getExternalId(), claimedId.get(), claimedIdentifier);
+ rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ }
+ } else if (linkMode) {
+ // Use case 2: link mode activated from the UI
+ Account.Id accountId = identifiedUser.get().getAccountId();
+ try {
+ logger.atFine().log("Linking \"%s\" to \"%s\"", user.getExternalId(), accountId);
+ accountManager.link(accountId, areq);
+ } catch (OrmException | ConfigInvalidException e) {
+ logger.atSevere().log(
+ "Cannot link: %s to user identity: %s", user.getExternalId(), accountId);
+ rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ } finally {
+ linkMode = false;
+ }
+ }
+ areq.setUserName(user.getUserName());
+ areq.setEmailAddress(user.getEmailAddress());
+ areq.setDisplayName(user.getDisplayName());
+ arsp = accountManager.authenticate(areq);
+ } catch (AccountException e) {
+ logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user);
+ rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ webSession.get().login(arsp, true);
+ StringBuilder rdr = new StringBuilder(urlProvider.get(req));
+ rdr.append(Url.decode(redirectToken));
+ rsp.sendRedirect(rdr.toString());
+ }
+
+ void logout() {
+ token = null;
+ user = null;
+ redirectToken = null;
+ serviceProvider = null;
+ }
+
+ private boolean checkState(ServletRequest request) {
+ String s = Strings.nullToEmpty(request.getParameter("state"));
+ if (!s.equals(state)) {
+ logger.atSevere().log("Illegal request state '%s' on OAuthProtocol %s", s, this);
+ return false;
+ }
+ return true;
+ }
+
+ private static SecureRandom newRandomGenerator() {
+ try {
+ return SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("No SecureRandom available for GitHub authentication", e);
+ }
+ }
+
+ private static String generateRandomState() {
+ byte[] state = new byte[32];
+ randomState.nextBytes(state);
+ return Base64.encodeBase64URLSafeString(state);
+ }
+
+ @Override
+ public String toString() {
+ return "OAuthSession [token=" + token + ", user=" + user + "]";
+ }
+
+ public void setServiceProvider(OAuthServiceProvider provider) {
+ this.serviceProvider = provider;
+ }
+
+ public OAuthServiceProvider getServiceProvider() {
+ return serviceProvider;
+ }
+
+ public void setLinkMode(boolean linkMode) {
+ this.linkMode = linkMode;
+ }
+
+ public boolean isLinkMode() {
+ return linkMode;
+ }
+}