fixes
This commit is contained in:
@@ -398,7 +398,9 @@ public class ApplicationProperties {
|
||||
private boolean enableKeystore = true;
|
||||
private boolean enableKeyRotation = false;
|
||||
private boolean enableKeyCleanup = true;
|
||||
private int keyRetentionDays = 7;
|
||||
private int keyRetentionDays = 30;
|
||||
private int tokenExpiryMinutes = 1440;
|
||||
private int allowedClockSkewSeconds = 60;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -64,7 +64,9 @@ security:
|
||||
persistence: true # Set to 'true' to enable JWT key store
|
||||
enableKeyRotation: true # Set to 'true' to enable key pair rotation
|
||||
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
|
||||
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
|
||||
keyRetentionDays: 30 # Number of days to retain old keys. The default is 30 days.
|
||||
tokenExpiryMinutes: 1440 # JWT access token lifetime in minutes (1 day).
|
||||
allowedClockSkewSeconds: 60 # Allowed JWT validation clock skew in seconds to tolerate small client/server time drift.
|
||||
validation: # PDF signature validation settings
|
||||
trust:
|
||||
serverAsAnchor: true # Trust server certificate as anchor for PDF signatures (if configured and self-signed or CA)
|
||||
|
||||
+24
-4
@@ -34,6 +34,7 @@ import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.model.api.user.MfaCodeRequest;
|
||||
import stirling.software.proprietary.security.model.api.user.UsernameAndPassMfa;
|
||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
@@ -180,7 +181,12 @@ public class AuthController {
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"user", buildUserResponse(user),
|
||||
"session", Map.of("access_token", token, "expires_in", 3600)));
|
||||
"session",
|
||||
Map.of(
|
||||
"access_token",
|
||||
token,
|
||||
"expires_in",
|
||||
getTokenExpirySeconds())));
|
||||
|
||||
} catch (UsernameNotFoundException e) {
|
||||
String username = request.getUsername();
|
||||
@@ -272,8 +278,7 @@ public class AuthController {
|
||||
.body(Map.of("error", "No token found"));
|
||||
}
|
||||
|
||||
jwtService.validateToken(token);
|
||||
String username = jwtService.extractUsername(token);
|
||||
String username = jwtService.extractUsernameAllowExpired(token);
|
||||
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
User user = (User) userDetails;
|
||||
@@ -289,8 +294,17 @@ public class AuthController {
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"user", buildUserResponse(user),
|
||||
"session", Map.of("access_token", newToken, "expires_in", 3600)));
|
||||
"session",
|
||||
Map.of(
|
||||
"access_token",
|
||||
newToken,
|
||||
"expires_in",
|
||||
getTokenExpirySeconds())));
|
||||
|
||||
} catch (AuthenticationFailureException e) {
|
||||
log.error("Token refresh error", e);
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(Map.of("error", "Token refresh failed"));
|
||||
} catch (Exception e) {
|
||||
log.error("Token refresh error", e);
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
@@ -532,6 +546,12 @@ public class AuthController {
|
||||
return userMap;
|
||||
}
|
||||
|
||||
private long getTokenExpirySeconds() {
|
||||
int configuredMinutes = securityProperties.getJwt().getTokenExpiryMinutes();
|
||||
int expiryMinutes = configuredMinutes > 0 ? configuredMinutes : 720;
|
||||
return expiryMinutes * 60L;
|
||||
}
|
||||
|
||||
private ResponseEntity<?> ensureWebAuth(User user) {
|
||||
if (!AuthenticationType.WEB.name().equalsIgnoreCase(user.getAuthenticationType())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
|
||||
+49
-4
@@ -30,6 +30,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.model.JwtVerificationKey;
|
||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
@@ -39,17 +40,21 @@ import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrin
|
||||
public class JwtService implements JwtServiceInterface {
|
||||
|
||||
private static final String ISSUER = "https://stirling.com";
|
||||
private static final long EXPIRATION = 43200000;
|
||||
private static final long ONE_MINUTE_MILLIS = 60_000L;
|
||||
private static final int DEFAULT_EXPIRY_MINUTES = 720;
|
||||
|
||||
private final KeyPersistenceServiceInterface keyPersistenceService;
|
||||
private final boolean v2Enabled;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
|
||||
@Autowired
|
||||
public JwtService(
|
||||
@Qualifier("v2Enabled") boolean v2Enabled,
|
||||
KeyPersistenceServiceInterface keyPersistenceService) {
|
||||
KeyPersistenceServiceInterface keyPersistenceService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.v2Enabled = v2Enabled;
|
||||
this.keyPersistenceService = keyPersistenceService;
|
||||
this.securityProperties = applicationProperties.getSecurity();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,7 +91,8 @@ public class JwtService implements JwtServiceInterface {
|
||||
.subject(username)
|
||||
.issuer(ISSUER)
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + EXPIRATION))
|
||||
.expiration(
|
||||
new Date(System.currentTimeMillis() + getExpirationMillis()))
|
||||
.signWith(keyPair.getPrivate(), Jwts.SIG.RS256);
|
||||
|
||||
String keyId = activeKey.getKeyId();
|
||||
@@ -114,12 +120,23 @@ public class JwtService implements JwtServiceInterface {
|
||||
return extractClaim(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extractUsernameAllowExpired(String token) {
|
||||
return extractClaim(token, Claims::getSubject, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> extractClaims(String token) {
|
||||
Claims claims = extractAllClaims(token);
|
||||
return new HashMap<>(claims);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> extractClaimsAllowExpired(String token) {
|
||||
Claims claims = extractAllClaims(token, true);
|
||||
return new HashMap<>(claims);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTokenExpired(String token) {
|
||||
return extractExpiration(token).before(new Date());
|
||||
@@ -130,11 +147,21 @@ public class JwtService implements JwtServiceInterface {
|
||||
}
|
||||
|
||||
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = extractAllClaims(token);
|
||||
final Claims claims = extractAllClaims(token, false);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
private <T> T extractClaim(
|
||||
String token, Function<Claims, T> claimsResolver, boolean allowExpired) {
|
||||
final Claims claims = extractAllClaims(token, allowExpired);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
private Claims extractAllClaims(String token) {
|
||||
return extractAllClaims(token, false);
|
||||
}
|
||||
|
||||
private Claims extractAllClaims(String token, boolean allowExpired) {
|
||||
try {
|
||||
String keyId = extractKeyId(token);
|
||||
KeyPair keyPair;
|
||||
@@ -181,6 +208,7 @@ public class JwtService implements JwtServiceInterface {
|
||||
|
||||
return Jwts.parser()
|
||||
.verifyWith(keyPair.getPublic())
|
||||
.clockSkewSeconds(getAllowedClockSkewSeconds())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
@@ -191,6 +219,9 @@ public class JwtService implements JwtServiceInterface {
|
||||
log.warn("Invalid token: {}", e.getMessage());
|
||||
throw new AuthenticationFailureException("Invalid token", e);
|
||||
} catch (ExpiredJwtException e) {
|
||||
if (allowExpired) {
|
||||
return e.getClaims();
|
||||
}
|
||||
log.warn("The token has expired: {}", e.getMessage());
|
||||
throw new AuthenticationFailureException("The token has expired", e);
|
||||
} catch (UnsupportedJwtException e) {
|
||||
@@ -210,6 +241,7 @@ public class JwtService implements JwtServiceInterface {
|
||||
keyPersistenceService.decodePublicKey(activeKey.getVerifyingKey());
|
||||
return Jwts.parser()
|
||||
.verifyWith(publicKey)
|
||||
.clockSkewSeconds(getAllowedClockSkewSeconds())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
@@ -230,6 +262,7 @@ public class JwtService implements JwtServiceInterface {
|
||||
verificationKey.getVerifyingKey());
|
||||
return Jwts.parser()
|
||||
.verifyWith(publicKey)
|
||||
.clockSkewSeconds(getAllowedClockSkewSeconds())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
@@ -276,6 +309,7 @@ public class JwtService implements JwtServiceInterface {
|
||||
(String)
|
||||
Jwts.parser()
|
||||
.verifyWith(signingKey)
|
||||
.clockSkewSeconds(getAllowedClockSkewSeconds())
|
||||
.build()
|
||||
.parse(token)
|
||||
.getHeader()
|
||||
@@ -286,4 +320,15 @@ public class JwtService implements JwtServiceInterface {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private long getExpirationMillis() {
|
||||
int configuredMinutes = securityProperties.getJwt().getTokenExpiryMinutes();
|
||||
int expiryMinutes = configuredMinutes > 0 ? configuredMinutes : DEFAULT_EXPIRY_MINUTES;
|
||||
return expiryMinutes * ONE_MINUTE_MILLIS;
|
||||
}
|
||||
|
||||
private long getAllowedClockSkewSeconds() {
|
||||
int configuredSeconds = securityProperties.getJwt().getAllowedClockSkewSeconds();
|
||||
return configuredSeconds >= 0 ? configuredSeconds : 60L;
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -41,6 +41,15 @@ public interface JwtServiceInterface {
|
||||
*/
|
||||
String extractUsername(String token);
|
||||
|
||||
/**
|
||||
* Extract username from JWT token while allowing expired tokens. Signature and token structure
|
||||
* must still be valid.
|
||||
*
|
||||
* @param token the JWT token
|
||||
* @return username extracted from token
|
||||
*/
|
||||
String extractUsernameAllowExpired(String token);
|
||||
|
||||
/**
|
||||
* Extract all claims from JWT token
|
||||
*
|
||||
@@ -49,6 +58,15 @@ public interface JwtServiceInterface {
|
||||
*/
|
||||
Map<String, Object> extractClaims(String token);
|
||||
|
||||
/**
|
||||
* Extract all claims from JWT token while allowing expired tokens. Signature and token
|
||||
* structure must still be valid.
|
||||
*
|
||||
* @param token the JWT token
|
||||
* @return map of claims
|
||||
*/
|
||||
Map<String, Object> extractClaimsAllowExpired(String token);
|
||||
|
||||
/**
|
||||
* Check if token is expired
|
||||
*
|
||||
|
||||
@@ -542,7 +542,14 @@ export class AuthService {
|
||||
}
|
||||
);
|
||||
|
||||
const { token } = response.data;
|
||||
const token = response.data?.session?.access_token ?? response.data?.token;
|
||||
|
||||
if (!token) {
|
||||
console.error('[Desktop AuthService] Refresh response missing token payload');
|
||||
this.setAuthStatus('unauthenticated', null);
|
||||
await this.logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save token to all storage locations
|
||||
await this.saveTokenEverywhere(token);
|
||||
|
||||
Reference in New Issue
Block a user