From 6115dc836a3eb7fa739a512d5093c2cc5232b91d Mon Sep 17 00:00:00 2001 From: Barna Magyarkuti Date: Wed, 17 Sep 2025 17:09:30 +0200 Subject: [PATCH 1/2] in allowed domains, allow unlinked sso logins --- .../uk/ac/cam/cl/dtg/segue/api/Constants.java | 1 + .../api/managers/UserAccountManager.java | 17 +++++- .../auth/MicrosoftAutoLinkingConfig.java | 52 +++++++++++++++++++ .../SegueGuiceConfigurationModule.java | 7 ++- .../api/AbstractIsaacIntegrationTest.java | 5 +- .../dtg/isaac/api/AuthenticationFacadeIT.java | 21 +++++++- .../api/IsaacIntegrationTestWithREST.java | 27 +++++++++- .../ac/cam/cl/dtg/isaac/api/SignupFlowIT.java | 2 +- .../cam/cl/dtg/isaac/api/UsersFacadeIT.java | 4 +- .../segue/api/managers/UserManagerTest.java | 3 +- .../auth/MicrosoftAutoLinkingConfigTest.java | 38 ++++++++++++++ .../segue-integration-test-config.yaml | 2 + 12 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java create mode 100644 src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java index c2ca274452..647a835e01 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java @@ -300,6 +300,7 @@ public String toString() { public static final String GOOGLE_OAUTH_SCOPES = "GOOGLE_OAUTH_SCOPES"; // Microsoft properties + public static final String MICROSOFT_ALLOW_AUTO_LINKING = "MICROSOFT_ALLOW_AUTO_LINKING"; public static final String MICROSOFT_SECRET = "MICROSOFT_SECRET"; public static final String MICROSOFT_CLIENT_ID = "MICROSOFT_CLIENT_ID"; public static final String MICROSOFT_TENANT_ID = "MICROSOFT_TENANT_ID"; diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java index ee94a6a13b..3f2cfab669 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java @@ -53,6 +53,7 @@ import uk.ac.cam.cl.dtg.segue.auth.IAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.IPasswordAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.ISecondFactorAuthenticator; +import uk.ac.cam.cl.dtg.segue.auth.MicrosoftAutoLinkingConfig; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AdditionalAuthenticationRequiredException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationCodeException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationProviderMappingException; @@ -129,6 +130,7 @@ public class UserAccountManager implements IUserAccountManager { private final ISecondFactorAuthenticator secondFactorManager; private final AbstractUserPreferenceManager userPreferenceManager; + private final MicrosoftAutoLinkingConfig microsoftAutoLinkingConfig; private final Pattern restrictedSignupEmailRegex; private static final int USER_NAME_MAX_LENGTH = 255; @@ -155,7 +157,8 @@ public UserAccountManager(final IUserDataManager database, final QuestionManager final EmailManager emailQueue, final IAnonymousUserDataManager temporaryUserCache, final ILogManager logManager, final UserAuthenticationManager userAuthenticationManager, final ISecondFactorAuthenticator secondFactorManager, - final AbstractUserPreferenceManager userPreferenceManager) { + final AbstractUserPreferenceManager userPreferenceManager, + final MicrosoftAutoLinkingConfig msAutoLinkingConfig) { Objects.requireNonNull(properties.getProperty(HMAC_SALT)); Objects.requireNonNull(properties.getProperty(SESSION_EXPIRY_SECONDS_DEFAULT)); @@ -177,6 +180,7 @@ public UserAccountManager(final IUserDataManager database, final QuestionManager this.userAuthenticationManager = userAuthenticationManager; this.secondFactorManager = secondFactorManager; this.userPreferenceManager = userPreferenceManager; + this.microsoftAutoLinkingConfig = msAutoLinkingConfig; String forbiddenEmailRegex = properties.getProperty(RESTRICTED_SIGNUP_EMAIL_REGEX); if (null == forbiddenEmailRegex || forbiddenEmailRegex.isEmpty()) { @@ -267,6 +271,17 @@ public RegisteredUserDTO authenticateCallback(final HttpServletRequest request, } RegisteredUser currentUser = getCurrentRegisteredUserDO(request); + RegisteredUser matchedUserFromEmail = this.findUserByEmail(providerUserDO.getEmail()); + + if (microsoftAutoLinkingConfig.enabledFor(providerUserDO.getEmail()) && + userFromLinkedAccount == null && + currentUser == null && + providerUserDO.getEmail() != null && + !providerUserDO.getEmail().isEmpty() && + matchedUserFromEmail != null) { + return this.logUserIn(request, response, matchedUserFromEmail, rememberMe); + } + // if the user is currently logged in and this is a request for a linked account, then create the new link. if (null != currentUser) { Boolean intentionToLinkRegistered = (Boolean) request.getSession().getAttribute(LINK_ACCOUNT_PARAM_NAME); diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java b/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java new file mode 100644 index 0000000000..eb2e9e8a77 --- /dev/null +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java @@ -0,0 +1,52 @@ +package uk.ac.cam.cl.dtg.segue.auth; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import org.apache.commons.lang3.StringUtils; +import uk.ac.cam.cl.dtg.segue.api.Constants; + +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public class MicrosoftAutoLinkingConfig { + private List entries; + + @Inject + public MicrosoftAutoLinkingConfig(@Nullable @Named(Constants.MICROSOFT_ALLOW_AUTO_LINKING) final String jsonConfig) { + try { + this.entries = new ObjectMapper().readValue(jsonConfig, new TypeReference<>() {}); + } catch (final JsonProcessingException | IllegalArgumentException e) { + this.entries = new ArrayList<>(); + } + } + + public boolean enabledFor(final String email) { + return this.entries.stream().anyMatch( + entry -> entry.emailDomain.equals(StringUtils.substringAfter(email, '@')) + ); + } + + public int size() { + return this.entries.size(); + } + + private static class ConfigEntry { + String tenantId; + String emailDomain; + + @JsonCreator + public ConfigEntry( + @JsonProperty("tenantId") final String tenantId, + @JsonProperty("emailDomain") final String emailDomain + ) { + this.tenantId = tenantId; + this.emailDomain = emailDomain; + } + } +} diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/configuration/SegueGuiceConfigurationModule.java b/src/main/java/uk/ac/cam/cl/dtg/segue/configuration/SegueGuiceConfigurationModule.java index e1a05e78ea..e31ad66267 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/configuration/SegueGuiceConfigurationModule.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/configuration/SegueGuiceConfigurationModule.java @@ -92,6 +92,7 @@ import uk.ac.cam.cl.dtg.segue.auth.IAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.ISecondFactorAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.ISegueHashingAlgorithm; +import uk.ac.cam.cl.dtg.segue.auth.MicrosoftAutoLinkingConfig; import uk.ac.cam.cl.dtg.segue.auth.RaspberryPiOidcAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.SegueChainedPBKDFv1SCryptv1; import uk.ac.cam.cl.dtg.segue.auth.SegueChainedPBKDFv2SCryptv1; @@ -367,6 +368,7 @@ private void configureAuthenticationProviders() { // Microsoft try { + this.bindConstantToNullableProperty(MICROSOFT_ALLOW_AUTO_LINKING, globalProperties); new MicrosoftAuthenticator( globalProperties.getProperty(Constants.MICROSOFT_CLIENT_ID), globalProperties.getProperty(Constants.MICROSOFT_TENANT_ID), @@ -807,11 +809,12 @@ private IUserAccountManager getUserManager(final IUserDataManager database, fina final ILogManager logManager, final MapperFacade mapperFacade, final UserAuthenticationManager userAuthenticationManager, final ISecondFactorAuthenticator secondFactorManager, - final AbstractUserPreferenceManager userPreferenceManager) { + final AbstractUserPreferenceManager userPreferenceManager, + final MicrosoftAutoLinkingConfig microsoftAutoLinkingConfig) { if (null == userManager) { userManager = new UserAccountManager(database, questionManager, properties, providersToRegister, mapperFacade, emailQueue, temporaryUserCache, logManager, userAuthenticationManager, - secondFactorManager, userPreferenceManager); + secondFactorManager, userPreferenceManager, microsoftAutoLinkingConfig); log.info("Creating singleton of UserManager"); } diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java index 96652e6da3..b630da5ba2 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java @@ -60,6 +60,7 @@ import uk.ac.cam.cl.dtg.segue.auth.IAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.ISecondFactorAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.ISegueHashingAlgorithm; +import uk.ac.cam.cl.dtg.segue.auth.MicrosoftAutoLinkingConfig; import uk.ac.cam.cl.dtg.segue.auth.RaspberryPiOidcAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.SegueLocalAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.SeguePBKDF2v3; @@ -151,6 +152,7 @@ public class AbstractIsaacIntegrationTest { protected static IQuizQuestionAttemptPersistenceManager quizQuestionAttemptPersistenceManager; protected static QuizQuestionManager quizQuestionManager; protected static PgUsers pgUsers; + protected static MicrosoftAutoLinkingConfig microsoftAutoLinkingConfig; // Services protected static AssignmentService assignmentService; @@ -282,7 +284,8 @@ public static void setUpClass() throws Exception { } replay(secondFactorManager); - userAccountManager = new UserAccountManager(pgUsers, questionManager, properties, providersToRegister, mapperFacade, emailManager, pgAnonymousUsers, logManager, userAuthenticationManager, secondFactorManager, userPreferenceManager); + microsoftAutoLinkingConfig = new MicrosoftAutoLinkingConfig(properties.getProperty("MICROSOFT_ALLOW_AUTO_LINKING")); + userAccountManager = new UserAccountManager(pgUsers, questionManager, properties, providersToRegister, mapperFacade, emailManager, pgAnonymousUsers, logManager, userAuthenticationManager, secondFactorManager, userPreferenceManager, microsoftAutoLinkingConfig); ObjectMapper objectMapper = new ObjectMapper(); mailGunEmailManager = new MailGunEmailManager(globalTokens, properties, userPreferenceManager); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AuthenticationFacadeIT.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AuthenticationFacadeIT.java index e37158b0e2..bfeca8e253 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AuthenticationFacadeIT.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AuthenticationFacadeIT.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; import uk.ac.cam.cl.dtg.segue.api.AuthenticationFacade; +import uk.ac.cam.cl.dtg.segue.api.UsersFacade; import uk.ac.cam.cl.dtg.segue.auth.AuthenticationProvider; import uk.ac.cam.cl.dtg.segue.auth.MicrosoftAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.microsoft.KeyPair; @@ -86,7 +87,21 @@ public void matchedAccountNotConnected_returnsNotUsingMicrosoftResponse() throws response.assertError(notUsingMicrosoftMessage, Response.Status.FORBIDDEN); response.assertNoUserLoggedIn(); + } + + @Test + public void matchedAccountNotConnectedAutoLinkingEnabled_signsInAndReturnsUser() throws Exception { + var client = prepareTestCase(token.valid(s -> s, u -> { + u.put("oid", UUID.randomUUID().toString()); + u.put("email", "not_linked@linkable.com"); + return null; + })); + var userId = client.register("not_linked@linkable.com"); + + var response = client.get("/auth/microsoft/callback" + validQuery); + response.assertEntityReturned(userAccountManager.getUserDTOById(userId)); + response.assertUserLoggedIn(userId); } @Test @@ -171,7 +186,11 @@ public void initialSignup_addsForceSignUpParameterToRedirectURL() throws Excepti } TestServer subject() throws Exception { - return startServer(new AuthenticationFacade(properties, userAccountManager, logManager, misuseMonitor)); + return startServer( + new AuthenticationFacade(properties, userAccountManager, logManager, misuseMonitor), + new UsersFacade(properties, userAccountManager, logManager, userAssociationManager, + misuseMonitor, userPreferenceManager, schoolListReader) + ); } TestClient prepareTestCase(final String token) throws Exception { diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java index fa5c71e7f0..2677f87004 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java @@ -19,6 +19,7 @@ import jakarta.ws.rs.core.Application; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -123,7 +124,7 @@ static class TestClient { this.baseUrl = baseUrl; this.registerCleanup = registerCleanup; this.builder = builder; - this.client = ClientBuilder.newClient().register(new CookieJarFilter()); + this.client = ClientBuilder.newClient(); } public TestResponse get(final String url) { @@ -141,12 +142,30 @@ public TestResponse post(final String url, final Object body) { } public void loginAs(final RegisteredUser user) { + this.client.register(new CookieJarFilter()); var request = client.target(baseUrl + "/auth/SEGUE/authenticate").request(MediaType.APPLICATION_JSON); var body = new LocalAuthDTO(); body.setEmail(user.getEmail()); body.setPassword("test1234"); this.currentUser = builder.apply(request).post(Entity.json(body), RegisteredUserDTO.class); } + + public long register(final String email) { + var user = new HashMap<>(); + user.put("email", email); + user.put("password", ITConstants.TEST_SIGNUP_PASSWORD); + user.put("familyName", "signup"); + user.put("givenName", "test"); + + var payload = new HashMap<>(); + payload.put("registeredUser", user); + payload.put("userPreferences", new HashMap<>()); + payload.put("passwordCurrent", null); + + TestResponse response = this.post("/users", payload); + response.assertStatus(Response.Status.OK.getStatusCode()); + return response.readEntity(RegisteredUserDTO.class).getId(); + } } static class TestResponse { @@ -178,9 +197,13 @@ void assertEntityReturned(final T entity) { } T readEntity(final Class klass) { - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + this.assertStatus(Response.Status.OK.getStatusCode()); return response.readEntity(klass); } + + void assertStatus(int expectedStatus) { + assertEquals(expectedStatus, response.getStatus()); + } } interface RequestBuilder extends Function {} diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/SignupFlowIT.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/SignupFlowIT.java index 9ed8ffeb70..7b2f66f4f5 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/SignupFlowIT.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/SignupFlowIT.java @@ -38,7 +38,7 @@ public void signUpFlow_emailVerificationRequiredCaveatSet_removesCaveatAfterVeri // set up email facade UserAccountManager userAccountManagerForTest = new UserAccountManager(pgUsers, questionManager, propertiesForTest, providersToRegister, mapperFacade, emailManager, pgAnonymousUsers, logManager, - userAuthenticationManager, secondFactorManager, userPreferenceManager); + userAuthenticationManager, secondFactorManager, userPreferenceManager, microsoftAutoLinkingConfig); EmailFacade emailFacade = new EmailFacade(propertiesForTest, logManager, emailManager, userAccountManagerForTest, contentManager, misuseMonitor); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/UsersFacadeIT.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/UsersFacadeIT.java index a64dcf027d..6f80c3a1ca 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/UsersFacadeIT.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/UsersFacadeIT.java @@ -123,7 +123,7 @@ public void createOrUpdateEndpoint_registerAsTeacherWithSignUpFlagEnabled_accept UserAccountManager userAccountManagerForTest = new UserAccountManager(pgUsers, questionManager, propertiesForTest, providersToRegister, mapperFacade, emailManager, pgAnonymousUsers, logManager, - userAuthenticationManager, secondFactorManager, userPreferenceManager); + userAuthenticationManager, secondFactorManager, userPreferenceManager, microsoftAutoLinkingConfig); UsersFacade usersFacadeForTest = new UsersFacade(propertiesForTest, userAccountManagerForTest, logManager, userAssociationManager, misuseMonitor, userPreferenceManager, schoolListReader); @@ -833,7 +833,7 @@ public void generatePasswordResetTokenForOtherUser_teacherResetsStudentPassword_ pgUsers, dummyDeletionTokenManager, properties, providersToRegister, dummyEmailManager); UserAccountManager userAccountManager = new UserAccountManager( pgUsers, questionManager, properties, providersToRegister, mapperFacade, emailManager, pgAnonymousUsers, - logManager, userAuthenticationManager, secondFactorManager, userPreferenceManager); + logManager, userAuthenticationManager, secondFactorManager, userPreferenceManager, microsoftAutoLinkingConfig); UsersFacade usersFacadeForTest = new UsersFacade(properties, userAccountManager, logManager, userAssociationManager, misuseMonitor, userPreferenceManager, schoolListReader); diff --git a/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java index 2089afa4bc..dc9a23e49a 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java @@ -44,6 +44,7 @@ import uk.ac.cam.cl.dtg.segue.auth.IFederatedAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.IOAuth2Authenticator; import uk.ac.cam.cl.dtg.segue.auth.ISecondFactorAuthenticator; +import uk.ac.cam.cl.dtg.segue.auth.MicrosoftAutoLinkingConfig; import uk.ac.cam.cl.dtg.segue.auth.SegueLocalAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationProviderMappingException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.CrossSiteRequestForgeryException; @@ -788,7 +789,7 @@ private UserAccountManager buildTestUserManager(final AuthenticationProvider pro return new UserAccountManager(dummyDatabase, this.dummyQuestionDatabase, this.dummyPropertiesLoader, providerMap, this.dummyMapper, this.dummyQueue, this.dummyUserCache, this.dummyLogManager, buildTestAuthenticationManager(provider, authenticator), dummySecondFactorAuthenticator, - dummyUserPreferenceManager); + dummyUserPreferenceManager, new MicrosoftAutoLinkingConfig("{}")); } private UserAuthenticationManager buildTestAuthenticationManager() { diff --git a/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java b/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java new file mode 100644 index 0000000000..342b0dba0d --- /dev/null +++ b/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java @@ -0,0 +1,38 @@ +package uk.ac.cam.cl.dtg.segue.auth; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import static org.junit.Assert.*; + +@SuppressWarnings("checkstyle:MissingJavadocType") +public class MicrosoftAutoLinkingConfigTest { + private final String emptyConfig = "{}"; + private final String tenantId = "766576da-ca7a-46eb-bf55-43eea65e85b7"; + private final String emailDomain = "@linkable.com"; + private final String validConfig = new JSONArray() + .put(new JSONObject().put("tenantId", tenantId).put("emailDomain", emailDomain)) + .toString(); + + @Test + public final void testConstruction_emptyConfig_noConfigParsed() { + assertEquals(0, new MicrosoftAutoLinkingConfig(emptyConfig).size()); + } + + @Test + public final void testConstruction_nullConfig_noConfigParsed() { + assertEquals(0, new MicrosoftAutoLinkingConfig(null).size()); + } + + @Test + public final void testConstruction_validConfig_parsedSuccessfully() { + assertEquals(1, new MicrosoftAutoLinkingConfig(validConfig).size()); + } + + @Test + public final void testEnabledFor_validConfig_enabledForMatchingTidAndEmail() { + var autoLinkingConfig = new MicrosoftAutoLinkingConfig(validConfig); + assertTrue(autoLinkingConfig.enabledFor(String.format("someone@%s", emailDomain))); + } +} \ No newline at end of file diff --git a/src/test/resources/segue-integration-test-config.yaml b/src/test/resources/segue-integration-test-config.yaml index b1b522944d..6929f23516 100644 --- a/src/test/resources/segue-integration-test-config.yaml +++ b/src/test/resources/segue-integration-test-config.yaml @@ -62,6 +62,8 @@ SESSION_EXPIRY_SECONDS_DEFAULT: "432000" SESSION_EXPIRY_SECONDS_REMEMBERED: "1209600" EMAIL_VERIFICATION_ENDPOINT_TOKEN: some-very-secret-token # Federated Authentication +# Microsoft +MICROSOFT_ALLOW_AUTO_LINKING: "[{\"tenantId\": \"766576da-ca7a-46eb-bf55-43eea65e85b7\", \"emailDomain\": \"linkable.com\"}]" # Google GOOGLE_CLIENT_SECRET_LOCATION: x GOOGLE_CALLBACK_URI: x From cbcc037134858910d159f2360bf40f860edf13fe Mon Sep 17 00:00:00 2001 From: Barna Magyarkuti Date: Wed, 17 Sep 2025 17:48:50 +0200 Subject: [PATCH 2/2] don't link accounts with an invalid email --- .../auth/MicrosoftAutoLinkingConfig.java | 15 ++++++++---- .../auth/MicrosoftAutoLinkingConfigTest.java | 23 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java b/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java index eb2e9e8a77..2cf330eb76 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfig.java @@ -9,16 +9,20 @@ import com.google.inject.name.Named; import org.apache.commons.lang3.StringUtils; import uk.ac.cam.cl.dtg.segue.api.Constants; +import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager; import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; public class MicrosoftAutoLinkingConfig { private List entries; @Inject - public MicrosoftAutoLinkingConfig(@Nullable @Named(Constants.MICROSOFT_ALLOW_AUTO_LINKING) final String jsonConfig) { + public MicrosoftAutoLinkingConfig( + @Nullable @Named(Constants.MICROSOFT_ALLOW_AUTO_LINKING) final String jsonConfig + ) { try { this.entries = new ObjectMapper().readValue(jsonConfig, new TypeReference<>() {}); } catch (final JsonProcessingException | IllegalArgumentException e) { @@ -27,9 +31,8 @@ public MicrosoftAutoLinkingConfig(@Nullable @Named(Constants.MICROSOFT_ALLOW_AUT } public boolean enabledFor(final String email) { - return this.entries.stream().anyMatch( - entry -> entry.emailDomain.equals(StringUtils.substringAfter(email, '@')) - ); + return UserAccountManager.isUserEmailValid(email) + && this.entries.stream().anyMatch(ConfigEntry.matchingDomain(email)); } public int size() { @@ -48,5 +51,9 @@ public ConfigEntry( this.tenantId = tenantId; this.emailDomain = emailDomain; } + + public static Predicate matchingDomain(final String email) { + return entry -> entry.emailDomain.equals(StringUtils.substringAfter(email, '@')); + } } } diff --git a/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java b/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java index 342b0dba0d..8995e186d1 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/segue/auth/MicrosoftAutoLinkingConfigTest.java @@ -10,7 +10,7 @@ public class MicrosoftAutoLinkingConfigTest { private final String emptyConfig = "{}"; private final String tenantId = "766576da-ca7a-46eb-bf55-43eea65e85b7"; - private final String emailDomain = "@linkable.com"; + private final String emailDomain = "enabled.com"; private final String validConfig = new JSONArray() .put(new JSONObject().put("tenantId", tenantId).put("emailDomain", emailDomain)) .toString(); @@ -25,14 +25,31 @@ public final void testConstruction_nullConfig_noConfigParsed() { assertEquals(0, new MicrosoftAutoLinkingConfig(null).size()); } + @Test + public final void testConstruction_invalidConfig_noConfigParsed() { + assertEquals(0, new MicrosoftAutoLinkingConfig("invalid json").size()); + } + @Test public final void testConstruction_validConfig_parsedSuccessfully() { assertEquals(1, new MicrosoftAutoLinkingConfig(validConfig).size()); } @Test - public final void testEnabledFor_validConfig_enabledForMatchingTidAndEmail() { + public final void testEnabledFor_matchingDomain_returnsTrue() { + var autoLinkingConfig = new MicrosoftAutoLinkingConfig(validConfig); + assertTrue(autoLinkingConfig.enabledFor("someone@enabled.com")); + } + + @Test + public final void testEnabledFor_mistmatchedDomain_returnsFalse() { + var autoLinkingConfig = new MicrosoftAutoLinkingConfig(validConfig); + assertFalse(autoLinkingConfig.enabledFor("someone@disabled.com")); + } + + @Test + public final void testEnabledFor_invalidEmail_returnsFalse() { var autoLinkingConfig = new MicrosoftAutoLinkingConfig(validConfig); - assertTrue(autoLinkingConfig.enabledFor(String.format("someone@%s", emailDomain))); + assertFalse(autoLinkingConfig.enabledFor("@enabled.com")); } } \ No newline at end of file