Add Cognito OAuth provider This adds OAuth provider for AWS Cognito[1]. This was tested against a Cognito instance and authentication worked as expected. 1. https://aws.amazon.com/cognito/ Change-Id: Ieb1d014d5d2c40910641c8080a020ab980d997b4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoApi.java new file mode 100644 index 0000000..ca8da2d --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoApi.java
@@ -0,0 +1,37 @@ +// Copyright (C) 2024 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.oauth; + +import com.github.scribejava.core.builder.api.DefaultApi20; + +public class CognitoApi extends DefaultApi20 { + private static final String AUTHORIZE_URL = "%s/oauth2/authorize/"; + private static final String ACCESS_TOKEN_URL = "%s/oauth2/token/"; + private final String rootUrl; + + public CognitoApi(String rootUrl) { + this.rootUrl = rootUrl; + } + + @Override + public String getAccessTokenEndpoint() { + return String.format(ACCESS_TOKEN_URL, rootUrl); + } + + @Override + protected String getAuthorizationBaseUrl() { + return String.format(AUTHORIZE_URL, rootUrl); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java new file mode 100644 index 0000000..3daef77 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java
@@ -0,0 +1,144 @@ +// Copyright (C) 2024 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.oauth; + +import static com.google.gerrit.json.OutputFormat.JSON; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.google.common.base.CharMatcher; +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.server.config.CanonicalWebUrl; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CognitoOAuthService implements OAuthServiceProvider { + private static final Logger log = LoggerFactory.getLogger(GitHubOAuthService.class); + static final String CONFIG_SUFFIX = "-cognito-oauth"; + private static final String COGNITO_PROVIDER_PREFIX = "cognito-oauth:"; + private static final String PROTECTED_RESOURCE_URL = "%s/oauth2/userInfo"; + private final String rootUrl; + private final OAuth20Service service; + private final String serviceName; + + @Inject + CognitoOAuthService( + PluginConfigFactory cfgFactory, + @PluginName String pluginName, + @CanonicalWebUrl Provider<String> urlProvider) { + PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); + String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; + + rootUrl = cfg.getString(InitOAuth.ROOT_URL); + if (!URI.create(rootUrl).isAbsolute()) { + throw new ProvisionException("Root URL must be absolute URL"); + } + + serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Cognito"); + + service = + new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) + .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) + .callback(canonicalWebUrl + "oauth") + .defaultScope("openid profile email") + .build(new CognitoApi(rootUrl)); + } + + @Override + public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { + OAuthRequest request = + new OAuthRequest(Verb.GET, String.format(PROTECTED_RESOURCE_URL, rootUrl)); + OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); + service.signRequest(t, request); + + JsonElement userJson = null; + try (Response response = service.execute(request)) { + if (response.getCode() != HttpServletResponse.SC_OK) { + throw new IOException( + String.format( + "Status %s (%s) for request %s", + response.getCode(), response.getBody(), request.getUrl())); + } + userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); + if (log.isDebugEnabled()) { + log.debug("User info response: {}", response.getBody()); + } + + JsonObject jsonObject = userJson.getAsJsonObject(); + if (jsonObject == null || jsonObject.isJsonNull()) { + throw new IOException("Response doesn't contain 'user' field" + jsonObject); + } + JsonElement id = jsonObject.get("sub"); + JsonElement username = jsonObject.get("preferred_username"); + JsonElement email = jsonObject.get("email"); + JsonElement name = jsonObject.get("name"); + return new OAuthUserInfo( + COGNITO_PROVIDER_PREFIX + id.getAsString() /*externalId*/, + username == null || username.isJsonNull() ? null : username.getAsString() /*username*/, + email == null || email.isJsonNull() ? null : email.getAsString() /*email*/, + name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/, + null /*claimedIdentity*/); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Cannot retrieve user info resource", e); + } + } + + @Override + public OAuthToken getAccessToken(OAuthVerifier rv) { + try { + OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); + return new OAuthToken( + accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); + } catch (InterruptedException | ExecutionException | IOException e) { + String msg = "Cannot retrieve access token"; + log.error(msg, e); + throw new RuntimeException(msg, e); + } + } + + @Override + public String getAuthorizationUrl() { + return service.getAuthorizationUrl(); + } + + @Override + public String getVersion() { + return service.getVersion(); + } + + @Override + public String getName() { + return serviceName; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java index adfde3a..d64a26c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -158,5 +158,12 @@ .annotatedWith(Exports.named(AuthentikOAuthService.CONFIG_SUFFIX)) .to(AuthentikOAuthService.class); } + + cfg = cfgFactory.getFromGerritConfig(pluginName + CognitoOAuthService.CONFIG_SUFFIX); + if (cfg.getString(InitOAuth.CLIENT_ID) != null) { + bind(OAuthServiceProvider.class) + .annotatedWith(Exports.named(CognitoOAuthService.CONFIG_SUFFIX)) + .to(CognitoOAuthService.class); + } } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java index 2eac861..183f46e 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -58,6 +58,7 @@ private final Section tuleapOAuthProviderSection; private final Section auth0OAuthProviderSection; private final Section authentikOAuthProviderSection; + private final Section cognitoOAuthProviderSection; @Inject InitOAuth(ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) { @@ -94,6 +95,8 @@ sections.get(PLUGIN_SECTION, pluginName + Auth0OAuthService.CONFIG_SUFFIX); this.authentikOAuthProviderSection = sections.get(PLUGIN_SECTION, pluginName + AuthentikOAuthService.CONFIG_SUFFIX); + this.cognitoOAuthProviderSection = + sections.get(PLUGIN_SECTION, pluginName + CognitoOAuthService.CONFIG_SUFFIX); } @Override @@ -238,6 +241,14 @@ authentikOAuthProviderSection.string( "Link to existing gerrit accounts?", LINK_TO_EXISTING_GERRIT_ACCOUNT, "false"); } + + boolean configureCognitoOAuthProvider = + ui.yesno( + isConfigured(cognitoOAuthProviderSection), + "Use Cognito OAuth provider for Gerrit login ?"); + if (configureCognitoOAuthProvider && configureOAuth(cognitoOAuthProviderSection)) { + checkRootUrl(cognitoOAuthProviderSection.string("Cognito Root URL", ROOT_URL, null)); + } } /**
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index ee8f852..2ebd021 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -87,6 +87,11 @@ client-id = "<client-id>" client-secret = "<client-secret>" link-to-existing-gerrit-accounts = false + + [plugin "@PLUGIN@-cognito-oauth"] + root-url = "<root url>" # for example, https://cognito.example.com + client-id = "<client-id>" + client-secret = "<client-secret>" ``` When one from the sections above is omitted, OAuth SSO is used. @@ -326,6 +331,11 @@ You can optionally set `link-to-existing-gerrit-accounts = true` if you want the provider to link a account based on the username instead of trying to create a new account, see below migration from LDAP. +### Cognito + +The client-id and secret-id can be obtained in the AWS Cognito web interface once you create a new App Integration for Gerrit. +See [Creating an app integration](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-configuring-app-integration.html). + #### Migrating from LDAP Set the `link-to-existing-gerrit-accounts = true` option.