[Cognito OAuth]Support for link-to-existing-gerrit-accounts
This adds support for link-to-existing-gerrit-accounts to Cognito
service in OAuth plugin.
This adds a configuration option named link-to-existing-gerrit-accounts
that can be set to true to migrate to this provider from existing
LDAP accounts by linking them with externalIDs instead of trying
to create new accounts.
Change-Id: I9724cf13efe7026f6959b5e585b9c465063cdf82
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java
index eee5a94..bcd2126 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthService.java
@@ -53,6 +53,7 @@
private final String rootUrl;
private final OAuth20Service service;
private final String serviceName;
+ private final boolean linkExistingGerrit;
@Inject
CognitoOAuthService(
@@ -68,6 +69,7 @@
}
serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Cognito");
+ linkExistingGerrit = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_GERRIT_ACCOUNT, false);
service =
new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
@@ -110,7 +112,7 @@
asString(username),
asString(email),
asString(name),
- null);
+ linkExistingGerrit ? "gerrit:" + username.getAsString() : null /*claimedIdentity*/);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Cannot retrieve user info resource", e);
}
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 183f46e..a292b74 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -248,6 +248,8 @@
"Use Cognito OAuth provider for Gerrit login ?");
if (configureCognitoOAuthProvider && configureOAuth(cognitoOAuthProviderSection)) {
checkRootUrl(cognitoOAuthProviderSection.string("Cognito Root URL", ROOT_URL, null));
+ cognitoOAuthProviderSection.string(
+ "Link to existing Gerrit LDAP accounts?", LINK_TO_EXISTING_GERRIT_ACCOUNT, "false");
}
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 2ebd021..2373dec 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -336,11 +336,14 @@
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
+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.
+
+#### Migrating from LDAP to Authentik/Cognito
Set the `link-to-existing-gerrit-accounts = true` option.
-If you have used LDAP before and have accounts with an externalIDs like `gerrit:firstname.lastname` and a user in Authentik
-with username `firstname.lastname` logs in it will link the Authentik account to that Gerrit account.
+If you have used LDAP before and have accounts with an externalIDs like `gerrit:firstname.lastname` and a user in Authentik/Cognito
+with username `firstname.lastname` logs in it will link the Authentik/Cognito account to that Gerrit account.
-When all users has logged in once in Gerrit with their Authentik account it's recommended that the configuration option is removed.
+When all users has logged in once in Gerrit with their Authentik/Cognito account it's recommended that the configuration option is removed.
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/CognitoApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/CognitoApiTest.java
new file mode 100644
index 0000000..bf97a70
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/CognitoApiTest.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2025 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 org.junit.Before;
+import org.junit.Test;
+
+public class CognitoApiTest {
+
+ private CognitoApi api;
+ private static final String TEST_ROOT_URL = "https://test.example.com";
+
+ @Before
+ public void setUp() {
+ api = new CognitoApi(TEST_ROOT_URL);
+ }
+
+ @Test
+ public void testGetAccessTokenEndpoint() {
+ assertThat(api.getAccessTokenEndpoint()).isEqualTo(TEST_ROOT_URL + "/oauth2/token/");
+ }
+
+ @Test
+ public void testGetAuthorizationBaseUrl() {
+ assertThat(api.getAuthorizationBaseUrl()).isEqualTo(TEST_ROOT_URL + "/oauth2/authorize/");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthServiceTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthServiceTest.java
new file mode 100644
index 0000000..6b10e78
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/CognitoOAuthServiceTest.java
@@ -0,0 +1,195 @@
+// Copyright (C) 2025 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Provider;
+
+import java.lang.reflect.Field;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CognitoOAuthServiceTest {
+
+ // Mocks for constructor dependencies of CognitoOAuthService
+ @Mock private PluginConfigFactory mockConfigFactory;
+ @Mock private PluginConfig mockPluginConfig;
+ @Mock private Provider<String> mockUrlProvider;
+
+ // This is the ScribeJava service we want to mock
+ @Mock private OAuth20Service mockScribeOAuthService;
+
+ // Constants for configuration values
+ private static final String TEST_PLUGIN_NAME = "gerrit-oauth-provider-cognito";
+ private static final String TEST_CANONICAL_WEB_URL = "http://gerrit.example.com";
+ private static final String TEST_COGNITO_ROOT_URL =
+ "https://cognito-idp.us-east-1.amazonaws.com/USER_POOL_ID";
+ private static final String TEST_CLIENT_ID = "test-client-id-123";
+ private static final String TEST_CLIENT_SECRET = "test-client-secret-abc";
+ private static final String DEFAULT_SERVICE_NAME = "Cognito";
+
+ // User details from Cognito
+ private static final String COGNITO_USER_ID = "abcdef-12345-uuid";
+ private static final String COGNITO_USERNAME = "jane.doe"; // This is the expected username
+ private static final String COGNITO_EMAIL = "jane.doe@example.com";
+ private static final String COGNITO_NAME = "Jane Doe"; // This is the expected display name
+
+ // Define the prefix locally in the test, mirroring CognitoOAuthService
+ private static final String COGNITO_PROVIDER_PREFIX_FOR_TEST = "cognito-oauth:";
+
+ @Before
+ public void setUp() throws Exception {
+ // Mock the PluginConfigFactory to return our mockPluginConfig
+ when(mockConfigFactory.getFromGerritConfig(
+ TEST_PLUGIN_NAME + CognitoOAuthService.CONFIG_SUFFIX))
+ .thenReturn(mockPluginConfig);
+
+ // Mock the CanonicalWebUrl provider
+ when(mockUrlProvider.get()).thenReturn(TEST_CANONICAL_WEB_URL);
+
+ // Configure the mockPluginConfig with necessary values for CognitoOAuthService
+ // constructor
+ when(mockPluginConfig.getString(InitOAuth.ROOT_URL)).thenReturn(TEST_COGNITO_ROOT_URL);
+ when(mockPluginConfig.getString(InitOAuth.CLIENT_ID)).thenReturn(TEST_CLIENT_ID);
+ when(mockPluginConfig.getString(InitOAuth.CLIENT_SECRET)).thenReturn(TEST_CLIENT_SECRET);
+ when(mockPluginConfig.getString(InitOAuth.SERVICE_NAME, DEFAULT_SERVICE_NAME))
+ .thenReturn(DEFAULT_SERVICE_NAME);
+ }
+
+ /**
+ * Helper method to create an instance of CognitoOAuthService with a mocked internal
+ * OAuth20Service. This allows testing the logic of CognitoOAuthService in isolation.
+ *
+ * @param linkExistingGerritAccounts The value for the 'link-to-existing-gerrit-account' config.
+ * @return An instance of CognitoOAuthService with the ScribeJava service mocked.
+ * @throws Exception If reflection fails.
+ */
+ private CognitoOAuthService createServiceAndInjectMock(boolean linkExistingGerritAccounts)
+ throws Exception {
+ // Configure the specific 'link-to-existing-gerrit-account' for this instance
+ when(mockPluginConfig.getBoolean(InitOAuth.LINK_TO_EXISTING_GERRIT_ACCOUNT, false))
+ .thenReturn(linkExistingGerritAccounts);
+
+ CognitoOAuthService serviceInstance =
+ new CognitoOAuthService(mockConfigFactory, TEST_PLUGIN_NAME, mockUrlProvider);
+
+ // Replace the internal OAuth20Service with our mock using reflection.
+ Field serviceField = CognitoOAuthService.class.getDeclaredField("service");
+ serviceField.setAccessible(true); // Allow modification of the private final field
+ serviceField.set(serviceInstance, mockScribeOAuthService); // Inject our mock
+ return serviceInstance;
+ }
+
+ /**
+ * Helper method to mock the HTTP response from Cognito's user info endpoint.
+ *
+ * @param userId The 'sub' (subject) ID from Cognito.
+ * @param username The 'preferred_username' from Cognito. Can be null.
+ * @param email The 'email' from Cognito. Can be null.
+ * @param name The 'name' from Cognito. Can be null.
+ * @throws Exception If mocking fails.
+ */
+ private void mockCognitoUserInfoResponse(
+ String userId, String username, String email, String name) throws Exception {
+ // Construct the JSON response string. Handles nulls correctly for JSON.
+ String cognitoJsonResponse =
+ String.format(
+ "{\"sub\":\"%s\",\"preferred_username\":%s,\"email\":%s,\"name\":%s}",
+ userId, // 'sub' should always be a non-null string
+ username == null ? "null" : "\"" + username + "\"",
+ email == null ? "null" : "\"" + email + "\"",
+ name == null ? "null" : "\"" + name + "\"");
+
+ Response mockHttpResponse = mock(Response.class);
+ when(mockHttpResponse.getCode()).thenReturn(HttpServletResponse.SC_OK);
+ // Simulate successful HTTP 200 OK
+ when(mockHttpResponse.getBody()).thenReturn(cognitoJsonResponse);
+
+ // Configure the mockScribeOAuthService to return this mockHttpResponse
+ when(mockScribeOAuthService.execute(any(OAuthRequest.class))).thenReturn(mockHttpResponse);
+ }
+
+ /**
+ * Test Case 1: linkExistingGerrit=true, username is VALID. Expects claimedIdentity to be
+ * "gerrit:{username}".
+ */
+ @Test
+ public void getUserInfo_linkTrue_validUsername_shouldSetClaimedIdentity() throws Exception {
+ // --- ARRANGE ---
+ CognitoOAuthService service = createServiceAndInjectMock(true);
+ // linkExistingGerrit = true
+ mockCognitoUserInfoResponse(COGNITO_USER_ID, COGNITO_USERNAME, COGNITO_EMAIL, COGNITO_NAME);
+ OAuthToken inputToken =
+ new OAuthToken("dummyAccessToken", "dummySecretForTest", "dummyRawResponse");
+
+ // --- ACT ---
+ OAuthUserInfo userInfo = service.getUserInfo(inputToken);
+
+ // --- ASSERT ---
+ // Primary assertion for this test case
+ assertThat(userInfo.getClaimedIdentity()).isEqualTo("gerrit:" + COGNITO_USERNAME);
+
+ // Secondary assertions for completeness of OAuthUserInfo object
+ assertThat(userInfo).isNotNull();
+ assertThat(userInfo.getExternalId())
+ .isEqualTo(COGNITO_PROVIDER_PREFIX_FOR_TEST + COGNITO_USER_ID);
+ assertThat(userInfo.getUserName()).isEqualTo(COGNITO_USERNAME);
+ assertThat(userInfo.getDisplayName()).isEqualTo(COGNITO_NAME);
+ assertThat(userInfo.getEmailAddress()).isEqualTo(COGNITO_EMAIL);
+ }
+
+ /**
+ * Test Case 2: linkExistingGerrit=false, username is VALID. Expects claimedIdentity to be null.
+ */
+ @Test
+ public void getUserInfo_linkFalse_validUsername_shouldSetClaimedIdentityNull() throws Exception {
+ // --- ARRANGE ---
+ CognitoOAuthService service = createServiceAndInjectMock(false);
+ // linkExistingGerrit = false
+ mockCognitoUserInfoResponse(COGNITO_USER_ID, COGNITO_USERNAME, COGNITO_EMAIL, COGNITO_NAME);
+ OAuthToken inputToken =
+ new OAuthToken("dummyAccessToken", "dummySecretForTest", "dummyRawResponse");
+
+ // --- ACT ---
+ OAuthUserInfo userInfo = service.getUserInfo(inputToken);
+
+ // --- ASSERT ---
+ // Primary assertion for this test case
+ assertThat(userInfo.getClaimedIdentity()).isNull();
+
+ // Secondary assertions
+ assertThat(userInfo).isNotNull();
+ assertThat(userInfo.getExternalId())
+ .isEqualTo(COGNITO_PROVIDER_PREFIX_FOR_TEST + COGNITO_USER_ID);
+ assertThat(userInfo.getUserName()).isEqualTo(COGNITO_USERNAME);
+ }
+}