summaryrefslogtreecommitdiffstats
path: root/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java')
-rw-r--r--gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java554
1 files changed, 554 insertions, 0 deletions
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
new file mode 100644
index 0000000000..02ef7657e2
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -0,0 +1,554 @@
+// Copyright (C) 2009 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 com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.auth.SignInMode;
+import com.google.gerrit.common.auth.openid.DiscoveryResult;
+import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
+import com.google.gerrit.common.auth.openid.OpenIdService;
+import com.google.gerrit.common.auth.openid.OpenIdUrls;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.UrlEncoded;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.openid4java.consumer.ConsumerException;
+import org.openid4java.consumer.ConsumerManager;
+import org.openid4java.consumer.VerificationResult;
+import org.openid4java.discovery.DiscoveryException;
+import org.openid4java.discovery.DiscoveryInformation;
+import org.openid4java.message.AuthRequest;
+import org.openid4java.message.Message;
+import org.openid4java.message.MessageException;
+import org.openid4java.message.MessageExtension;
+import org.openid4java.message.ParameterList;
+import org.openid4java.message.ax.AxMessage;
+import org.openid4java.message.ax.FetchRequest;
+import org.openid4java.message.ax.FetchResponse;
+import org.openid4java.message.pape.PapeMessage;
+import org.openid4java.message.pape.PapeRequest;
+import org.openid4java.message.pape.PapeResponse;
+import org.openid4java.message.sreg.SRegMessage;
+import org.openid4java.message.sreg.SRegRequest;
+import org.openid4java.message.sreg.SRegResponse;
+import org.openid4java.util.HttpClientFactory;
+import org.openid4java.util.ProxyProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class OpenIdServiceImpl implements OpenIdService {
+ private static final Logger log =
+ LoggerFactory.getLogger(OpenIdServiceImpl.class);
+
+ static final String RETURN_URL = "OpenID";
+
+ private static final String P_MODE = "gerrit.mode";
+ private static final String P_TOKEN = "gerrit.token";
+ private static final String P_REMEMBER = "gerrit.remember";
+ private static final String P_CLAIMED = "gerrit.claimed";
+ private static final int LASTID_AGE = 365 * 24 * 60 * 60; // seconds
+
+ private static final String OPENID_MODE = "openid.mode";
+ private static final String OMODE_CANCEL = "cancel";
+
+ private static final String SCHEMA_EMAIL =
+ "http://schema.openid.net/contact/email";
+ private static final String SCHEMA_FIRSTNAME =
+ "http://schema.openid.net/namePerson/first";
+ private static final String SCHEMA_LASTNAME =
+ "http://schema.openid.net/namePerson/last";
+
+ private final Provider<WebSession> webSession;
+ private final Provider<IdentifiedUser> identifiedUser;
+ private final Provider<String> urlProvider;
+ private final AccountManager accountManager;
+ private final ConsumerManager manager;
+ private final List<OpenIdProviderPattern> allowedOpenIDs;
+
+ /** Maximum age, in seconds, before forcing re-authentication of account. */
+ private final int papeMaxAuthAge;
+
+ @Inject
+ OpenIdServiceImpl(final Provider<WebSession> cf,
+ final Provider<IdentifiedUser> iu,
+ @CanonicalWebUrl @Nullable final Provider<String> up,
+ @GerritServerConfig final Config config, final AuthConfig ac,
+ final AccountManager am) throws ConsumerException, MalformedURLException {
+
+ if (config.getString("http", null, "proxy") != null) {
+ final URL proxyUrl = new URL(config.getString("http", null, "proxy"));
+ String username = config.getString("http", null, "proxyUsername");
+ String password = config.getString("http", null, "proxyPassword");
+
+ final String userInfo = proxyUrl.getUserInfo();
+ if (userInfo != null) {
+ int c = userInfo.indexOf(':');
+ if (0 < c) {
+ username = userInfo.substring(0, c);
+ password = userInfo.substring(c + 1);
+ } else {
+ username = userInfo;
+ }
+ }
+
+ final ProxyProperties proxy = new ProxyProperties();
+ proxy.setProxyHostName(proxyUrl.getHost());
+ proxy.setProxyPort(proxyUrl.getPort());
+ proxy.setUserName(username);
+ proxy.setPassword(password);
+ HttpClientFactory.setProxyProperties(proxy);
+ }
+
+ webSession = cf;
+ identifiedUser = iu;
+ urlProvider = up;
+ accountManager = am;
+ manager = new ConsumerManager();
+ allowedOpenIDs = ac.getAllowedOpenIDs();
+ papeMaxAuthAge = (int) ConfigUtil.getTimeUnit(config, //
+ "auth", null, "maxOpenIdSessionAge", -1, TimeUnit.SECONDS);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void discover(final String openidIdentifier, final SignInMode mode,
+ final boolean remember, final String returnToken,
+ final AsyncCallback<DiscoveryResult> cb) {
+ if (!isAllowedOpenID(openidIdentifier)) {
+ cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NOT_ALLOWED));
+ return;
+ }
+
+ final State state;
+ state = init(openidIdentifier, mode, remember, returnToken);
+ if (state == null) {
+ cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER));
+ return;
+ }
+
+ final AuthRequest aReq;
+ try {
+ aReq = manager.authenticate(state.discovered, state.retTo.toString());
+ aReq.setRealm(state.contextUrl);
+
+ if (requestRegistration(aReq)) {
+ final SRegRequest sregReq = SRegRequest.createFetchRequest();
+ sregReq.addAttribute("fullname", true);
+ sregReq.addAttribute("email", true);
+ aReq.addExtension(sregReq);
+
+ final FetchRequest fetch = FetchRequest.createFetchRequest();
+ fetch.addAttribute("FirstName", SCHEMA_FIRSTNAME, true);
+ fetch.addAttribute("LastName", SCHEMA_LASTNAME, true);
+ fetch.addAttribute("Email", SCHEMA_EMAIL, true);
+ aReq.addExtension(fetch);
+ }
+
+ if (0 <= papeMaxAuthAge) {
+ final PapeRequest pape = PapeRequest.createPapeRequest();
+ pape.setMaxAuthAge(papeMaxAuthAge);
+ aReq.addExtension(pape);
+ }
+ } catch (MessageException e) {
+ log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
+ cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
+ return;
+ } catch (ConsumerException e) {
+ log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
+ cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
+ return;
+ }
+
+ cb.onSuccess(new DiscoveryResult(aReq.getDestinationUrl(false), //
+ aReq.getParameterMap()));
+ }
+
+ private boolean requestRegistration(final AuthRequest aReq) {
+ if (AuthRequest.SELECT_ID.equals(aReq.getIdentity())) {
+ // We don't know anything about the identity, as the provider
+ // will offer the user a way to indicate their identity. Skip
+ // any database query operation and assume we must ask for the
+ // registration information, in case the identity is new to us.
+ //
+ return true;
+
+ }
+
+ // We might already have this account on file. Look for it.
+ //
+ try {
+ return accountManager.lookup(aReq.getIdentity()) == null;
+ } catch (AccountException e) {
+ log.warn("Cannot determine if user account exists", e);
+ return true;
+ }
+ }
+
+ /** Called by {@link OpenIdLoginServlet} doGet, doPost */
+ void doAuth(final HttpServletRequest req, final HttpServletResponse rsp)
+ throws Exception {
+ if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) {
+ cancel(req, rsp);
+ return;
+ }
+
+ // Process the authentication response.
+ //
+ final SignInMode mode = signInMode(req);
+ final String openidIdentifier = req.getParameter("openid.identity");
+ final String claimedIdentifier = req.getParameter(P_CLAIMED);
+ final String returnToken = req.getParameter(P_TOKEN);
+ final boolean remember = "1".equals(req.getParameter(P_REMEMBER));
+ final String rediscoverIdentifier =
+ claimedIdentifier != null ? claimedIdentifier : openidIdentifier;
+ final State state;
+
+ if (!isAllowedOpenID(rediscoverIdentifier)
+ || !isAllowedOpenID(openidIdentifier)
+ || (claimedIdentifier != null && !isAllowedOpenID(claimedIdentifier))) {
+ cancelWithError(req, rsp, "Provider not allowed");
+ return;
+ }
+
+ state = init(rediscoverIdentifier, mode, remember, returnToken);
+ if (state == null) {
+ // Re-discovery must have failed, we can't run a login.
+ //
+ cancel(req, rsp);
+ return;
+ }
+
+ final String returnTo = req.getParameter("openid.return_to");
+ if (returnTo != null && returnTo.contains("openid.rpnonce=")) {
+ // Some providers (claimid.com) seem to embed these request
+ // parameters into our return_to URL, and then give us them
+ // in the return_to request parameter. But not all.
+ //
+ state.retTo.put("openid.rpnonce", req.getParameter("openid.rpnonce"));
+ state.retTo.put("openid.rpsig", req.getParameter("openid.rpsig"));
+ }
+
+ final VerificationResult result =
+ manager.verify(state.retTo.toString(), new ParameterList(req
+ .getParameterMap()), state.discovered);
+ if (result.getVerifiedId() == null /* authentication failure */) {
+ if ("Nonce verification failed.".equals(result.getStatusMsg())) {
+ // We might be suffering from clock skew on this system.
+ //
+ log.error("OpenID failure: " + result.getStatusMsg()
+ + " Likely caused by clock skew on this server,"
+ + " install/configure NTP.");
+ cancelWithError(req, rsp, result.getStatusMsg());
+
+ } else if (result.getStatusMsg() != null) {
+ // Authentication failed.
+ //
+ log.error("OpenID failure: " + result.getStatusMsg());
+ cancelWithError(req, rsp, result.getStatusMsg());
+
+ } else {
+ // Assume authentication was canceled.
+ //
+ cancel(req, rsp);
+ }
+ return;
+ }
+
+ final Message authRsp = result.getAuthResponse();
+ SRegResponse sregRsp = null;
+ FetchResponse fetchRsp = null;
+
+ if (0 <= papeMaxAuthAge) {
+ PapeResponse ext;
+ boolean unsupported = false;
+
+ try {
+ ext = (PapeResponse) authRsp.getExtension(PapeMessage.OPENID_NS_PAPE);
+ } catch (MessageException err) {
+ // Far too many providers are unable to provide PAPE extensions
+ // right now. Instead of blocking all of them log the error and
+ // let the authentication complete anyway.
+ //
+ log.error("Invalid PAPE response " + openidIdentifier + ": " + err);
+ unsupported = true;
+ ext = null;
+ }
+ if (!unsupported && ext == null) {
+ log.error("No PAPE extension response from " + openidIdentifier);
+ cancelWithError(req, rsp, "OpenID provider does not support PAPE.");
+ return;
+ }
+ }
+
+ if (authRsp.hasExtension(SRegMessage.OPENID_NS_SREG)) {
+ final MessageExtension ext =
+ authRsp.getExtension(SRegMessage.OPENID_NS_SREG);
+ if (ext instanceof SRegResponse) {
+ sregRsp = (SRegResponse) ext;
+ }
+ }
+
+ if (authRsp.hasExtension(AxMessage.OPENID_NS_AX)) {
+ final MessageExtension ext = authRsp.getExtension(AxMessage.OPENID_NS_AX);
+ if (ext instanceof FetchResponse) {
+ fetchRsp = (FetchResponse) ext;
+ }
+ }
+
+ final com.google.gerrit.server.account.AuthRequest areq =
+ new com.google.gerrit.server.account.AuthRequest(openidIdentifier);
+
+ if (sregRsp != null) {
+ areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
+ areq.setEmailAddress(sregRsp.getAttributeValue("email"));
+
+ } else if (fetchRsp != null) {
+ final String firstName = fetchRsp.getAttributeValue("FirstName");
+ final String lastName = fetchRsp.getAttributeValue("LastName");
+ final StringBuilder n = new StringBuilder();
+ if (firstName != null && firstName.length() > 0) {
+ n.append(firstName);
+ }
+ if (lastName != null && lastName.length() > 0) {
+ if (n.length() > 0) {
+ n.append(' ');
+ }
+ n.append(lastName);
+ }
+ areq.setDisplayName(n.length() > 0 ? n.toString() : null);
+ areq.setEmailAddress(fetchRsp.getAttributeValue("Email"));
+ }
+
+ if (claimedIdentifier != null) {
+ // The user used a claimed identity which has delegated to the verified
+ // identity we have in our AuthRequest above. We still should have a
+ // link between the two, so set one up if not present.
+ //
+ Account.Id claimedId = accountManager.lookup(claimedIdentifier);
+ Account.Id actualId = accountManager.lookup(areq.getExternalId());
+
+ if (claimedId != null && actualId != null) {
+ if (claimedId.equals(actualId)) {
+ // Both link to the same account, that's what we expected.
+ } else {
+ // This is (for now) a fatal error. There are two records
+ // for what might be the same user.
+ //
+ log.error("OpenID accounts disagree over user identity:\n"
+ + " Claimed ID: " + claimedId + " is " + claimedIdentifier
+ + "\n" + " Delgate ID: " + actualId + " is "
+ + areq.getExternalId());
+ cancelWithError(req, rsp, "Contact site administrator");
+ return;
+ }
+
+ } else if (claimedId == null && actualId != null) {
+ // Older account, the actual was already created but the claimed
+ // was missing due to a bug in Gerrit. Link the claimed.
+ //
+ final com.google.gerrit.server.account.AuthRequest linkReq =
+ new com.google.gerrit.server.account.AuthRequest(claimedIdentifier);
+ linkReq.setDisplayName(areq.getDisplayName());
+ linkReq.setEmailAddress(areq.getEmailAddress());
+ accountManager.link(actualId, linkReq);
+
+ } else if (claimedId != null && actualId == null) {
+ // Claimed account already exists, but it smells like the user has
+ // changed their delegate to point to a different provider. Link
+ // the new provider.
+ //
+ accountManager.link(claimedId, areq);
+
+ } else {
+ // Both are null, we are going to create a new account below.
+ }
+ }
+
+ try {
+ final com.google.gerrit.server.account.AuthResult arsp;
+ switch (mode) {
+ case REGISTER:
+ case SIGN_IN:
+ arsp = accountManager.authenticate(areq);
+
+ final Cookie lastId = new Cookie(OpenIdUrls.LASTID_COOKIE, "");
+ lastId.setPath(req.getContextPath() + "/");
+ if (remember) {
+ lastId.setValue(rediscoverIdentifier);
+ lastId.setMaxAge(LASTID_AGE);
+ } else {
+ lastId.setMaxAge(0);
+ }
+ rsp.addCookie(lastId);
+ webSession.get().login(arsp, remember);
+ if (arsp.isNew() && claimedIdentifier != null) {
+ final com.google.gerrit.server.account.AuthRequest linkReq =
+ new com.google.gerrit.server.account.AuthRequest(
+ claimedIdentifier);
+ linkReq.setDisplayName(areq.getDisplayName());
+ linkReq.setEmailAddress(areq.getEmailAddress());
+ accountManager.link(arsp.getAccountId(), linkReq);
+ }
+ callback(arsp.isNew(), req, rsp);
+ break;
+
+ case LINK_IDENTIY: {
+ arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
+ webSession.get().login(arsp, remember);
+ callback(false, req, rsp);
+ break;
+ }
+ }
+ } catch (AccountException e) {
+ log.error("OpenID authentication failure", e);
+ cancelWithError(req, rsp, "Contact site administrator");
+ }
+ }
+
+ private boolean isSignIn(final SignInMode mode) {
+ switch (mode) {
+ case SIGN_IN:
+ case REGISTER:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static SignInMode signInMode(final HttpServletRequest req) {
+ try {
+ return SignInMode.valueOf(req.getParameter(P_MODE));
+ } catch (RuntimeException e) {
+ return SignInMode.SIGN_IN;
+ }
+ }
+
+ private void callback(final boolean isNew, final HttpServletRequest req,
+ final HttpServletResponse rsp) throws IOException {
+ String token = req.getParameter(P_TOKEN);
+ if (token == null || token.isEmpty() || token.startsWith("/SignInFailure,")) {
+ token = PageLinks.MINE;
+ }
+
+ final StringBuilder rdr = new StringBuilder();
+ rdr.append(urlProvider.get());
+ rdr.append('#');
+ if (isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
+ rdr.append(PageLinks.REGISTER);
+ }
+ rdr.append(token);
+ rsp.sendRedirect(rdr.toString());
+ }
+
+ private void cancel(final HttpServletRequest req,
+ final HttpServletResponse rsp) throws IOException {
+ if (isSignIn(signInMode(req))) {
+ webSession.get().logout();
+ }
+ callback(false, req, rsp);
+ }
+
+ private void cancelWithError(final HttpServletRequest req,
+ final HttpServletResponse rsp, final String errorDetail)
+ throws IOException {
+ final SignInMode mode = signInMode(req);
+ if (isSignIn(mode)) {
+ webSession.get().logout();
+ }
+ final StringBuilder rdr = new StringBuilder();
+ rdr.append(urlProvider.get());
+ rdr.append('#');
+ rdr.append("SignInFailure");
+ rdr.append(',');
+ rdr.append(mode.name());
+ rdr.append(',');
+ rdr.append(errorDetail != null ? KeyUtil.encode(errorDetail) : "");
+ rsp.sendRedirect(rdr.toString());
+ }
+
+ private State init(final String openidIdentifier, final SignInMode mode,
+ final boolean remember, final String returnToken) {
+ final List<?> list;
+ try {
+ list = manager.discover(openidIdentifier);
+ } catch (DiscoveryException e) {
+ log.error("Cannot discover OpenID " + openidIdentifier, e);
+ return null;
+ }
+ if (list == null || list.isEmpty()) {
+ return null;
+ }
+
+ final String contextUrl = urlProvider.get();
+ final DiscoveryInformation discovered = manager.associate(list);
+ final UrlEncoded retTo = new UrlEncoded(contextUrl + RETURN_URL);
+ retTo.put(P_MODE, mode.name());
+ if (returnToken != null && returnToken.length() > 0) {
+ retTo.put(P_TOKEN, returnToken);
+ }
+ if (remember) {
+ retTo.put(P_REMEMBER, "1");
+ }
+ if (discovered.hasClaimedIdentifier()) {
+ retTo.put(P_CLAIMED, discovered.getClaimedIdentifier().getIdentifier());
+ }
+ return new State(discovered, retTo, contextUrl);
+ }
+
+ private boolean isAllowedOpenID(final String id) {
+ for (final OpenIdProviderPattern pattern : allowedOpenIDs) {
+ if (pattern.matches(id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class State {
+ final DiscoveryInformation discovered;
+ final UrlEncoded retTo;
+ final String contextUrl;
+
+ State(final DiscoveryInformation d, final UrlEncoded r, final String c) {
+ discovered = d;
+ retTo = r;
+ contextUrl = c;
+ }
+ }
+}