From 0f4e5ca75dab2d48dadee8a777e38c6971f672d2 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 10:44:36 +0200
Subject: [PATCH 1/8] added translates about account linking

---
 src/test/shell/data/i18n/de.json | 4 ++++
 src/test/shell/data/i18n/en.json | 4 ++++
 src/test/shell/data/i18n/fr.json | 4 ++++
 src/test/shell/data/i18n/pl.json | 4 ++++
 4 files changed, 16 insertions(+)

diff --git a/src/test/shell/data/i18n/de.json b/src/test/shell/data/i18n/de.json
index 4cce919a9..a9ffdf59f 100644
--- a/src/test/shell/data/i18n/de.json
+++ b/src/test/shell/data/i18n/de.json
@@ -93,6 +93,10 @@
     "LOGIN_CAN_NOT_BE_PERFORMED_MESSAGE": "Systemkomponenten-Login kann nicht durchgeführt werden",
     "USER_DISABLED_MESSAGE": "User is disabled"
   },
+  "ACCOUNT_LINKING": {
+    "HEADER": "Verknüpfung von Konten",
+    "INFO": "Sie versuchen, sich bei einem lokalen Konto anzumelden, das mit der von Ihnen angegebenen E-Mail-Adresse bereits in unserem System existiert. Bitte geben Sie Ihr Passwort ein, um Ihre Identität zu bestätigen und die Verknüpfung der Konten zu bestätigen."
+  },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "nmaas Testinstanz",
     "BODY": "Bitte beachten Sie, dass dies eine Test-nmaas-Instanz ist. Die Benutzerisolations- und Datensicherheitsmaßnahmen sind nicht so streng wie in der Produktionsinstanz. Verwenden Sie diese Instanz nicht zur Überwachung realer Geräte.",
diff --git a/src/test/shell/data/i18n/en.json b/src/test/shell/data/i18n/en.json
index 8dd956592..c7ce98f03 100644
--- a/src/test/shell/data/i18n/en.json
+++ b/src/test/shell/data/i18n/en.json
@@ -93,6 +93,10 @@
     "LOGIN_CAN_NOT_BE_PERFORMED_MESSAGE": "System component login cannot be performed",
     "USER_DISABLED_MESSAGE": "User is disabled"
   },
+  "ACCOUNT_LINKING": {
+    "HEADER": "Account linking",
+    "INFO": "You are trying to log in to an local account that already exists in our system with the email address you provided. Please enter your password to verify it's You and confirm linking of these accounts."
+  },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "nmaas test instance",
     "BODY": "Please be aware that this is a test nmaas instance. The user isolation and data security measures are not so strict as on the production instance. Do not use this instance to monitor real equipment.",
diff --git a/src/test/shell/data/i18n/fr.json b/src/test/shell/data/i18n/fr.json
index ae72dda76..d997acb72 100644
--- a/src/test/shell/data/i18n/fr.json
+++ b/src/test/shell/data/i18n/fr.json
@@ -93,6 +93,10 @@
     "LOGIN_CAN_NOT_BE_PERFORMED_MESSAGE": "Il est impossible de se connecter au composant système",
     "USER_DISABLED_MESSAGE": "User is disabled"
   },
+  "ACCOUNT_LINKING": {
+    "HEADER": "Association de comptes",
+    "INFO": "Vous essayez de vous connecter à un compte local qui existe déjà dans notre système avec l'adresse e-mail que vous avez fournie. Veuillez entrer votre mot de passe pour vérifier votre identité et confirmer la liaison des comptes."
+  },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "Instance de test nmaas",
     "BODY": "Veuillez noter qu'il s'agit d'une instance de test nmaas. Les mesures d'isolation de l'utilisateur et de sécurité des données ne sont pas aussi strictes que sur l'instance de production. N'utilisez pas cette instance pour surveiller un équipement réel.",
diff --git a/src/test/shell/data/i18n/pl.json b/src/test/shell/data/i18n/pl.json
index 756a41814..ad465245e 100644
--- a/src/test/shell/data/i18n/pl.json
+++ b/src/test/shell/data/i18n/pl.json
@@ -93,6 +93,10 @@
     "LOGIN_CAN_NOT_BE_PERFORMED_MESSAGE": "Błąd logowania do systemu",
     "USER_DISABLED_MESSAGE": "Użytkownik jest nieaktywny"
   },
+  "ACCOUNT_LINKING": {
+    "HEADER": "Powiązanie kont",
+    "INFO": "Próbujesz zalogować się na lokalne konto, które już istnieje w naszym systemie dla podanego adresu e-mail. Wprowadź swoje hasło, aby potwierdzić swoją tożsamość i zatwierdzić połączenie kont."
+  },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "Instancja testowa nmaas",
     "BODY": "Pamiętaj, że jest to testowa instancja nmaas. Izolacja użytkowników oraz środki zabezpieczenia danych nie są tak rygorystyczne, jak w przypadku instancji produkcyjnej. Nie używaj tej instancji do monitorowania rzeczywistych urządzeń.",
-- 
GitLab


From 455b074674204f37fd98f2265edc02a9d2d48855 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 10:45:09 +0200
Subject: [PATCH 2/8] init new record

---
 .../java/net/geant/nmaas/portal/api/auth/OidcLogin.java  | 9 +++++++++
 .../net/geant/nmaas/portal/api/auth/UserOidcToken.java   | 5 +++++
 2 files changed, 14 insertions(+)
 create mode 100644 src/main/java/net/geant/nmaas/portal/api/auth/OidcLogin.java
 create mode 100644 src/main/java/net/geant/nmaas/portal/api/auth/UserOidcToken.java

diff --git a/src/main/java/net/geant/nmaas/portal/api/auth/OidcLogin.java b/src/main/java/net/geant/nmaas/portal/api/auth/OidcLogin.java
new file mode 100644
index 000000000..a145c3d84
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/api/auth/OidcLogin.java
@@ -0,0 +1,9 @@
+package net.geant.nmaas.portal.api.auth;
+
+public record OidcLogin(String email,
+                        String password,
+                        String oidcToken,
+                        String uuid,
+                        String firstName,
+                        String lastName) {
+}
diff --git a/src/main/java/net/geant/nmaas/portal/api/auth/UserOidcToken.java b/src/main/java/net/geant/nmaas/portal/api/auth/UserOidcToken.java
new file mode 100644
index 000000000..0d1871d15
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/api/auth/UserOidcToken.java
@@ -0,0 +1,5 @@
+package net.geant.nmaas.portal.api.auth;
+
+
+public record UserOidcToken(String token, String refreshToken, String oidcToken) {
+}
-- 
GitLab


From f66f29a12879a2a9767bd8623558337e07573e97 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 10:45:39 +0200
Subject: [PATCH 3/8] opened endpoint to linking account

---
 src/main/java/net/geant/nmaas/SecurityConfig.java | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/java/net/geant/nmaas/SecurityConfig.java b/src/main/java/net/geant/nmaas/SecurityConfig.java
index db380467d..5e322bc72 100644
--- a/src/main/java/net/geant/nmaas/SecurityConfig.java
+++ b/src/main/java/net/geant/nmaas/SecurityConfig.java
@@ -53,6 +53,7 @@ public class SecurityConfig {
     private static final String AUTH_OIDC_LOGIN_PAGE = "/api/oauth2/authorization/my-oidc";
     private static final String AUTH_OIDC_LOGIN = "/api/auth/oidc/login";
     private static final String AUTH_OIDC_SUCCESS = "/api/oidc/success";
+    private static final String AUTH_OIDC_LINK = "/api/oidc/link";
     private static final String AUTH_LOGOUT = "/api/oidc/logout/*";
     private static final String AUTH_OIDC = "/api/oidc/**";
     private static final String AUTH_CODE = "/api/login/oauth2/code";
@@ -146,6 +147,7 @@ public class SecurityConfig {
                 .csrf(AbstractHttpConfigurer::disable)
                 .authorizeHttpRequests(httpRequest -> httpRequest
                         .requestMatchers(AUTH_WHITELIST).permitAll()
+                        .requestMatchers(AUTH_OIDC_LINK).permitAll()
                         .requestMatchers(AUTH_AUTHENTICATED_LIST).authenticated()
                 )
                 .oauth2Login(oAuth2 -> oAuth2
-- 
GitLab


From 999deb5450bb106c0c71fab727f29d156fab9a10 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 10:47:25 +0200
Subject: [PATCH 4/8] added new methods

---
 .../nmaas/portal/service/OidcUserService.java |  2 ++
 .../service/impl/OidcUserServiceImpl.java     | 35 +++++++++++++------
 2 files changed, 26 insertions(+), 11 deletions(-)

diff --git a/src/main/java/net/geant/nmaas/portal/service/OidcUserService.java b/src/main/java/net/geant/nmaas/portal/service/OidcUserService.java
index aad3204b8..7f94bf9f9 100644
--- a/src/main/java/net/geant/nmaas/portal/service/OidcUserService.java
+++ b/src/main/java/net/geant/nmaas/portal/service/OidcUserService.java
@@ -9,5 +9,7 @@ public interface OidcUserService {
     User checkUser(OidcUser oidcUser);
     User register(OidcUser user, Domain globalDomain);
     User registerNewUser(OidcUser oidcUser);
+    boolean externalUserRequiredLinking(OidcUser oidcUser);
+    User linkUser(String email, String samlToken, String firstName, String lastName);
 
 }
diff --git a/src/main/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImpl.java b/src/main/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImpl.java
index 733624d31..a9eebe460 100644
--- a/src/main/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImpl.java
+++ b/src/main/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImpl.java
@@ -71,16 +71,7 @@ public class OidcUserServiceImpl implements OidcUserService {
                         + oidcUserPreferredUsername
                         + " does not match internal user ");
             }
-        } else if (existUserByEmail) {
-
-            if (allowedLinkingUsersByEmail) {
-                return linkUser(oidcUserEmail, oidcUserSub);
-            } else {
-                throw new ExternalUserCanNotBeLinked("External user "
-                        + oidcUserPreferredUsername
-                        + " cannot be linked to an internal user ");
-            }
-        } else {
+        }  else {
             return registerNewUser(oidcUser);
         }
     }
@@ -122,10 +113,32 @@ public class OidcUserServiceImpl implements OidcUserService {
         return newUser;
     }
 
-    private User linkUser(String email, String samlToken) {
+    @Override
+    public boolean externalUserRequiredLinking(OidcUser oidcUser) {
+
+        String oidcUserSub = oidcUser.getAttribute("sub");
+        String oidcUserEmail = oidcUser.getAttribute("email");
+        String oidcUserPreferredUsername = oidcUser.getAttribute("preferred_username");
+
+        boolean existUserBySamlToken = userService
+                .existsBySamlToken(oidcUserSub);
+        boolean existUserByUsernameAsSamlToken = userService
+                .existsBySamlToken(oidcUserPreferredUsername);
+        boolean existUserByEmail = userService
+                .existsByEmail(oidcUserEmail);
+
+        if(existUserBySamlToken || existUserByUsernameAsSamlToken) {
+            return false;
+        }else return existUserByEmail;
+    }
+
+    @Override
+    public User linkUser(String email, String samlToken, String firstName, String lastName) {
 
         User user = userService.findByEmail(email);
         user.setSamlToken(samlToken);
+        user.setFirstname(firstName);
+        user.setLastname(lastName);
 
         userService.update(user);
         return user;
-- 
GitLab


From 646aa72f10da9a07b129d37b6cab220a68561a17 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 10:47:53 +0200
Subject: [PATCH 5/8] added new endpoint

---
 .../portal/api/auth/OIDCAuthController.java   | 101 +++++++++++++++++-
 1 file changed, 99 insertions(+), 2 deletions(-)

diff --git a/src/main/java/net/geant/nmaas/portal/api/auth/OIDCAuthController.java b/src/main/java/net/geant/nmaas/portal/api/auth/OIDCAuthController.java
index 1b006bc26..16512e829 100644
--- a/src/main/java/net/geant/nmaas/portal/api/auth/OIDCAuthController.java
+++ b/src/main/java/net/geant/nmaas/portal/api/auth/OIDCAuthController.java
@@ -1,25 +1,38 @@
 package net.geant.nmaas.portal.api.auth;
 
+import com.google.common.collect.ImmutableSet;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
+import net.geant.nmaas.portal.api.exception.AuthenticationException;
 import net.geant.nmaas.portal.api.exception.ExternalUserCanNotBeLinked;
 import net.geant.nmaas.portal.api.exception.ExternalUserMatchException;
+import net.geant.nmaas.portal.api.exception.SignupException;
 import net.geant.nmaas.portal.api.security.JWTTokenService;
+import net.geant.nmaas.portal.exceptions.UndergoingMaintenanceException;
+import net.geant.nmaas.portal.persistent.entity.Role;
 import net.geant.nmaas.portal.persistent.entity.User;
+import net.geant.nmaas.portal.persistent.entity.UserRole;
+import net.geant.nmaas.portal.service.ConfigurationManager;
 import net.geant.nmaas.portal.service.DomainService;
 import net.geant.nmaas.portal.service.OidcUserService;
 import net.geant.nmaas.portal.service.UserLoginRegisterService;
+import net.geant.nmaas.portal.service.UserService;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpHeaders;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.view.RedirectView;
 
+import static java.lang.String.format;
+
 
 @RestController
 @RequiredArgsConstructor
@@ -33,18 +46,83 @@ public class OIDCAuthController {
 
     private final UserLoginRegisterService loginRegisterService;
 
+    private final UserService userService;
+
+    private final PasswordEncoder passwordEncoder;
 
     private final DomainService domains;
 
+    private final ConfigurationManager configurationManager;
+
+
     @Value("${portal.address}")
     private String portalAddress;
     @Value("${spring.security.oauth2.client.provider.my-oidc.issuer-uri:http://localhost:8080/realms/geant}")
     private String oidcAddress;
 
 
+    @PostMapping("api/oidc/link")
+    public UserOidcToken oidcLinkedSuccess(@RequestBody final OidcLogin oidcLogin, HttpServletRequest request) {
+
+        User user = userService.findByEmail(oidcLogin.email());
+        try {
+            validate(
+                    oidcLogin.email(),
+                    oidcLogin.password(),
+                    user.getPassword(),
+                    user.isEnabled());
+        } catch (AuthenticationException ae) {
+            this.loginRegisterService.registerNewFailedLogin(
+                    user,
+                    request.getHeader(HttpHeaders.HOST),
+                    request.getHeader(HttpHeaders.USER_AGENT),
+                    BasicAuthController.getClientIpAddr(request)
+            );
+            throw new AuthenticationException(ae.getMessage());
+        }
+        checkUserApprovals(user);
+        if (
+                configurationManager.getConfiguration().isMaintenance()
+                        && user.getRoles().stream().noneMatch(
+                        value -> value.getRole().equals(Role.ROLE_SYSTEM_ADMIN)
+                )
+        ) {
+            throw new UndergoingMaintenanceException("Application is undergoing maintenance right now");
+        }
+        this.loginRegisterService.registerNewSuccessfulLogin(
+                user,
+                request.getHeader(HttpHeaders.HOST),
+                request.getHeader(HttpHeaders.USER_AGENT),
+                BasicAuthController.getClientIpAddr(request)
+        );
+
+        User linkedUser = oidcUserService.linkUser(
+                oidcLogin.email(),
+                oidcLogin.uuid(),
+                oidcLogin.firstName(),
+                oidcLogin.lastName()
+        );
+
+        return new UserOidcToken(
+                jwtTokenService.getToken(linkedUser),
+                jwtTokenService.getRefreshToken(linkedUser),
+                oidcLogin.oidcToken()
+        );
+
+
+    }
+
     @GetMapping("/api/oidc/success")
     public RedirectView oidcLoginSuccess(@AuthenticationPrincipal OidcUser oidcUser, HttpServletRequest request) {
 
+        if (oidcUserService.externalUserRequiredLinking(oidcUser)) {
+            String linkingRedirectUrl = portalAddress
+                    + "/login-linking?oidc_token="
+                    + oidcUser.getIdToken().getTokenValue();
+            return new RedirectView(linkingRedirectUrl);
+        }
+
+
         try {
             User user = oidcUserService.checkUser(oidcUser);
             String redirectUrl = portalAddress
@@ -72,7 +150,6 @@ public class OIDCAuthController {
         }
     }
 
-
     @GetMapping("/api/oidc/logout/{oidcToken}")
     public RedirectView logout(@PathVariable String oidcToken) {
 
@@ -80,6 +157,26 @@ public class OIDCAuthController {
         return new RedirectView(logoutUrl + "?id_token_hint=" + oidcToken);
 
     }
-}
 
 
+    void validate(String email, String providedPassword, String actualPassword, boolean isEnabled) {
+        validateConditionAndLogMessage(email == null || providedPassword == null,
+                format("Login failed: missing credentials%s", email != null ? (format(" (email: %s)", email)) : ""));
+        validateConditionAndLogMessage(!isEnabled, format("Login failed: user [%s] is not active", email));
+        validateConditionAndLogMessage(!passwordEncoder.matches(providedPassword, actualPassword), format("Login failed: user [%s] entered incorrect password", email));
+    }
+
+    void checkUserApprovals(User user) {
+        if (!user.isTermsOfUseAccepted() || !user.isPrivacyPolicyAccepted()) {
+            log.info(format("Check during login: Terms of Use or Privacy Policy were not accepted by user [%s]", user.getUsername()));
+            user.setNewRoles(ImmutableSet.of(new UserRole(user, domains.getGlobalDomain().orElseThrow(SignupException::new), Role.ROLE_NOT_ACCEPTED)));
+        }
+    }
+
+    private void validateConditionAndLogMessage(boolean loginCondition, String errorMessage) {
+        if (loginCondition) {
+            log.info(errorMessage);
+            throw new AuthenticationException("Invalid Credentials");
+        }
+    }
+}
\ No newline at end of file
-- 
GitLab


From 80e9c8094e897e3d7239d3ff744f3d0e111e26ec Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 10:50:50 +0200
Subject: [PATCH 6/8] removed expired test

---
 .../portal/service/impl/OidcUserServiceImplTest.java      | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/src/test/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImplTest.java b/src/test/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImplTest.java
index 17a88fd3e..bfc13ee3c 100644
--- a/src/test/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImplTest.java
+++ b/src/test/java/net/geant/nmaas/portal/service/impl/OidcUserServiceImplTest.java
@@ -88,13 +88,5 @@ class OidcUserServiceImplTest {
         assertThrows(ExternalUserMatchException.class, () -> oidcUserService.checkUser(oidcUser));
     }
 
-    @Test
-    void shouldThrowExceptionWhenLinkingNotAllowed() {
-
-        //when
-        when(userService.existsByEmail("test@example.com")).thenReturn(true);
-        //then
-        assertThrows(ExternalUserCanNotBeLinked.class, () -> oidcUserService.checkUser(oidcUser));
-    }
 
 }
\ No newline at end of file
-- 
GitLab


From bf06ee0b30f876b4d947661aa1b42aaa625bff94 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 11:22:30 +0200
Subject: [PATCH 7/8] added translate

---
 src/test/shell/data/i18n/en.json | 3 ++-
 src/test/shell/data/i18n/fr.json | 4 +++-
 src/test/shell/data/i18n/pl.json | 3 ++-
 3 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/test/shell/data/i18n/en.json b/src/test/shell/data/i18n/en.json
index c7ce98f03..b305c1296 100644
--- a/src/test/shell/data/i18n/en.json
+++ b/src/test/shell/data/i18n/en.json
@@ -95,7 +95,8 @@
   },
   "ACCOUNT_LINKING": {
     "HEADER": "Account linking",
-    "INFO": "You are trying to log in to an local account that already exists in our system with the email address you provided. Please enter your password to verify it's You and confirm linking of these accounts."
+    "INFO": "You are trying to log in to an local account that already exists in our system with the email address you provided. Please enter your password to verify it's You and confirm linking of these accounts.",
+    "CONFIRM": "Confirm"
   },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "nmaas test instance",
diff --git a/src/test/shell/data/i18n/fr.json b/src/test/shell/data/i18n/fr.json
index d997acb72..013fcd57f 100644
--- a/src/test/shell/data/i18n/fr.json
+++ b/src/test/shell/data/i18n/fr.json
@@ -95,7 +95,9 @@
   },
   "ACCOUNT_LINKING": {
     "HEADER": "Association de comptes",
-    "INFO": "Vous essayez de vous connecter à un compte local qui existe déjà dans notre système avec l'adresse e-mail que vous avez fournie. Veuillez entrer votre mot de passe pour vérifier votre identité et confirmer la liaison des comptes."
+    "INFO": "Vous essayez de vous connecter à un compte local qui existe déjà dans notre système avec l'adresse e-mail que vous avez fournie. Veuillez entrer votre mot de passe pour vérifier votre identité et confirmer la liaison des comptes.",
+    "CONFIRM": "Confirm"
+
   },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "Instance de test nmaas",
diff --git a/src/test/shell/data/i18n/pl.json b/src/test/shell/data/i18n/pl.json
index ad465245e..f9d315bab 100644
--- a/src/test/shell/data/i18n/pl.json
+++ b/src/test/shell/data/i18n/pl.json
@@ -95,7 +95,8 @@
   },
   "ACCOUNT_LINKING": {
     "HEADER": "Powiązanie kont",
-    "INFO": "Próbujesz zalogować się na lokalne konto, które już istnieje w naszym systemie dla podanego adresu e-mail. Wprowadź swoje hasło, aby potwierdzić swoją tożsamość i zatwierdzić połączenie kont."
+    "INFO": "Próbujesz zalogować się na lokalne konto, które już istnieje w naszym systemie dla podanego adresu e-mail. Wprowadź swoje hasło, aby potwierdzić swoją tożsamość i zatwierdzić połączenie kont.",
+    "CONFIRM": "Potwierdź"
   },
   "TEST_INSTANCE_MODAL": {
     "HEADER": "Instancja testowa nmaas",
-- 
GitLab


From d078898f870bd3b7b64bead6104ef402b7d3bad0 Mon Sep 17 00:00:00 2001
From: pkazimierowski <pkazimierowski@man.poznan.pl>
Date: Tue, 1 Apr 2025 14:07:08 +0200
Subject: [PATCH 8/8] init new test

---
 .../api/auth/OIDCAuthControllerTest.java      | 158 ++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100644 src/test/java/net/geant/nmaas/portal/api/auth/OIDCAuthControllerTest.java

diff --git a/src/test/java/net/geant/nmaas/portal/api/auth/OIDCAuthControllerTest.java b/src/test/java/net/geant/nmaas/portal/api/auth/OIDCAuthControllerTest.java
new file mode 100644
index 000000000..90b0cdce3
--- /dev/null
+++ b/src/test/java/net/geant/nmaas/portal/api/auth/OIDCAuthControllerTest.java
@@ -0,0 +1,158 @@
+package net.geant.nmaas.portal.api.auth;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.core.HttpHeaders;
+import net.geant.nmaas.portal.api.configuration.ConfigurationView;
+import net.geant.nmaas.portal.api.security.JWTTokenService;
+import net.geant.nmaas.portal.persistent.entity.Domain;
+import net.geant.nmaas.portal.persistent.entity.Role;
+import net.geant.nmaas.portal.persistent.entity.User;
+import net.geant.nmaas.portal.persistent.entity.UserRole;
+import net.geant.nmaas.portal.service.ConfigurationManager;
+import net.geant.nmaas.portal.service.DomainService;
+import net.geant.nmaas.portal.service.OidcUserService;
+import net.geant.nmaas.portal.service.UserLoginRegisterService;
+import net.geant.nmaas.portal.service.UserService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.servlet.view.RedirectView;
+
+import java.lang.reflect.Constructor;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class OIDCAuthControllerTest {
+
+    @InjectMocks
+    private OIDCAuthController oidcAuthController;
+
+    @Mock
+    private OidcUserService oidcUserService;
+    @Mock
+    private JWTTokenService jwtTokenService;
+    @Mock
+    private UserLoginRegisterService loginRegisterService;
+    @Mock
+    private UserService userService;
+    @Mock
+    private PasswordEncoder passwordEncoder;
+    @Mock
+    private DomainService domainService;
+    @Mock
+    private ConfigurationManager configurationManager;
+    @Mock
+    private HttpServletRequest request;
+
+    @Test
+    void shouldReturnOidcTokenOnSuccessfulLinking() throws Exception {
+        // Given
+        OidcLogin oidcLogin = new OidcLogin("user@example.com", "pass123", "uuid-123", "John", "Doe", "oidc-token");
+
+        Constructor<User> userConstructor = User.class.getDeclaredConstructor();
+        userConstructor.setAccessible(true);
+        User user = userConstructor.newInstance();
+
+        ReflectionTestUtils.setField(user, "password", "hashed-password");
+        ReflectionTestUtils.setField(user, "enabled", true);
+        ReflectionTestUtils.setField(user, "termsOfUseAccepted", true);
+        ReflectionTestUtils.setField(user, "privacyPolicyAccepted", true);
+
+        Constructor<Domain> domainConstructor = Domain.class.getDeclaredConstructor();
+        domainConstructor.setAccessible(true);
+        Domain domain = domainConstructor.newInstance();
+        ReflectionTestUtils.setField(domain, "name", "Global");
+
+        UserRole role = new UserRole(user, domain, Role.ROLE_USER);
+        ReflectionTestUtils.setField(user, "roles", List.of(role));
+
+        when(userService.findByEmail(any())).thenReturn(user);
+        when(passwordEncoder.matches(any(), any())).thenReturn(true);
+
+        ConfigurationView config = mock(ConfigurationView.class);
+        when(config.isMaintenance()).thenReturn(false);
+        when(configurationManager.getConfiguration()).thenReturn(config);
+
+        User linkedUser = userConstructor.newInstance();
+        when(oidcUserService.linkUser(any(), any(), any(), any())).thenReturn(linkedUser);
+        when(jwtTokenService.getToken(any())).thenReturn("jwt-token");
+        when(jwtTokenService.getRefreshToken(any())).thenReturn("refresh-token");
+
+        when(request.getHeader(HttpHeaders.HOST)).thenReturn("localhost");
+        when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn("JUnit");
+
+        // When
+        UserOidcToken result = oidcAuthController.oidcLinkedSuccess(oidcLogin, request);
+
+        // Then
+        assertEquals("jwt-token", result.token());
+        assertEquals("refresh-token", result.refreshToken());
+        assertEquals("uuid-123", result.oidcToken());
+
+        verify(loginRegisterService).registerNewSuccessfulLogin(eq(user), any(), any(), any());
+    }
+
+    @Test
+    void shouldRedirectToLoginSuccessIfOidcUserIsValid() throws Exception {
+        // given
+        OidcUser oidcUser = mock(OidcUser.class);
+        OidcIdToken idToken = mock(OidcIdToken.class);
+        when(idToken.getTokenValue()).thenReturn("oidc-token");
+        when(oidcUser.getIdToken()).thenReturn(idToken);
+
+        when(oidcUserService.externalUserRequiredLinking(any())).thenReturn(false);
+
+        Constructor<User> userConstructor = User.class.getDeclaredConstructor();
+        userConstructor.setAccessible(true);
+        User user = userConstructor.newInstance();
+
+        when(oidcUserService.checkUser(any())).thenReturn(user);
+        when(jwtTokenService.getToken(any())).thenReturn("jwt-token");
+        when(jwtTokenService.getRefreshToken(any())).thenReturn("refresh-token");
+
+        when(request.getHeader(HttpHeaders.HOST)).thenReturn("localhost");
+        when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn("JUnit");
+
+        // when
+        RedirectView result = oidcAuthController.oidcLoginSuccess(oidcUser, request);
+
+        // then
+        assertTrue(result.getUrl().contains("login-success"));
+        assertTrue(result.getUrl().contains("token=jwt-token"));
+        assertTrue(result.getUrl().contains("refresh_token=refresh-token"));
+        assertTrue(result.getUrl().contains("oidc_token=oidc-token"));
+
+        verify(loginRegisterService).registerNewSuccessfulLogin(eq(user), any(), any(), any());
+    }
+
+    @Test
+    void shouldRedirectToLinkingIfExternalUserRequiresLinking() {
+        // given
+        OidcUser oidcUser = mock(OidcUser.class);
+        OidcIdToken idToken = mock(OidcIdToken.class);
+        when(idToken.getTokenValue()).thenReturn("oidc-token");
+        when(oidcUser.getIdToken()).thenReturn(idToken);
+
+        when(oidcUserService.externalUserRequiredLinking(any())).thenReturn(true);
+
+        // when
+        RedirectView result = oidcAuthController.oidcLoginSuccess(oidcUser, request);
+
+        // then
+        assertTrue(result.getUrl().contains("/login-linking?oidc_token=oidc-token"));
+    }
+}
-- 
GitLab