Merge "Expose extension point for generic OAuth providers" into stable-2.10
This commit is contained in:
commit
c842693e80
@ -145,6 +145,16 @@ request is the exact string supplied in the dialog by the user.
|
||||
The configured <<ldap.username,ldap.username>> identity is not used to obtain
|
||||
account information.
|
||||
+
|
||||
* OAUTH
|
||||
+
|
||||
OAuth is a protocol that lets external apps request authorization to private
|
||||
details in a user's 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. Note
|
||||
that provider specific plugins must be used with this authentication scheme.
|
||||
+
|
||||
* `DEVELOPMENT_BECOME_ANY_ACCOUNT`
|
||||
+
|
||||
*DO NOT USE*. Only for use in a development environment.
|
||||
|
@ -0,0 +1,74 @@
|
||||
// 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.extensions.auth.oauth;
|
||||
|
||||
import com.google.gerrit.extensions.annotations.ExtensionPoint;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/* Contract that OAuth provider must implement */
|
||||
@ExtensionPoint
|
||||
public interface OAuthServiceProvider {
|
||||
|
||||
/**
|
||||
* Retrieve the request token.
|
||||
*
|
||||
* @return request token
|
||||
*/
|
||||
OAuthToken getRequestToken();
|
||||
|
||||
/**
|
||||
* Returns the URL where you should redirect your users to authenticate
|
||||
* your application.
|
||||
*
|
||||
* @param requestToken the request token you need to authorize
|
||||
* @return the URL where you should redirect your users
|
||||
*/
|
||||
String getAuthorizationUrl(OAuthToken requestToken);
|
||||
|
||||
/**
|
||||
* Retrieve the access token
|
||||
*
|
||||
* @param requestToken request token (obtained previously)
|
||||
* @param verifier verifier code
|
||||
* @return access token
|
||||
*/
|
||||
OAuthToken getAccessToken(OAuthToken requestToken, OAuthVerifier verifier);
|
||||
|
||||
/**
|
||||
* After establishing of secure communication channel, this method supossed to
|
||||
* access the protected resoure and retrieve the username.
|
||||
*
|
||||
* @param token
|
||||
* @return OAuth user information
|
||||
* @throws IOException
|
||||
*/
|
||||
OAuthUserInfo getUserInfo(OAuthToken token) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the OAuth version of the service.
|
||||
*
|
||||
* @return oauth version as string
|
||||
*/
|
||||
String getVersion();
|
||||
|
||||
/**
|
||||
* Returns the name of this service. This name is resented the user to choose
|
||||
* between multiple service providers
|
||||
*
|
||||
* @return name of the service
|
||||
*/
|
||||
String getName();
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
// 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.extensions.auth.oauth;
|
||||
|
||||
/* OAuth token */
|
||||
public class OAuthToken {
|
||||
|
||||
private final String token;
|
||||
private final String secret;
|
||||
private final String raw;
|
||||
|
||||
public OAuthToken(String token, String secret, String raw) {
|
||||
this.token = token;
|
||||
this.secret = secret;
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
public String getRaw() {
|
||||
return raw;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// 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.extensions.auth.oauth;
|
||||
|
||||
public class OAuthUserInfo {
|
||||
|
||||
private final String externalId;
|
||||
private final String userName;
|
||||
private final String emailAddress;
|
||||
private final String displayName;
|
||||
|
||||
public OAuthUserInfo(String externalId,
|
||||
String userName,
|
||||
String emailAddress,
|
||||
String displayName) {
|
||||
this.externalId = externalId;
|
||||
this.userName = userName;
|
||||
this.emailAddress = emailAddress;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getExternalId() {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public String getEmailAddress() {
|
||||
return emailAddress;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// 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.extensions.auth.oauth;
|
||||
|
||||
/* OAuth verifier */
|
||||
public class OAuthVerifier {
|
||||
|
||||
private final String value;
|
||||
|
||||
public OAuthVerifier(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -718,6 +718,15 @@ public class Gerrit implements EntryPoint {
|
||||
});
|
||||
break;
|
||||
|
||||
case OAUTH:
|
||||
menuRight.addItem(C.menuSignIn(), new Command() {
|
||||
@Override
|
||||
public void execute() {
|
||||
doSignIn(History.getToken());
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case OPENID_SSO:
|
||||
menuRight.addItem(C.menuSignIn(), new Command() {
|
||||
public void execute() {
|
||||
|
@ -112,6 +112,7 @@ class GerritConfigProvider implements Provider<GerritConfig> {
|
||||
|
||||
case CLIENT_SSL_CERT_LDAP:
|
||||
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
|
||||
case OAUTH:
|
||||
case OPENID:
|
||||
case OPENID_SSO:
|
||||
break;
|
||||
|
@ -35,7 +35,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Singleton
|
||||
class HttpLogoutServlet extends HttpServlet {
|
||||
public class HttpLogoutServlet extends HttpServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DynamicItem<WebSession> webSession;
|
||||
@ -44,7 +44,7 @@ class HttpLogoutServlet extends HttpServlet {
|
||||
private final AuditService audit;
|
||||
|
||||
@Inject
|
||||
HttpLogoutServlet(final AuthConfig authConfig,
|
||||
protected HttpLogoutServlet(final AuthConfig authConfig,
|
||||
final DynamicItem<WebSession> webSession,
|
||||
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
|
||||
final AccountManager accountManager,
|
||||
@ -55,7 +55,7 @@ class HttpLogoutServlet extends HttpServlet {
|
||||
this.audit = audit;
|
||||
}
|
||||
|
||||
private void doLogout(final HttpServletRequest req,
|
||||
protected void doLogout(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) throws IOException {
|
||||
webSession.get().logout();
|
||||
if (logoutUrl != null) {
|
||||
|
@ -33,8 +33,10 @@ import com.google.gerrit.httpd.rpc.config.ConfigRestApiServlet;
|
||||
import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
|
||||
import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
|
||||
import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
|
||||
import com.google.gerrit.reviewdb.client.AuthType;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gwtexpui.server.CacheControlFilter;
|
||||
import com.google.inject.Inject;
|
||||
@ -64,10 +66,12 @@ class UrlModule extends ServletModule {
|
||||
|
||||
private final UrlConfig cfg;
|
||||
private GerritUiOptions uiOptions;
|
||||
private AuthConfig authConfig;
|
||||
|
||||
UrlModule(UrlConfig cfg, GerritUiOptions uiOptions) {
|
||||
UrlModule(UrlConfig cfg, GerritUiOptions uiOptions, AuthConfig authConfig) {
|
||||
this.cfg = cfg;
|
||||
this.uiOptions = uiOptions;
|
||||
this.authConfig = authConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -81,8 +85,11 @@ class UrlModule extends ServletModule {
|
||||
serve("/Gerrit/*").with(legacyGerritScreen());
|
||||
}
|
||||
serve("/cat/*").with(CatServlet.class);
|
||||
serve("/logout").with(HttpLogoutServlet.class);
|
||||
serve("/signout").with(HttpLogoutServlet.class);
|
||||
|
||||
if (authConfig.getAuthType() != AuthType.OAUTH) {
|
||||
serve("/logout").with(HttpLogoutServlet.class);
|
||||
serve("/signout").with(HttpLogoutServlet.class);
|
||||
}
|
||||
serve("/ssh_info").with(SshInfoServlet.class);
|
||||
serve("/static/*").with(StaticServlet.class);
|
||||
|
||||
|
@ -101,6 +101,8 @@ public class WebModule extends LifecycleModule {
|
||||
install(new BecomeAnyAccountModule());
|
||||
break;
|
||||
|
||||
case OAUTH:
|
||||
// OAuth support is bound in WebAppInitializer and Daemon.
|
||||
case OPENID:
|
||||
case OPENID_SSO:
|
||||
// OpenID support is bound in WebAppInitializer and Daemon.
|
||||
@ -110,7 +112,7 @@ public class WebModule extends LifecycleModule {
|
||||
throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
|
||||
}
|
||||
|
||||
install(new UrlModule(urlConfig, uiOptions));
|
||||
install(new UrlModule(urlConfig, uiOptions, authConfig));
|
||||
install(new UiRpcModule());
|
||||
install(new GerritRequestModule());
|
||||
install(new GitOverHttpServlet.Module());
|
||||
|
24
gerrit-oauth/BUCK
Normal file
24
gerrit-oauth/BUCK
Normal file
@ -0,0 +1,24 @@
|
||||
SRCS = glob(
|
||||
['src/main/java/**/*.java'],
|
||||
)
|
||||
RESOURCES = glob(['src/main/resources/**/*'])
|
||||
|
||||
java_library(
|
||||
name = 'oauth',
|
||||
srcs = SRCS,
|
||||
resources = RESOURCES,
|
||||
deps = [
|
||||
'//gerrit-common:annotations',
|
||||
'//gerrit-extension-api:api',
|
||||
'//gerrit-httpd:httpd',
|
||||
'//gerrit-server:server',
|
||||
'//lib:gson',
|
||||
'//lib:guava',
|
||||
'//lib/commons:codec',
|
||||
'//lib/guice:guice',
|
||||
'//lib/guice:guice-servlet',
|
||||
'//lib/log:api',
|
||||
],
|
||||
provided_deps = ['//lib:servlet-api-3_1'],
|
||||
visibility = ['PUBLIC'],
|
||||
)
|
@ -0,0 +1,57 @@
|
||||
// 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.oauth;
|
||||
|
||||
import com.google.gerrit.audit.AuditService;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.httpd.HttpLogoutServlet;
|
||||
import com.google.gerrit.httpd.WebSession;
|
||||
import com.google.gerrit.server.account.AccountManager;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Singleton
|
||||
class OAuthLogoutServlet extends HttpLogoutServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Provider<OAuthSession> oauthSession;
|
||||
|
||||
@Inject
|
||||
OAuthLogoutServlet(AuthConfig authConfig,
|
||||
DynamicItem<WebSession> webSession,
|
||||
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
||||
AccountManager accountManager,
|
||||
AuditService audit,
|
||||
Provider<OAuthSession> oauthSession) {
|
||||
super(authConfig, webSession, urlProvider, accountManager, audit);
|
||||
this.oauthSession = oauthSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLogout(HttpServletRequest req, HttpServletResponse rsp)
|
||||
throws IOException {
|
||||
super.doLogout(req, rsp);
|
||||
oauthSession.get().logout();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// 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.oauth;
|
||||
|
||||
import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.inject.servlet.ServletModule;
|
||||
|
||||
/** Servlets and support related to OAuth authentication. */
|
||||
public class OAuthModule extends ServletModule {
|
||||
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
filter("/login", "/login/*", "/oauth").through(OAuthWebFilter.class);
|
||||
// This is needed to invalidate OAuth session during logout
|
||||
serve("/logout").with(OAuthLogoutServlet.class);
|
||||
DynamicMap.mapOf(binder(), OAuthServiceProvider.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
// 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.oauth;
|
||||
|
||||
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
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.httpd.WebSession;
|
||||
import com.google.gerrit.server.account.AccountException;
|
||||
import com.google.gerrit.server.account.AccountManager;
|
||||
import com.google.gerrit.server.account.AuthResult;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.servlet.SessionScoped;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@SessionScoped
|
||||
/* OAuth protocol implementation */
|
||||
class OAuthSession {
|
||||
private static final Logger log = LoggerFactory.getLogger(OAuthSession.class);
|
||||
private static final SecureRandom randomState = newRandomGenerator();
|
||||
private final String state;
|
||||
private final DynamicItem<WebSession> webSession;
|
||||
private final AccountManager accountManager;
|
||||
private OAuthServiceProvider serviceProvider;
|
||||
private OAuthToken token;
|
||||
private OAuthUserInfo user;
|
||||
private String redirectUrl;
|
||||
|
||||
@Inject
|
||||
OAuthSession(DynamicItem<WebSession> webSession,
|
||||
AccountManager accountManager) {
|
||||
this.state = generateRandomState();
|
||||
this.webSession = webSession;
|
||||
this.accountManager = accountManager;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log.debug("Login " + this);
|
||||
|
||||
if (isOAuthFinal(request)) {
|
||||
if (!checkState(request)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug("Login-Retrieve-User " + this);
|
||||
token = oauth.getAccessToken(null,
|
||||
new OAuthVerifier(request.getParameter("code")));
|
||||
|
||||
user = oauth.getUserInfo(token);
|
||||
|
||||
if (isLoggedIn()) {
|
||||
log.debug("Login-SUCCESS " + this);
|
||||
authenticateAndRedirect(response);
|
||||
return true;
|
||||
} else {
|
||||
response.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log.debug("Login-PHASE1 " + this);
|
||||
redirectUrl = request.getRequestURI();
|
||||
response.sendRedirect(oauth.getAuthorizationUrl(null) +
|
||||
"&state=" + state);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void authenticateAndRedirect(HttpServletResponse rsp)
|
||||
throws IOException {
|
||||
com.google.gerrit.server.account.AuthRequest areq =
|
||||
new com.google.gerrit.server.account.AuthRequest(user.getExternalId());
|
||||
areq.setUserName(user.getUserName());
|
||||
areq.setEmailAddress(user.getEmailAddress());
|
||||
areq.setDisplayName(user.getDisplayName());
|
||||
AuthResult arsp;
|
||||
try {
|
||||
arsp = accountManager.authenticate(areq);
|
||||
} catch (AccountException e) {
|
||||
log.error("Unable to authenticate user \"" + user + "\"", e);
|
||||
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
webSession.get().login(arsp, true);
|
||||
String suffix = redirectUrl.substring(
|
||||
OAuthWebFilter.GERRIT_LOGIN.length() + 1);
|
||||
suffix = URLDecoder.decode(suffix, StandardCharsets.UTF_8.name());
|
||||
rsp.sendRedirect(suffix);
|
||||
}
|
||||
|
||||
void logout() {
|
||||
token = null;
|
||||
user = null;
|
||||
redirectUrl = null;
|
||||
serviceProvider = null;
|
||||
}
|
||||
|
||||
private boolean checkState(ServletRequest request) {
|
||||
String s = Strings.nullToEmpty(request.getParameter("state"));
|
||||
if (!s.equals(state)) {
|
||||
log.error("Illegal request state '" + s + "' on OAuthProtocol " + 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
// 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.oauth;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.httpd.HtmlDomUtil;
|
||||
import com.google.gerrit.httpd.LoginUrlToken;
|
||||
import com.google.gerrit.httpd.template.SiteHeaderFooter;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
@Singleton
|
||||
/* OAuth web filter uses active OAuth session to perform OAuth requests */
|
||||
class OAuthWebFilter implements Filter {
|
||||
static final String GERRIT_LOGIN = "/login";
|
||||
|
||||
private final Provider<String> urlProvider;
|
||||
private final Provider<CurrentUser> currentUserProvider;
|
||||
private final Provider<OAuthSession> oauthSessionProvider;
|
||||
private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
|
||||
private final SiteHeaderFooter header;
|
||||
private OAuthServiceProvider ssoProvider;
|
||||
|
||||
@Inject
|
||||
OAuthWebFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
||||
Provider<CurrentUser> currentUserProvider,
|
||||
DynamicMap<OAuthServiceProvider> oauthServiceProviders,
|
||||
Provider<OAuthSession> oauthSessionProvider,
|
||||
SiteHeaderFooter header) {
|
||||
this.urlProvider = urlProvider;
|
||||
this.currentUserProvider = currentUserProvider;
|
||||
this.oauthServiceProviders = oauthServiceProviders;
|
||||
this.oauthSessionProvider = oauthSessionProvider;
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
pickSSOServiceProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
HttpSession httpSession = ((HttpServletRequest) request).getSession(false);
|
||||
if (currentUserProvider.get().isIdentifiedUser()) {
|
||||
if (httpSession != null) {
|
||||
httpSession.invalidate();
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
|
||||
String provider = httpRequest.getParameter("provider");
|
||||
OAuthSession oauthSession = oauthSessionProvider.get();
|
||||
OAuthServiceProvider service = ssoProvider == null
|
||||
? oauthSession.getServiceProvider()
|
||||
: ssoProvider;
|
||||
|
||||
if ((isGerritLogin(httpRequest)
|
||||
|| oauthSession.isOAuthFinal(httpRequest))
|
||||
&& !oauthSession.isLoggedIn()) {
|
||||
if (service == null && Strings.isNullOrEmpty(provider)) {
|
||||
selectProvider(httpRequest, httpResponse, null);
|
||||
return;
|
||||
} else {
|
||||
if (service == null) {
|
||||
service = findService(provider);
|
||||
}
|
||||
oauthSession.setServiceProvider(service);
|
||||
oauthSession.login(httpRequest, httpResponse, service);
|
||||
}
|
||||
} else {
|
||||
chain.doFilter(httpRequest, response);
|
||||
}
|
||||
}
|
||||
|
||||
private OAuthServiceProvider findService(String providerId)
|
||||
throws ServletException {
|
||||
Set<String> plugins = oauthServiceProviders.plugins();
|
||||
for (String pluginName : plugins) {
|
||||
Map<String, Provider<OAuthServiceProvider>> m =
|
||||
oauthServiceProviders.byPlugin(pluginName);
|
||||
for (Map.Entry<String, Provider<OAuthServiceProvider>> e
|
||||
: m.entrySet()) {
|
||||
if (providerId.equals(
|
||||
String.format("%s_%s", pluginName, e.getKey()))) {
|
||||
return e.getValue().get();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ServletException("No provider found for: " + providerId);
|
||||
}
|
||||
|
||||
private void selectProvider(HttpServletRequest req, HttpServletResponse res,
|
||||
@Nullable String errorMessage)
|
||||
throws IOException {
|
||||
String self = req.getRequestURI();
|
||||
String cancel = Objects.firstNonNull(
|
||||
urlProvider != null ? urlProvider.get() : "/", "/");
|
||||
cancel += LoginUrlToken.getToken(req);
|
||||
|
||||
Document doc = header.parse(OAuthWebFilter.class, "LoginForm.html");
|
||||
HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
|
||||
HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
|
||||
HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
|
||||
|
||||
Element emsg = HtmlDomUtil.find(doc, "error_message");
|
||||
if (Strings.isNullOrEmpty(errorMessage)) {
|
||||
emsg.getParentNode().removeChild(emsg);
|
||||
} else {
|
||||
emsg.setTextContent(errorMessage);
|
||||
}
|
||||
|
||||
Element providers = HtmlDomUtil.find(doc, "providers");
|
||||
|
||||
Set<String> plugins = oauthServiceProviders.plugins();
|
||||
for (String pluginName : plugins) {
|
||||
Map<String, Provider<OAuthServiceProvider>> m =
|
||||
oauthServiceProviders.byPlugin(pluginName);
|
||||
for (Map.Entry<String, Provider<OAuthServiceProvider>> e
|
||||
: m.entrySet()) {
|
||||
addProvider(providers, pluginName, e.getKey(),
|
||||
e.getValue().get().getName());
|
||||
}
|
||||
}
|
||||
|
||||
sendHtml(res, doc);
|
||||
}
|
||||
|
||||
private static void addProvider(Element form, String pluginName,
|
||||
String id, String serviceName) {
|
||||
Element div = form.getOwnerDocument().createElement("div");
|
||||
div.setAttribute("id", id);
|
||||
Element hyperlink = form.getOwnerDocument().createElement("a");
|
||||
hyperlink.setAttribute("href", String.format("?provider=%s_%s",
|
||||
pluginName, id));
|
||||
hyperlink.setTextContent(serviceName +
|
||||
" (" + pluginName + " plugin)");
|
||||
div.appendChild(hyperlink);
|
||||
form.appendChild(div);
|
||||
}
|
||||
|
||||
private static void sendHtml(HttpServletResponse res, Document doc)
|
||||
throws IOException {
|
||||
byte[] bin = HtmlDomUtil.toUTF8(doc);
|
||||
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
res.setContentType("text/html");
|
||||
res.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
res.setContentLength(bin.length);
|
||||
try (ServletOutputStream out = res.getOutputStream()) {
|
||||
out.write(bin);
|
||||
}
|
||||
}
|
||||
|
||||
private void pickSSOServiceProvider()
|
||||
throws ServletException {
|
||||
SortedSet<String> plugins = oauthServiceProviders.plugins();
|
||||
if (plugins.isEmpty()) {
|
||||
throw new ServletException(
|
||||
"OAuth service provider wasn't installed");
|
||||
}
|
||||
if (plugins.size() == 1) {
|
||||
SortedMap<String, Provider<OAuthServiceProvider>> services =
|
||||
oauthServiceProviders.byPlugin(Iterables.getOnlyElement(plugins));
|
||||
if (services.size() == 1) {
|
||||
ssoProvider = Iterables.getOnlyElement(services.values()).get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isGerritLogin(HttpServletRequest request) {
|
||||
return request.getRequestURI().indexOf(GERRIT_LOGIN) >= 0;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -99,6 +99,7 @@ java_library(
|
||||
'//gerrit-gwtexpui:server',
|
||||
'//gerrit-httpd:httpd',
|
||||
'//gerrit-lucene:lucene',
|
||||
'//gerrit-oauth:oauth',
|
||||
'//gerrit-openid:openid',
|
||||
'//gerrit-reviewdb:server',
|
||||
'//gerrit-server:server',
|
||||
|
@ -27,6 +27,7 @@ import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
|
||||
import com.google.gerrit.httpd.RequestContextFilter;
|
||||
import com.google.gerrit.httpd.WebModule;
|
||||
import com.google.gerrit.httpd.WebSshGlueModule;
|
||||
import com.google.gerrit.httpd.auth.oauth.OAuthModule;
|
||||
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
|
||||
import com.google.gerrit.httpd.plugins.HttpPluginModule;
|
||||
import com.google.gerrit.lifecycle.LifecycleManager;
|
||||
@ -430,6 +431,8 @@ public class Daemon extends SiteProgram {
|
||||
if (authConfig.getAuthType() == AuthType.OPENID ||
|
||||
authConfig.getAuthType() == AuthType.OPENID_SSO) {
|
||||
modules.add(new OpenIdModule());
|
||||
} else if (authConfig.getAuthType() == AuthType.OAUTH) {
|
||||
modules.add(new OAuthModule());
|
||||
}
|
||||
modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
|
||||
|
||||
|
@ -60,6 +60,7 @@ class InitAuth implements InitStep {
|
||||
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
|
||||
case LDAP:
|
||||
case LDAP_BIND:
|
||||
case OAUTH:
|
||||
case OPENID:
|
||||
case OPENID_SSO:
|
||||
break;
|
||||
@ -94,6 +95,7 @@ class InitAuth implements InitStep {
|
||||
case CUSTOM_EXTENSION:
|
||||
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
|
||||
case HTTP:
|
||||
case OAUTH:
|
||||
case OPENID:
|
||||
case OPENID_SSO:
|
||||
break;
|
||||
|
@ -80,5 +80,8 @@ public enum AuthType {
|
||||
CUSTOM_EXTENSION,
|
||||
|
||||
/** Development mode to enable becoming anyone you want. */
|
||||
DEVELOPMENT_BECOME_ANY_ACCOUNT
|
||||
DEVELOPMENT_BECOME_ANY_ACCOUNT,
|
||||
|
||||
/** Generic OAuth provider over HTTP. */
|
||||
OAUTH
|
||||
}
|
||||
|
@ -40,7 +40,8 @@ public class DefaultRealm implements Realm {
|
||||
|
||||
@Override
|
||||
public boolean allowsEdit(final Account.FieldName field) {
|
||||
if (authConfig.getAuthType() == AuthType.HTTP) {
|
||||
if (authConfig.getAuthType() == AuthType.HTTP
|
||||
|| authConfig.getAuthType() == AuthType.OAUTH) {
|
||||
switch (field) {
|
||||
case USER_NAME:
|
||||
return false;
|
||||
|
@ -197,7 +197,7 @@ public class AuthConfig {
|
||||
case LDAP_BIND:
|
||||
case CLIENT_SSL_CERT_LDAP:
|
||||
case CUSTOM_EXTENSION:
|
||||
// Its safe to assume yes for an HTTP authentication type, as the
|
||||
case OAUTH:
|
||||
// only way in is through some external system that the admin trusts
|
||||
//
|
||||
return true;
|
||||
|
@ -8,6 +8,7 @@ java_library(
|
||||
'//gerrit-extension-api:api',
|
||||
'//gerrit-httpd:httpd',
|
||||
'//gerrit-lucene:lucene',
|
||||
'//gerrit-oauth:oauth',
|
||||
'//gerrit-openid:openid',
|
||||
'//gerrit-pgm:init-api',
|
||||
'//gerrit-pgm:init-base',
|
||||
|
@ -19,6 +19,7 @@ import static com.google.inject.Stage.PRODUCTION;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.gerrit.common.ChangeHookRunner;
|
||||
import com.google.gerrit.httpd.auth.oauth.OAuthModule;
|
||||
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
|
||||
import com.google.gerrit.httpd.plugins.HttpPluginModule;
|
||||
import com.google.gerrit.lifecycle.LifecycleManager;
|
||||
@ -343,6 +344,8 @@ public class WebAppInitializer extends GuiceServletContextListener
|
||||
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
|
||||
if (authConfig.getAuthType() == AuthType.OPENID) {
|
||||
modules.add(new OpenIdModule());
|
||||
} else if (authConfig.getAuthType() == AuthType.OAUTH) {
|
||||
modules.add(new OAuthModule());
|
||||
}
|
||||
|
||||
return sysInjector.createChildInjector(modules);
|
||||
|
Loading…
Reference in New Issue
Block a user