Skip to content

Commit 0f8c6c8

Browse files
committed
Fix flow login
1 parent bd98fc6 commit 0f8c6c8

4 files changed

Lines changed: 202 additions & 29 deletions

File tree

knowage-core/src/main/java/it/eng/spagobi/commons/services/LoginConfigResource.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import it.eng.knowage.monitor.KnowageMonitorFactory;
1818
import it.eng.spagobi.api.AbstractSpagoBIResource;
1919
import it.eng.spagobi.commons.SingletonConfig;
20+
import it.eng.spagobi.security.OAuth2.OAuth2Config;
2021
import it.eng.spagobi.services.rest.annotations.PublicService;
2122

2223
@Path("/loginconfig")
@@ -36,13 +37,24 @@ public Response getLoginConfig(@Context HttpServletRequest req) {
3637
Map<String, Object> item = new HashMap<>();
3738
Object ssoActiveValue = config.getConfigValue("SPAGOBI_SSO.ACTIVE");
3839
boolean ssoActive = Boolean.parseBoolean(String.valueOf(ssoActiveValue));
40+
String oauth2FlowType = Optional.ofNullable(System.getProperty("oauth2_flow_type", System.getenv("OAUTH2_FLOW_TYPE"))).orElse("");
3941
item.put("ssoActive", ssoActive);
4042
item.put("defaultLanguage", config.getConfigValue("SPAGOBI.LANGUAGE_SUPPORTED.LANGUAGE.default"));
41-
item.put("oauth2FlowType", Optional.ofNullable(System.getProperty("oauth2_flow_type", System.getenv("OAUTH2_FLOW_TYPE"))).orElse(""));
43+
item.put("oauth2FlowType", oauth2FlowType);
4244

4345
item.put("JWT_LABEL", System.getProperty("JWT_LABEL", System.getenv("JWT_LABEL")));
4446
item.put("JWT_SESSION_STORAGE", System.getProperty("JWT_SESSION_STORAGE", System.getenv("JWT_SESSION_STORAGE")));
4547

48+
if (oauth2FlowType != null && !oauth2FlowType.equalsIgnoreCase("NONE")) {
49+
OAuth2Config oauth2Config = OAuth2Config.getInstance();
50+
item.put("authorizeUrl", oauth2Config.getAuthorizeUrl());
51+
item.put("clientId", oauth2Config.getClientId());
52+
item.put("redirectUrl", oauth2Config.getRedirectUrl());
53+
item.put("scopes", oauth2Config.getScopes());
54+
item.put("ssoProvider", "oidc");
55+
56+
}
57+
4658
monitor.stop();
4759
return Response.ok(Map.of("items", List.of(item))).build();
4860
} catch (Exception e) {

knowage-core/src/main/java/it/eng/spagobi/commons/services/LoginResource.java

Lines changed: 186 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
import java.net.URI;
44
import java.text.SimpleDateFormat;
5+
import java.time.Instant;
6+
import java.time.temporal.ChronoUnit;
57
import java.util.ArrayList;
68
import java.util.Calendar;
79
import java.util.Collections;
810
import java.util.Date;
911
import java.util.List;
1012
import java.util.Locale;
1113
import java.util.Map;
14+
import java.util.UUID;
15+
import java.util.concurrent.TimeUnit;
1216
import java.util.stream.Collectors;
1317

1418
import javax.servlet.http.HttpServletRequest;
@@ -34,6 +38,7 @@
3438
import com.auth0.jwt.algorithms.Algorithm;
3539
import com.auth0.jwt.interfaces.DecodedJWT;
3640
import com.fasterxml.jackson.databind.ObjectMapper;
41+
import com.hazelcast.map.IMap;
3742

3843
import it.eng.knowage.monitor.IKnowageMonitor;
3944
import it.eng.knowage.monitor.KnowageMonitorFactory;
@@ -83,6 +88,7 @@
8388
import it.eng.spagobi.tenant.TenantManager;
8489
import it.eng.spagobi.tools.dataset.notifier.fiware.OAuth2Utils;
8590
import it.eng.spagobi.utilities.exceptions.SpagoBIRuntimeException;
91+
import it.eng.spagobi.utilities.locks.DistributedLockFactory;
8692

8793
@Path("/login")
8894
public 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+
}

knowage-core/src/main/java/it/eng/spagobi/commons/services/LogoutResource.java

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import it.eng.spagobi.api.AbstractSpagoBIResource;
2222
import it.eng.spagobi.commons.SingletonConfig;
2323
import it.eng.spagobi.commons.bo.UserProfile;
24-
import it.eng.spagobi.commons.constants.SpagoBIConstants;
2524
import it.eng.spagobi.commons.utilities.AuditLogUtilities;
2625

2726
@Path("/logout")
@@ -51,29 +50,10 @@ public Response logout(@Context HttpServletRequest request, @Context HttpServlet
5150
dto.setDescription("Logout");
5251
PrivacyManagerClient.getInstance().sendMessage(dto);
5352

54-
String redirectUrl = null;
55-
56-
String active = SingletonConfig.getInstance().getConfigValue("SPAGOBI_SSO.ACTIVE");
57-
String strUsePublicUser = SingletonConfig.getInstance().getConfigValue(SpagoBIConstants.USE_PUBLIC_USER);
58-
Boolean usePublicUser = (strUsePublicUser == null) ? false : Boolean.valueOf(strUsePublicUser);
59-
60-
61-
if ((active == null || active.equalsIgnoreCase("false"))) {
62-
String context = request.getContextPath();
63-
if (Boolean.TRUE.equals(usePublicUser)) {
64-
context += "/servlet/AdapterHTTP?PAGE=LoginPage&NEW_SESSION=TRUE";
65-
redirectUrl = context;
66-
} else {
67-
redirectUrl = context;
68-
}
69-
} else if (active != null && active.equalsIgnoreCase("true")) {
70-
71-
redirectUrl = SingletonConfig.getInstance().getConfigValue("SPAGOBI_SSO.SECURITY_LOGOUT_URL");
72-
}
7353
request.getSession().invalidate();
7454

7555
Map<String, Object> responseBody = new HashMap<>();
76-
responseBody.put("redirectUrl", redirectUrl);
56+
responseBody.put("redirectUrl", SingletonConfig.getInstance().getConfigValue("SPAGOBI_SSO.SECURITY_LOGOUT_URL"));
7757
responseBody.put("urlEnginesInvalidate", getUrlEnginesInvalidate());
7858

7959
logger.debug("OUT");

knowage/src/main/webapp/oauth2/authorization_code/flow.jsp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
4848
grant_type: "authorization_code",
4949
redirect_uri: oauth2Config.redirectUrl,
5050
code: code,
51-
state: state
51+
state: state,
52+
client_secret: oauth2Config.clientSecret
5253
})
5354
})
5455
.then(response => response.json().then(data => ({ status: response.status, body: data })))

0 commit comments

Comments
 (0)