Add support for Auth0 OAuth2/OIDC provider https://auth0.com/docs/authenticate/protocols/oauth Change-Id: I1549abda1e94fba82b2d81713ae37b8cdf264643
diff --git a/README.md b/README.md index 606b565..df77b90 100644 --- a/README.md +++ b/README.md
@@ -20,6 +20,7 @@ * [Azure (previously named Office365)](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols) * [Phabricator](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/) * [Tuleap](https://docs.tuleap.org/user-guide/oauth2.html) +* [Auth0](https://auth0.com/docs/authenticate/protocols/oauth) See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Auth0Api.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Auth0Api.java new file mode 100644 index 0000000..a6888b5 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Auth0Api.java
@@ -0,0 +1,38 @@ +// Copyright (C) 2023 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 Auth0Api extends DefaultApi20 { + private static final String AUTHORIZE_URL = "%s/authorize"; + private static final String ACCESS_TOKEN_URL = "%s/oauth/token"; + + private final String rootUrl; + + public Auth0Api(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/Auth0OAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Auth0OAuthService.java new file mode 100644 index 0000000..065120f --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Auth0OAuthService.java
@@ -0,0 +1,141 @@ +// Copyright (C) 2023 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 Auth0OAuthService implements OAuthServiceProvider { + private static final Logger log = LoggerFactory.getLogger(Auth0OAuthService.class); + private static final String AUTH0_PROVIDER_PREFIX = "auth0-oauth:"; + static final String CONFIG_SUFFIX = "-auth0-oauth"; + private static final String PROTECTED_RESOURCE_URL = "%s/userinfo"; + private final OAuth20Service service; + private final String serviceName; + private final String rootUrl; + + @Inject + Auth0OAuthService( + 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, "Auth0"); + + service = + new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) + .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) + .callback(canonicalWebUrl + "oauth") + .defaultScope("openid profile email") + .build(new Auth0Api(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); + + 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())); + } + JsonElement 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( + AUTH0_PROVIDER_PREFIX + id.getAsString(), + username == null || username.isJsonNull() ? null : username.getAsString(), + email == null || email.isJsonNull() ? null : email.getAsString(), + name == null || name.isJsonNull() ? null : name.getAsString(), + id.getAsString()); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Cannot retrieve user info resource", e); + } + } + + @Override + public OAuthToken getAccessToken(OAuthVerifier verifier) { + try { + OAuth2AccessToken accessToken = service.getAccessToken(verifier.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 45dea40..5b6a68a 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -144,5 +144,12 @@ .annotatedWith(Exports.named(TuleapOAuthService.CONFIG_SUFFIX)) .to(TuleapOAuthService.class); } + + cfg = cfgFactory.getFromGerritConfig(pluginName + Auth0OAuthService.CONFIG_SUFFIX); + if (cfg.getString(InitOAuth.CLIENT_ID) != null) { + bind(OAuthServiceProvider.class) + .annotatedWith(Exports.named(Auth0OAuthService.CONFIG_SUFFIX)) + .to(Auth0OAuthService.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 16e410f..101c1f1 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -55,6 +55,7 @@ private final Section airVantageOAuthProviderSection; private final Section phabricatorOAuthProviderSection; private final Section tuleapOAuthProviderSection; + private final Section auth0OAuthProviderSection; @Inject InitOAuth(ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) { @@ -87,6 +88,8 @@ sections.get(PLUGIN_SECTION, pluginName + PhabricatorOAuthService.CONFIG_SUFFIX); this.tuleapOAuthProviderSection = sections.get(PLUGIN_SECTION, pluginName + TuleapOAuthService.CONFIG_SUFFIX); + this.auth0OAuthProviderSection = + sections.get(PLUGIN_SECTION, pluginName + Auth0OAuthService.CONFIG_SUFFIX); } @Override @@ -214,6 +217,13 @@ if (configureTuleapOAuthProvider && configureOAuth(tuleapOAuthProviderSection)) { checkRootUrl(tuleapOAuthProviderSection.string("Tuleap Root URL", ROOT_URL, null)); } + + boolean configureAuth0OAuthProvider = + ui.yesno( + isConfigured(auth0OAuthProviderSection), "Use Auth0 OAuth provider for Gerrit login ?"); + if (configureAuth0OAuthProvider && configureOAuth(auth0OAuthProviderSection)) { + checkRootUrl(auth0OAuthProviderSection.string("Auth0 Root URL", ROOT_URL, null)); + } } /**
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index fa66ba6..2059c18 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -76,6 +76,11 @@ root-url = "<root url>" # for example, https://tuleap.example.com client-id = "<client-id>" client-secret = "<client-secret>" + + [plugin "@PLUGIN@-auth0-oauth"] + root-url = "<root url>" # for example, https://dev-abc.us.auth0.com + client-id = "<client-id>" + client-secret = "<client-secret>" ``` When one from the sections above is omitted, OAuth SSO is used.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/Auth0ApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/Auth0ApiTest.java new file mode 100644 index 0000000..8e7eebc --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/Auth0ApiTest.java
@@ -0,0 +1,35 @@ +// Copyright (C) 2023 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.common.truth.Truth.assertThat; + +import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; +import org.junit.Before; +import org.junit.Test; + +public class Auth0ApiTest { + private Auth0Api api; + + @Before + public void setUp() { + api = new Auth0Api("https://dev-abc.us.auth0.com"); + } + + @Test + public void testAccessTokenExtractor() { + assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); + } +}