22
33import java .net .URI ;
44import java .text .SimpleDateFormat ;
5+ import java .time .Instant ;
6+ import java .time .temporal .ChronoUnit ;
57import java .util .ArrayList ;
68import java .util .Calendar ;
79import java .util .Collections ;
810import java .util .Date ;
911import java .util .List ;
1012import java .util .Locale ;
1113import java .util .Map ;
14+ import java .util .UUID ;
15+ import java .util .concurrent .TimeUnit ;
1216import java .util .stream .Collectors ;
1317
1418import javax .servlet .http .HttpServletRequest ;
3438import com .auth0 .jwt .algorithms .Algorithm ;
3539import com .auth0 .jwt .interfaces .DecodedJWT ;
3640import com .fasterxml .jackson .databind .ObjectMapper ;
41+ import com .hazelcast .map .IMap ;
3742
3843import it .eng .knowage .monitor .IKnowageMonitor ;
3944import it .eng .knowage .monitor .KnowageMonitorFactory ;
8388import it .eng .spagobi .tenant .TenantManager ;
8489import it .eng .spagobi .tools .dataset .notifier .fiware .OAuth2Utils ;
8590import it .eng .spagobi .utilities .exceptions .SpagoBIRuntimeException ;
91+ import it .eng .spagobi .utilities .locks .DistributedLockFactory ;
8692
8793@ Path ("/login" )
8894public class LoginResource extends AbstractSpagoBIResource {
@@ -185,6 +191,16 @@ public Response login(@Context HttpServletRequest req, Map<String, String> paylo
185191 }
186192 }
187193
194+ /**
195+ * Verifies MFA code and completes login process
196+ * Accepts MFA token and TOTP code for user authentication with multi-factor authentication
197+ * Supports first-time MFA setup with secret provisioning
198+ *
199+ * @param req The HTTP servlet request
200+ * @param payload Request payload containing tokenMfa (JWT MFA token), code (TOTP code), and optional secret
201+ * @return Response with login token on success, or error response on failure
202+ * @throws Exception if verification or login process fails
203+ */
188204 @ POST
189205 @ Path ("/verifyMfa" )
190206 @ PublicService
@@ -288,9 +304,8 @@ public Response verifyMfaCode(@Context HttpServletRequest req, Map<String, Strin
288304
289305 List <String > roleNames = user .getSbiExtUserRoleses ().stream ().map (SbiExtRoles ::getName ).collect (Collectors .toCollection (ArrayList <String >::new ));
290306 spagoBIUserProfile .setRoles (roleNames .toArray (String []::new ));
291- // String jwtToken = JWTSsoService.userCompleteJwtToken(userId, roleNames, UserUtilities.readFunctionality(spagoBIUserProfile),
292- // expiresAt);
293- String jwtToken = JWTSsoService .userIdAndRole2jwtToken (userId , roleNames , expiresAt );
307+
308+ String jwtToken = JWTSsoService .userId2jwtToken (userId , expiresAt );
294309 spagoBIUserProfile .setUniqueIdentifier (jwtToken );
295310
296311 IEngUserProfile profile = loadUserProfile (jwtToken , req );
@@ -462,6 +477,128 @@ public Response validateOIDCIdToken(@Context HttpServletRequest req, Map<String,
462477 }
463478 }
464479
480+ @ POST
481+ @ Path ("/refreshToken" )
482+ @ PublicService
483+ public Response refreshToken (@ Context HttpServletRequest req , Map <String , String > payload ) {
484+
485+ try {
486+ String refreshToken = payload .get ("refreshToken" );
487+ if (StringUtils .isBlank (refreshToken )) {
488+ return Response .status (Response .Status .BAD_REQUEST ).entity (Map .of ("error" , "Missing refreshToken" )).build ();
489+ }
490+
491+ logger .debug ("Refreshing access token using refreshToken" );
492+
493+ // 1) Get the Hazelcast distributed map
494+ IMap <String , String > tokenMap = DistributedLockFactory .getDistributedMap (SpagoBIConstants .DISTRIBUTED_MAP_INSTANCE_NAME ,
495+ SpagoBIConstants .DISTRIBUTED_MAP_FOR_REFRESH_TOKEN );
496+
497+ // 2) Check if refresh token exists (revoked or never issued?)
498+ String userIdFromCache = tokenMap .get (refreshToken );
499+ if (userIdFromCache == null ) {
500+ logger .warn ("Refresh token not found or revoked" );
501+ return Response .status (Response .Status .UNAUTHORIZED ).entity (Map .of ("error" , "Invalid or expired refreshToken" )).build ();
502+ }
503+
504+ // 3) Verify JWT refresh token signature
505+ Algorithm algorithm = ALGORITHM_FACTORY .getAlgorithm ();
506+ DecodedJWT decoded ;
507+
508+ try {
509+ decoded = JWT .require (algorithm ).withIssuer (JWTSsoService .KNOWAGE_ISSUER ).build ().verify (refreshToken );
510+ } catch (Exception e ) {
511+ logger .error ("Invalid refresh token signature" , e );
512+ return Response .status (Response .Status .UNAUTHORIZED ).entity (Map .of ("error" , "Invalid refreshToken" )).build ();
513+ }
514+
515+ // 4) Extract data from refresh token
516+ String userIdFromJWT = decoded .getClaim ("user_id" ).asString ();
517+ Date expiresAt = decoded .getExpiresAt ();
518+ String deviceFromJWT = decoded .getClaim ("device" ).asString ();
519+
520+ // 5) Verify user ID consistency
521+ if (!userIdFromJWT .equals (userIdFromCache )) {
522+ logger .error ("Refresh token mismatch: JWT user != Hazelcast user" );
523+ return Response .status (Response .Status .UNAUTHORIZED ).entity (Map .of ("error" , "Invalid refreshToken" )).build ();
524+ }
525+
526+ // 6) Check token expiration
527+ if (expiresAt .before (new Date ())) {
528+ logger .warn ("Expired refresh token" );
529+ return Response .status (Response .Status .UNAUTHORIZED ).entity (Map .of ("error" , "Expired refreshToken" )).build ();
530+ }
531+
532+ // 7) (Optional but recommended) Verify DEVICE / USER-AGENT
533+ String currentUA = req .getHeader ("User-Agent" );
534+ if (deviceFromJWT != null && !deviceFromJWT .equals (currentUA )) {
535+ logger .error ("Refresh token used from different device" );
536+ return Response .status (Response .Status .UNAUTHORIZED ).entity (Map .of ("error" , "RefreshToken used from different device" )).build ();
537+ }
538+
539+ // 8) At this point the refresh token is valid → generate new access token
540+ String newAccessToken = JWTSsoService .userId2jwtToken (userIdFromJWT );
541+
542+ // 9) (Optional) Generate a new refresh token and revoke the previous one
543+ // - Remove old token
544+ tokenMap .remove (refreshToken );
545+ // - Generate and save new token
546+ String newRefreshToken = storeTokenInHazelcast (userIdFromJWT , req );
547+
548+ logger .info ("Successfully refreshed access token for user: " + userIdFromJWT );
549+
550+ return Response .ok (Map .of ("token" , newAccessToken , "refresh_token" , newRefreshToken )).build ();
551+
552+ } catch (Exception e ) {
553+ logger .error ("Error during refresh token process" , e );
554+ return Response .status (Response .Status .INTERNAL_SERVER_ERROR ).entity (Map .of ("error" , "Internal server error" )).build ();
555+ }
556+ }
557+
558+
559+ /**
560+ * Generates a new refresh token and stores it in Hazelcast distributed cache
561+ * The refresh token includes JWT ID, user ID, issue date, expiration time, and device information
562+ * Tokens are stored with TTL matching the configured refresh expiry period
563+ *
564+ * @param userId The unique identifier of the user
565+ * @param req The HTTP servlet request (used to extract User-Agent header)
566+ * @return The generated refresh token as a JWT string, or null if generation fails
567+ */
568+ private String storeTokenInHazelcast (String userId , HttpServletRequest req ) {
569+ try {
570+ logger .debug ("Generating and storing refresh token for user: " + userId );
571+
572+ long refreshExpiryHours = 10L ;
573+
574+
575+ Algorithm algorithm = ALGORITHM_FACTORY .getAlgorithm ();
576+ String jti = UUID .randomUUID ().toString ();
577+ Instant now = Instant .now ();
578+ Instant exp = now .plus (refreshExpiryHours , ChronoUnit .HOURS );
579+
580+ String userAgent = (req != null && req .getHeader ("User-Agent" ) != null ) ? req .getHeader ("User-Agent" ) : null ;
581+
582+ String refreshToken = JWT .create ().withIssuer (JWTSsoService .KNOWAGE_ISSUER )
583+ .withJWTId (jti ).withClaim ("user_id" , userId ).withIssuedAt (Date .from (now )).withExpiresAt (Date .from (exp ))
584+ .withClaim ("device" , userAgent ).sign (algorithm );
585+
586+ IMap <String , String > tokenMap = DistributedLockFactory .getDistributedMap (SpagoBIConstants .DISTRIBUTED_MAP_INSTANCE_NAME ,
587+ SpagoBIConstants .DISTRIBUTED_MAP_FOR_REFRESH_TOKEN );
588+
589+ tokenMap .put (refreshToken , userId , refreshExpiryHours , TimeUnit .HOURS );
590+
591+ logger .info (
592+ "Refresh token generated and stored in Hazelcast map : " + SpagoBIConstants .DISTRIBUTED_MAP_FOR_REFRESH_TOKEN + " for user : " + userId );
593+
594+ return refreshToken ;
595+
596+ } catch (Exception e ) {
597+ logger .error ("Error generating/storing refresh token in Hazelcast for user " + userId , e );
598+ return null ;
599+ }
600+ }
601+
465602 /**
466603 * Performs OAuth2 authentication logic
467604 */
@@ -501,6 +638,17 @@ private Response performOAuth2Login(HttpServletRequest req, IKnowageMonitor moni
501638 return completeLoginForKnowageFE (req , profile );
502639 }
503640
641+ /**
642+ * Exchanges OAuth2 authorization code for access token
643+ * Sends token request to OAuth2 provider endpoint and handles PKCE code verifier if provided
644+ * Includes client authentication for CONFIDENTIAL clients
645+ *
646+ * @param cfg OAuth2 configuration containing client details and provider URLs
647+ * @param code Authorization code received from OAuth2 provider
648+ * @param codeVerifier PKCE code verifier (optional, for public clients)
649+ * @return TokenResponse containing access token, ID token, refresh token, and expiration info
650+ * @throws Exception if token exchange fails or provider is unreachable
651+ */
504652 private TokenResponse exchangeCodeForToken (OAuth2Config cfg , String code , String codeVerifier ) throws Exception {
505653
506654 Client client = ClientBuilder .newBuilder ().connectTimeout (5 , java .util .concurrent .TimeUnit .SECONDS )
@@ -562,6 +710,16 @@ public String toString() {
562710 }
563711 }
564712
713+ /**
714+ * Checks if Multi-Factor Authentication (MFA) is required for the user
715+ * MFA is skipped for LDAP-based security implementations
716+ * MFA requirement is determined by tenant configuration
717+ *
718+ * @param req The HTTP servlet request
719+ * @param user The SbiUser entity to check
720+ * @return true if MFA is required and enabled for the user's tenant, false otherwise
721+ * @throws Exception if tenant or configuration lookup fails
722+ */
565723 private boolean checkCodeMfa (HttpServletRequest req , SbiUser user ) throws Exception {
566724
567725 String securityServiceSupplier = SingletonConfig .getInstance ().getConfigValue ("SPAGOBI.SECURITY.USER-PROFILE-FACTORY-CLASS.className" );
@@ -582,6 +740,13 @@ private boolean checkCodeMfa(HttpServletRequest req, SbiUser user) throws Except
582740 return true ;
583741 }
584742
743+ /**
744+ * Stores user profile in HTTP session and enriches it with PM (Portal Manager) information
745+ * Enrichment includes additional user context and profile attributes from request
746+ *
747+ * @param userProfile The UserProfile object to store in session
748+ * @param req The HTTP servlet request
749+ */
585750 private void storeProfileInSession (UserProfile userProfile , HttpServletRequest req ) {
586751 logger .debug ("IN" );
587752 // PM-int
@@ -593,6 +758,15 @@ private void storeProfileInSession(UserProfile userProfile, HttpServletRequest r
593758 }
594759
595760
761+ /**
762+ * Checks if user password needs to be changed based on security policies
763+ * Validates password expiration, age, disuse period, and other configured controls
764+ * Returns true if password change is mandatory, false if password is active
765+ *
766+ * @param user The SbiUser entity to validate
767+ * @return true if password change is required, false otherwise
768+ * @throws Exception if configuration lookup fails
769+ */
596770 private boolean checkPwd (SbiUser user ) throws Exception {
597771 logger .debug ("IN" );
598772 boolean toReturn = false ;
@@ -664,6 +838,13 @@ private boolean checkPwd(SbiUser user) throws Exception {
664838 return toReturn ;
665839 }
666840
841+ /**
842+ * Checks if user password was encrypted using the old SHA_SECRETPHRASE method (before version 7.2)
843+ * Passwords encrypted with this legacy method require immediate change
844+ *
845+ * @param user The SbiUser entity to check
846+ * @return true if password uses legacy encryption format, false otherwise
847+ */
667848 private boolean encriptedBefore72 (SbiUser user ) {
668849 return user .getPassword ().startsWith (Password .PREFIX_SHA_SECRETPHRASE_ENCRIPTING );
669850 }
@@ -838,7 +1019,6 @@ private Response completeLogin(HttpServletRequest req, IEngUserProfile profile)
8381019 aSession .close ();
8391020 }
8401021 }
841-
8421022 return Response .ok (Map .of ("token" , profile .getUserUniqueIdentifier ())).build ();
8431023
8441024 } finally {
@@ -868,7 +1048,7 @@ private Response completeLoginForKnowageFE(HttpServletRequest req, IEngUserProfi
8681048
8691049
8701050 URI redirectUri = URI
871- .create (System .getProperty ("JWT_KNOWAGE_VUE" , System .getenv ("JWT_KNOWAGE_VUE" )) + "login?authtoken =" + profile .getUserUniqueIdentifier ());
1051+ .create (System .getProperty ("JWT_KNOWAGE_VUE" , System .getenv ("JWT_KNOWAGE_VUE" )) + "login?authToken =" + profile .getUserUniqueIdentifier ());
8721052
8731053 return Response .seeOther (redirectUri ).build ();
8741054
@@ -935,4 +1115,4 @@ private void validateIdTokenSignature(DecodedJWT decodedJWT, String idToken) thr
9351115
9361116
9371117
938- }
1118+ }
0 commit comments