mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-27 14:49:23 +00:00
Adding JWTService and filter
This commit is contained in:
parent
d17d10b240
commit
65856e283f
@ -5,7 +5,11 @@
|
|||||||
"Bash(mkdir:*)",
|
"Bash(mkdir:*)",
|
||||||
"Bash(./gradlew:*)",
|
"Bash(./gradlew:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
"Bash(cat:*)"
|
"Bash(cat:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(rg:*)",
|
||||||
|
"Bash(strings:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
120
CLAUDE.md
Normal file
120
CLAUDE.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Essential Development Commands
|
||||||
|
|
||||||
|
### Build and Run
|
||||||
|
```bash
|
||||||
|
# Build the project
|
||||||
|
./gradlew clean build
|
||||||
|
|
||||||
|
# Run locally (includes JWT authentication work-in-progress)
|
||||||
|
./gradlew bootRun
|
||||||
|
|
||||||
|
# Run specific module
|
||||||
|
./gradlew :stirling-pdf:bootRun
|
||||||
|
|
||||||
|
# Build with security features enabled/disabled
|
||||||
|
DISABLE_ADDITIONAL_FEATURES=false ./gradlew clean build # enable security
|
||||||
|
DISABLE_ADDITIONAL_FEATURES=true ./gradlew clean build # disable security
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run unit tests
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
# Run comprehensive integration tests (builds all Docker versions and runs Cucumber tests)
|
||||||
|
./testing/test.sh
|
||||||
|
|
||||||
|
# Run Cucumber/BDD tests specifically
|
||||||
|
cd testing/cucumber && python -m behave
|
||||||
|
|
||||||
|
# Test web pages
|
||||||
|
cd testing && ./test_webpages.sh -f webpage_urls.txt -b http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality and Formatting
|
||||||
|
```bash
|
||||||
|
# Apply Java code formatting (required before commits)
|
||||||
|
./gradlew spotlessApply
|
||||||
|
|
||||||
|
# Check formatting compliance
|
||||||
|
./gradlew spotlessCheck
|
||||||
|
|
||||||
|
# Generate license report
|
||||||
|
./gradlew generateLicenseReport
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
```bash
|
||||||
|
# Build different Docker variants
|
||||||
|
docker build --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||||
|
docker build --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||||
|
DISABLE_ADDITIONAL_FEATURES=false docker build --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||||
|
|
||||||
|
# Use example Docker Compose configs
|
||||||
|
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
Stirling-PDF is a Spring Boot web application for PDF manipulation with the following key architectural components:
|
||||||
|
|
||||||
|
### Multi-Module Structure
|
||||||
|
- **stirling-pdf/**: Main application module with web UI and REST APIs
|
||||||
|
- **common/**: Shared utilities and common functionality
|
||||||
|
- **proprietary/**: Enterprise/security features (JWT authentication, audit, teams)
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Backend**: Spring Boot 3.5, Spring Security, Spring Data JPA
|
||||||
|
- **Frontend**: Thymeleaf templates, Bootstrap, vanilla JavaScript
|
||||||
|
- **PDF Processing**: Apache PDFBox 3.0, qpdf, LibreOffice
|
||||||
|
- **Authentication**: JWT-based stateless sessions (in development)
|
||||||
|
- **Database**: H2 (default), supports PostgreSQL/MySQL
|
||||||
|
- **Build**: Gradle with multi-project setup
|
||||||
|
|
||||||
|
### Current Development Context
|
||||||
|
The repository is on the `jwt-authentication` branch with work-in-progress changes to:
|
||||||
|
- JWT-based authentication system (`JWTService`, `JWTServiceInterface`)
|
||||||
|
- Stateless session management
|
||||||
|
- User model updates for JWT support
|
||||||
|
|
||||||
|
### Key Directories
|
||||||
|
- `stirling-pdf/src/main/java/stirling/software/SPDF/`: Main application code
|
||||||
|
- `controller/`: REST API endpoints and UI controllers
|
||||||
|
- `service/`: Business logic layer
|
||||||
|
- `config/`: Spring configuration classes
|
||||||
|
- `security/`: Authentication and authorization
|
||||||
|
- `stirling-pdf/src/main/resources/templates/`: Thymeleaf HTML templates
|
||||||
|
- `stirling-pdf/src/main/resources/static/`: CSS, JavaScript, and assets
|
||||||
|
- `proprietary/src/main/java/stirling/software/proprietary/`: Enterprise features
|
||||||
|
- `testing/`: Integration tests and Cucumber features
|
||||||
|
|
||||||
|
### Configuration Management
|
||||||
|
- Environment variables or `settings.yml` for runtime configuration
|
||||||
|
- Conditional feature compilation based on `DISABLE_ADDITIONAL_FEATURES`
|
||||||
|
- Multi-environment Docker configurations in `exampleYmlFiles/`
|
||||||
|
|
||||||
|
### API Design Patterns
|
||||||
|
- RESTful endpoints under `/api/v1/`
|
||||||
|
- OpenAPI/Swagger documentation available at `/swagger-ui/index.html`
|
||||||
|
- File upload/download handling with multipart form data
|
||||||
|
- Consistent error handling and response formats
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Environment Setup**: Set `DISABLE_ADDITIONAL_FEATURES=false` for full feature development
|
||||||
|
2. **Code Formatting**: Always run `./gradlew spotlessApply` before committing
|
||||||
|
3. **Testing Strategy**: Use `./testing/test.sh` for comprehensive testing before PRs
|
||||||
|
4. **Feature Development**: Follow the controller -> service -> template pattern
|
||||||
|
5. **Security**: JWT authentication is currently in development on this branch
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- The application supports conditional compilation of security features
|
||||||
|
- Translation files are in `messages_*.properties` format
|
||||||
|
- PDF processing operations are primarily stateless
|
||||||
|
- Docker is the recommended deployment method
|
||||||
|
- All text should be internationalized using translation keys
|
@ -109,13 +109,14 @@ public class ApplicationProperties {
|
|||||||
private InitialLogin initialLogin = new InitialLogin();
|
private InitialLogin initialLogin = new InitialLogin();
|
||||||
private OAUTH2 oauth2 = new OAUTH2();
|
private OAUTH2 oauth2 = new OAUTH2();
|
||||||
private SAML2 saml2 = new SAML2();
|
private SAML2 saml2 = new SAML2();
|
||||||
|
private JWT jwt = new JWT();
|
||||||
private int loginAttemptCount;
|
private int loginAttemptCount;
|
||||||
private long loginResetTimeMinutes;
|
private long loginResetTimeMinutes;
|
||||||
private String loginMethod = "all";
|
private String loginMethod = "all";
|
||||||
private String customGlobalAPIKey;
|
private String customGlobalAPIKey;
|
||||||
|
|
||||||
public Boolean isAltLogin() {
|
public Boolean isAltLogin() {
|
||||||
return saml2.getEnabled() || oauth2.getEnabled();
|
return saml2.getEnabled() || oauth2.getEnabled() || jwt.getEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LoginMethods {
|
public enum LoginMethods {
|
||||||
@ -153,6 +154,10 @@ public class ApplicationProperties {
|
|||||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isJwtActive() {
|
||||||
|
return (jwt != null && jwt.getEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class InitialLogin {
|
public static class InitialLogin {
|
||||||
private String username;
|
private String username;
|
||||||
@ -275,6 +280,26 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class JWT {
|
||||||
|
private Boolean enabled = false;
|
||||||
|
@ToString.Exclude private String secretKey;
|
||||||
|
private Long expiration = 3600000L; // Default 1 hour in milliseconds
|
||||||
|
private String algorithm = "HS256"; // Default HMAC algorithm
|
||||||
|
private String issuer = "Stirling-PDF"; // Default issuer
|
||||||
|
private Boolean enableRefreshToken = false;
|
||||||
|
private Long refreshTokenExpiration = 86400000L; // Default 24 hours
|
||||||
|
|
||||||
|
public boolean isSettingsValid() {
|
||||||
|
return enabled != null
|
||||||
|
&& enabled
|
||||||
|
&& secretKey != null
|
||||||
|
&& !secretKey.trim().isEmpty()
|
||||||
|
&& expiration != null
|
||||||
|
&& expiration > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
repositories {
|
repositories {
|
||||||
maven { url = "https://build.shibboleth.net/maven/releases" }
|
maven { url = "https://build.shibboleth.net/maven/releases" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
jwtVersion = '0.12.6'
|
||||||
|
}
|
||||||
|
|
||||||
bootRun {
|
bootRun {
|
||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
target sourceSets.main.allJava
|
target sourceSets.main.allJava
|
||||||
@ -38,6 +44,11 @@ dependencies {
|
|||||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE'
|
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE'
|
||||||
api 'io.micrometer:micrometer-registry-prometheus'
|
api 'io.micrometer:micrometer-registry-prometheus'
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
|
|
||||||
|
// JWT dependencies
|
||||||
|
api "io.jsonwebtoken:jjwt-api:$jwtVersion"
|
||||||
|
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion"
|
||||||
|
runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion"
|
||||||
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
|
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
|
||||||
runtimeOnly 'org.postgresql:postgresql:42.7.7'
|
runtimeOnly 'org.postgresql:postgresql:42.7.7'
|
||||||
constraints {
|
constraints {
|
||||||
|
@ -17,6 +17,7 @@ import stirling.software.common.util.RequestUriUtils;
|
|||||||
import stirling.software.proprietary.audit.AuditEventType;
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
import stirling.software.proprietary.audit.Audited;
|
import stirling.software.proprietary.audit.Audited;
|
||||||
|
import stirling.software.proprietary.security.service.JWTServiceInterface;
|
||||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
|
|
||||||
@ -24,13 +25,17 @@ import stirling.software.proprietary.security.service.UserService;
|
|||||||
public class CustomAuthenticationSuccessHandler
|
public class CustomAuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
private UserService userService;
|
private final UserService userService;
|
||||||
|
private final JWTServiceInterface jwtService;
|
||||||
|
|
||||||
public CustomAuthenticationSuccessHandler(
|
public CustomAuthenticationSuccessHandler(
|
||||||
LoginAttemptService loginAttemptService, UserService userService) {
|
LoginAttemptService loginAttemptService,
|
||||||
|
UserService userService,
|
||||||
|
JWTServiceInterface jwtService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.jwtService = jwtService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -46,23 +51,35 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
loginAttemptService.loginSucceeded(userName);
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Get the saved request
|
// Generate JWT token if JWT authentication is enabled
|
||||||
HttpSession session = request.getSession(false);
|
boolean jwtEnabled = jwtService.isJwtEnabled();
|
||||||
SavedRequest savedRequest =
|
if (jwtService != null && jwtEnabled) {
|
||||||
(session != null)
|
try {
|
||||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
String jwt = jwtService.generateToken(authentication);
|
||||||
: null;
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
|
log.debug("JWT token generated and added to response for user: {}", userName);
|
||||||
if (savedRequest != null
|
} catch (Exception e) {
|
||||||
&& !RequestUriUtils.isStaticResource(
|
log.error("Failed to generate JWT token for user: {}", userName, e);
|
||||||
request.getContextPath(), savedRequest.getRedirectUrl())) {
|
}
|
||||||
// Redirect to the original destination
|
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
|
||||||
} else {
|
|
||||||
// Redirect to the root URL (considering context path)
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// super.onAuthenticationSuccess(request, response, authentication);
|
if (jwtEnabled) {
|
||||||
|
// JWT mode: stateless authentication, redirect after setting token
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/");
|
||||||
|
} else {
|
||||||
|
// Get the saved request
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
SavedRequest savedRequest =
|
||||||
|
(session != null)
|
||||||
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (savedRequest != null
|
||||||
|
&& !RequestUriUtils.isStaticResource(
|
||||||
|
request.getContextPath(), savedRequest.getRedirectUrl())) {
|
||||||
|
// Redirect to the original destination
|
||||||
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import stirling.software.proprietary.audit.AuditLevel;
|
|||||||
import stirling.software.proprietary.audit.Audited;
|
import stirling.software.proprietary.audit.Audited;
|
||||||
import stirling.software.proprietary.security.saml2.CertificateUtils;
|
import stirling.software.proprietary.security.saml2.CertificateUtils;
|
||||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
|
import stirling.software.proprietary.security.service.JWTServiceInterface;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -40,15 +41,29 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
|
|
||||||
public static final String LOGOUT_PATH = "/login?logout=true";
|
public static final String LOGOUT_PATH = "/login?logout=true";
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties.Security securityProperties;
|
||||||
|
|
||||||
private final AppConfig appConfig;
|
private final AppConfig appConfig;
|
||||||
|
|
||||||
|
private final JWTServiceInterface jwtService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
|
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
|
// Clear JWT token if JWT authentication is enabled
|
||||||
|
if (jwtService != null && jwtService.isJwtEnabled()) {
|
||||||
|
try {
|
||||||
|
jwtService.clearTokenFromResponse(response);
|
||||||
|
log.debug("JWT token cleared from response during logout");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to clear JWT token during logout", e);
|
||||||
|
// Continue with normal logout flow even if JWT clearing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.isCommitted()) {
|
if (!response.isCommitted()) {
|
||||||
if (authentication != null) {
|
if (authentication != null) {
|
||||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||||
@ -82,7 +97,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
Saml2Authentication samlAuthentication)
|
Saml2Authentication samlAuthentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
SAML2 samlConf = securityProperties.getSaml2();
|
||||||
String registrationId = samlConf.getRegistrationId();
|
String registrationId = samlConf.getRegistrationId();
|
||||||
|
|
||||||
CustomSaml2AuthenticatedPrincipal principal =
|
CustomSaml2AuthenticatedPrincipal principal =
|
||||||
@ -127,7 +142,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
OAuth2AuthenticationToken oAuthToken)
|
OAuth2AuthenticationToken oAuthToken)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String registrationId;
|
String registrationId;
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = securityProperties.getOauth2();
|
||||||
String path = checkForErrors(request);
|
String path = checkForErrors(request);
|
||||||
|
|
||||||
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
package stirling.software.proprietary.security.configuration;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class JWTConfigurationValidator {
|
||||||
|
|
||||||
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void validateJWTConfiguration() {
|
||||||
|
if (!isJwtEnabled()) {
|
||||||
|
log.debug("JWT authentication is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Validating JWT configuration...");
|
||||||
|
|
||||||
|
ApplicationProperties.Security.JWT jwtConfig = applicationProperties.getSecurity().getJwt();
|
||||||
|
|
||||||
|
// Validate basic configuration
|
||||||
|
if (!jwtConfig.isSettingsValid()) {
|
||||||
|
log.error("JWT configuration is invalid. Please check your settings.yml file.");
|
||||||
|
log.error("Required fields: enabled=true, secretKey (Base64 encoded), expiration > 0");
|
||||||
|
throw new IllegalStateException("Invalid JWT configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate secret key length and format
|
||||||
|
validateSecretKey(jwtConfig.getSecretKey());
|
||||||
|
|
||||||
|
// Validate expiration time
|
||||||
|
validateExpiration(jwtConfig.getExpiration());
|
||||||
|
|
||||||
|
// Validate algorithm
|
||||||
|
validateAlgorithm(jwtConfig.getAlgorithm());
|
||||||
|
|
||||||
|
log.info("JWT configuration validated successfully");
|
||||||
|
log.info("JWT algorithm: {}", jwtConfig.getAlgorithm());
|
||||||
|
log.info(
|
||||||
|
"JWT expiration: {} ms ({} minutes)",
|
||||||
|
jwtConfig.getExpiration(),
|
||||||
|
jwtConfig.getExpiration() / 60000);
|
||||||
|
log.info("JWT issuer: {}", jwtConfig.getIssuer());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSecretKey(String secretKey) {
|
||||||
|
if (secretKey == null || secretKey.trim().isEmpty()) {
|
||||||
|
log.error("JWT secret key is not configured");
|
||||||
|
throw new IllegalStateException("JWT secret key is required when JWT is enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] decodedKey = Base64.getDecoder().decode(secretKey);
|
||||||
|
|
||||||
|
// For HMAC-SHA256, minimum key length should be 32 bytes (256 bits)
|
||||||
|
if (decodedKey.length < 32) {
|
||||||
|
log.warn(
|
||||||
|
"JWT secret key is shorter than recommended 256 bits. Current length: {} bits",
|
||||||
|
decodedKey.length * 8);
|
||||||
|
log.warn("Consider using a longer key for better security");
|
||||||
|
} else {
|
||||||
|
log.debug("JWT secret key length: {} bits", decodedKey.length * 8);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("JWT secret key is not a valid Base64 encoded string");
|
||||||
|
log.error("Generate a valid key using: openssl rand -base64 32");
|
||||||
|
throw new IllegalStateException("Invalid JWT secret key format", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateExpiration(Long expiration) {
|
||||||
|
if (expiration == null || expiration <= 0) {
|
||||||
|
log.error("JWT expiration time must be positive. Current value: {}", expiration);
|
||||||
|
throw new IllegalStateException("Invalid JWT expiration time");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if expiration is too short (less than 5 minutes)
|
||||||
|
if (expiration < 300000) { // 5 minutes in milliseconds
|
||||||
|
log.warn(
|
||||||
|
"JWT expiration time is very short: {} ms. Consider using a longer expiration time for better user experience.",
|
||||||
|
expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if expiration is too long (more than 24 hours)
|
||||||
|
if (expiration > 86400000) { // 24 hours in milliseconds
|
||||||
|
log.warn(
|
||||||
|
"JWT expiration time is very long: {} ms. Consider using a shorter expiration time for better security.",
|
||||||
|
expiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAlgorithm(String algorithm) {
|
||||||
|
if (algorithm == null || algorithm.trim().isEmpty()) {
|
||||||
|
log.warn("JWT algorithm is not specified, defaulting to HS256");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (algorithm.toUpperCase()) {
|
||||||
|
case "HS256", "HS384", "HS512" -> {
|
||||||
|
log.debug("Using HMAC algorithm: {}", algorithm);
|
||||||
|
}
|
||||||
|
case "RS256", "RS384", "RS512" -> {
|
||||||
|
log.debug("Using RSA algorithm: {}", algorithm);
|
||||||
|
log.warn(
|
||||||
|
"RSA algorithms are configured but current implementation uses HMAC. Consider implementing RSA support for production use.");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
log.warn("Unsupported JWT algorithm: {}. Falling back to HS256", algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a secure random Base64 encoded secret key for JWT This method is useful for
|
||||||
|
* generating initial secret keys
|
||||||
|
*/
|
||||||
|
public static String generateSecretKey() {
|
||||||
|
SecureRandom secureRandom = new SecureRandom();
|
||||||
|
byte[] key = new byte[32]; // 256 bits
|
||||||
|
secureRandom.nextBytes(key);
|
||||||
|
return Base64.getEncoder().encodeToString(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isJwtEnabled() {
|
||||||
|
return applicationProperties != null
|
||||||
|
&& applicationProperties.getSecurity() != null
|
||||||
|
&& applicationProperties.getSecurity().isJwtActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides helpful information for JWT configuration troubleshooting */
|
||||||
|
public void logConfigurationHelp() {
|
||||||
|
log.info("JWT Configuration Help:");
|
||||||
|
log.info("1. Enable JWT: Set jwt.enabled=true in settings.yml");
|
||||||
|
log.info("2. Generate secret key: openssl rand -base64 32");
|
||||||
|
log.info("3. Set expiration: jwt.expiration=3600000 (1 hour in milliseconds)");
|
||||||
|
log.info("4. Example generated secret key: {}", generateSecretKey());
|
||||||
|
log.info("5. Recommended expiration times:");
|
||||||
|
log.info(" - Short sessions: 900000 (15 minutes)");
|
||||||
|
log.info(" - Medium sessions: 3600000 (1 hour)");
|
||||||
|
log.info(" - Long sessions: 14400000 (4 hours)");
|
||||||
|
}
|
||||||
|
}
|
@ -8,11 +8,14 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
@ -39,6 +42,7 @@ import stirling.software.proprietary.security.database.repository.JPATokenReposi
|
|||||||
import stirling.software.proprietary.security.database.repository.PersistentLoginRepository;
|
import stirling.software.proprietary.security.database.repository.PersistentLoginRepository;
|
||||||
import stirling.software.proprietary.security.filter.FirstLoginFilter;
|
import stirling.software.proprietary.security.filter.FirstLoginFilter;
|
||||||
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
||||||
|
import stirling.software.proprietary.security.filter.JWTAuthenticationFilter;
|
||||||
import stirling.software.proprietary.security.filter.UserAuthenticationFilter;
|
import stirling.software.proprietary.security.filter.UserAuthenticationFilter;
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
@ -48,6 +52,7 @@ import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationSuc
|
|||||||
import stirling.software.proprietary.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
import stirling.software.proprietary.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||||
import stirling.software.proprietary.security.service.CustomOAuth2UserService;
|
import stirling.software.proprietary.security.service.CustomOAuth2UserService;
|
||||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||||
|
import stirling.software.proprietary.security.service.JWTServiceInterface;
|
||||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||||
@ -64,9 +69,11 @@ public class SecurityConfiguration {
|
|||||||
private final boolean loginEnabledValue;
|
private final boolean loginEnabledValue;
|
||||||
private final boolean runningProOrHigher;
|
private final boolean runningProOrHigher;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties.Security securityProperties;
|
||||||
private final AppConfig appConfig;
|
private final AppConfig appConfig;
|
||||||
private final UserAuthenticationFilter userAuthenticationFilter;
|
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
private final JWTAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
private final JWTServiceInterface jwtService;
|
||||||
private final LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
private final FirstLoginFilter firstLoginFilter;
|
private final FirstLoginFilter firstLoginFilter;
|
||||||
private final SessionPersistentRegistry sessionRegistry;
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
@ -82,8 +89,10 @@ public class SecurityConfiguration {
|
|||||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||||
AppConfig appConfig,
|
AppConfig appConfig,
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties.Security securityProperties,
|
||||||
UserAuthenticationFilter userAuthenticationFilter,
|
UserAuthenticationFilter userAuthenticationFilter,
|
||||||
|
JWTAuthenticationFilter jwtAuthenticationFilter,
|
||||||
|
JWTServiceInterface jwtService,
|
||||||
LoginAttemptService loginAttemptService,
|
LoginAttemptService loginAttemptService,
|
||||||
FirstLoginFilter firstLoginFilter,
|
FirstLoginFilter firstLoginFilter,
|
||||||
SessionPersistentRegistry sessionRegistry,
|
SessionPersistentRegistry sessionRegistry,
|
||||||
@ -97,8 +106,10 @@ public class SecurityConfiguration {
|
|||||||
this.loginEnabledValue = loginEnabledValue;
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
this.runningProOrHigher = runningProOrHigher;
|
this.runningProOrHigher = runningProOrHigher;
|
||||||
this.appConfig = appConfig;
|
this.appConfig = appConfig;
|
||||||
this.applicationProperties = applicationProperties;
|
this.securityProperties = securityProperties;
|
||||||
this.userAuthenticationFilter = userAuthenticationFilter;
|
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||||
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
|
this.jwtService = jwtService;
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
this.firstLoginFilter = firstLoginFilter;
|
this.firstLoginFilter = firstLoginFilter;
|
||||||
this.sessionRegistry = sessionRegistry;
|
this.sessionRegistry = sessionRegistry;
|
||||||
@ -115,14 +126,27 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
boolean jwtEnabled = securityProperties.isJwtActive();
|
||||||
http.csrf(csrf -> csrf.disable());
|
|
||||||
|
// Disable CSRF if explicitly disabled, login is disabled, or JWT is enabled (stateless)
|
||||||
|
if (securityProperties.getCsrfDisabled() || !loginEnabledValue || jwtEnabled) {
|
||||||
|
http.csrf(CsrfConfigurer::disable);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
http.addFilterBefore(
|
if (jwtEnabled && jwtAuthenticationFilter != null) {
|
||||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(
|
||||||
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
// .addFilterAfter(
|
||||||
|
// jwtAuthenticationFilter,
|
||||||
|
// userAuthenticationFilter.getClass());
|
||||||
|
} else {
|
||||||
|
http.addFilterBefore(
|
||||||
|
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
}
|
||||||
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterAfter(rateLimitingFilter(), firstLoginFilter.getClass());
|
||||||
|
if (!securityProperties.getCsrfDisabled()) {
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
CookieCsrfTokenRepository cookieRepo =
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
CsrfTokenRequestAttributeHandler requestHandler =
|
CsrfTokenRequestAttributeHandler requestHandler =
|
||||||
@ -156,18 +180,25 @@ public class SecurityConfiguration {
|
|||||||
.csrfTokenRepository(cookieRepo)
|
.csrfTokenRepository(cookieRepo)
|
||||||
.csrfTokenRequestHandler(requestHandler));
|
.csrfTokenRequestHandler(requestHandler));
|
||||||
}
|
}
|
||||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
// Configure session management based on JWT setting
|
||||||
http.sessionManagement(
|
http.sessionManagement(
|
||||||
sessionManagement ->
|
sessionManagement -> {
|
||||||
|
if (jwtEnabled) {
|
||||||
|
sessionManagement.sessionCreationPolicy(
|
||||||
|
SessionCreationPolicy.STATELESS);
|
||||||
|
} else {
|
||||||
sessionManagement
|
sessionManagement
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
.maximumSessions(10)
|
.maximumSessions(10)
|
||||||
.maxSessionsPreventsLogin(false)
|
.maxSessionsPreventsLogin(false)
|
||||||
.sessionRegistry(sessionRegistry)
|
.sessionRegistry(sessionRegistry)
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true");
|
||||||
|
}
|
||||||
|
});
|
||||||
http.authenticationProvider(daoAuthenticationProvider());
|
http.authenticationProvider(daoAuthenticationProvider());
|
||||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||||
|
// Configure logout behavior based on JWT setting
|
||||||
http.logout(
|
http.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutRequestMatcher(
|
logout.logoutRequestMatcher(
|
||||||
@ -175,31 +206,36 @@ public class SecurityConfiguration {
|
|||||||
.matcher("/logout"))
|
.matcher("/logout"))
|
||||||
.logoutSuccessHandler(
|
.logoutSuccessHandler(
|
||||||
new CustomLogoutSuccessHandler(
|
new CustomLogoutSuccessHandler(
|
||||||
applicationProperties, appConfig))
|
securityProperties, appConfig, jwtService))
|
||||||
.clearAuthentication(true)
|
.clearAuthentication(true)
|
||||||
.invalidateHttpSession(true)
|
.invalidateHttpSession(true)
|
||||||
.deleteCookies("JSESSIONID", "remember-me"));
|
.deleteCookies(
|
||||||
http.rememberMe(
|
"JSESSIONID", "remember-me", "STIRLING_JWT_TOKEN"));
|
||||||
rememberMeConfigurer -> // Use the configurator directly
|
// Only configure remember-me if JWT is not enabled (stateless) todo: check if remember-me can be used with JWT
|
||||||
rememberMeConfigurer
|
if (!jwtEnabled) {
|
||||||
.tokenRepository(persistentTokenRepository())
|
http.rememberMe(
|
||||||
.tokenValiditySeconds( // 14 days
|
rememberMeConfigurer -> // Use the configurator directly
|
||||||
14 * 24 * 60 * 60)
|
rememberMeConfigurer
|
||||||
.userDetailsService( // Your existing UserDetailsService
|
.tokenRepository(persistentTokenRepository())
|
||||||
userDetailsService)
|
.tokenValiditySeconds( // 14 days
|
||||||
.useSecureCookie( // Enable secure cookie
|
14 * 24 * 60 * 60)
|
||||||
true)
|
.userDetailsService( // Your existing UserDetailsService
|
||||||
.rememberMeParameter( // Form parameter name
|
userDetailsService)
|
||||||
"remember-me")
|
.useSecureCookie( // Enable secure cookie
|
||||||
.rememberMeCookieName( // Cookie name
|
true)
|
||||||
"remember-me")
|
.rememberMeParameter( // Form parameter name
|
||||||
.alwaysRemember(false));
|
"remember-me")
|
||||||
|
.rememberMeCookieName( // Cookie name
|
||||||
|
"remember-me")
|
||||||
|
.alwaysRemember(false));
|
||||||
|
}
|
||||||
http.authorizeHttpRequests(
|
http.authorizeHttpRequests(
|
||||||
authz ->
|
authz ->
|
||||||
authz.requestMatchers(
|
authz.requestMatchers(
|
||||||
req -> {
|
req -> {
|
||||||
String uri = req.getRequestURI();
|
String uri = req.getRequestURI();
|
||||||
String contextPath = req.getContextPath();
|
String contextPath = req.getContextPath();
|
||||||
|
|
||||||
// Remove the context path from the URI
|
// Remove the context path from the URI
|
||||||
String trimmedUri =
|
String trimmedUri =
|
||||||
uri.startsWith(contextPath)
|
uri.startsWith(contextPath)
|
||||||
@ -224,22 +260,23 @@ public class SecurityConfiguration {
|
|||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated());
|
.authenticated());
|
||||||
// Handle User/Password Logins
|
// Handle User/Password Logins
|
||||||
if (applicationProperties.getSecurity().isUserPass()) {
|
if (securityProperties.isUserPass()) {
|
||||||
http.formLogin(
|
http.formLogin(
|
||||||
formLogin ->
|
formLogin ->
|
||||||
formLogin
|
formLogin
|
||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomAuthenticationSuccessHandler(
|
new CustomAuthenticationSuccessHandler(
|
||||||
loginAttemptService, userService))
|
loginAttemptService,
|
||||||
|
userService,
|
||||||
|
jwtService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomAuthenticationFailureHandler(
|
new CustomAuthenticationFailureHandler(
|
||||||
loginAttemptService, userService))
|
loginAttemptService, userService))
|
||||||
.defaultSuccessUrl("/")
|
|
||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().isOauth2Active()) {
|
if (securityProperties.isOauth2Active()) {
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
oauth2.loginPage("/oauth2")
|
oauth2.loginPage("/oauth2")
|
||||||
@ -251,17 +288,17 @@ public class SecurityConfiguration {
|
|||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomOAuth2AuthenticationSuccessHandler(
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
applicationProperties,
|
securityProperties,
|
||||||
userService))
|
userService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomOAuth2AuthenticationFailureHandler())
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
. // Add existing Authorities from the database
|
// Add existing Authorities from the database
|
||||||
userInfoEndpoint(
|
.userInfoEndpoint(
|
||||||
userInfoEndpoint ->
|
userInfoEndpoint ->
|
||||||
userInfoEndpoint
|
userInfoEndpoint
|
||||||
.oidcUserService(
|
.oidcUserService(
|
||||||
new CustomOAuth2UserService(
|
new CustomOAuth2UserService(
|
||||||
applicationProperties,
|
securityProperties,
|
||||||
userService,
|
userService,
|
||||||
loginAttemptService))
|
loginAttemptService))
|
||||||
.userAuthoritiesMapper(
|
.userAuthoritiesMapper(
|
||||||
@ -269,7 +306,7 @@ public class SecurityConfiguration {
|
|||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) {
|
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
||||||
// Configure the authentication provider
|
// Configure the authentication provider
|
||||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
new OpenSaml4AuthenticationProvider();
|
new OpenSaml4AuthenticationProvider();
|
||||||
@ -287,7 +324,7 @@ public class SecurityConfiguration {
|
|||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomSaml2AuthenticationSuccessHandler(
|
new CustomSaml2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
applicationProperties,
|
securityProperties,
|
||||||
userService))
|
userService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomSaml2AuthenticationFailureHandler())
|
new CustomSaml2AuthenticationFailureHandler())
|
||||||
@ -306,6 +343,13 @@ public class SecurityConfiguration {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: check if this is needed
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
|
||||||
|
throws Exception {
|
||||||
|
return configuration.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
|
||||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
|
||||||
provider.setPasswordEncoder(passwordEncoder());
|
provider.setPasswordEncoder(passwordEncoder());
|
||||||
|
@ -0,0 +1,169 @@
|
|||||||
|
package stirling.software.proprietary.security.filter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||||
|
import stirling.software.proprietary.security.service.JWTServiceInterface;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ConditionalOnBooleanProperty("security.jwt.enabled")
|
||||||
|
public class JWTAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JWTServiceInterface jwtService;
|
||||||
|
private final CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
public JWTAuthenticationFilter(
|
||||||
|
JWTServiceInterface jwtService, CustomUserDetailsService userDetailsService) {
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
if (!jwtService.isJwtEnabled()) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shouldNotFilter(request)) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String jwtToken = jwtService.extractTokenFromRequest(request);
|
||||||
|
|
||||||
|
if (jwtToken == null) {
|
||||||
|
sendUnauthorizedResponse(response, "JWT token is missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jwtService.validateToken(jwtToken)) {
|
||||||
|
sendUnauthorizedResponse(response, "JWT token is invalid or expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = jwtService.extractUsername(jwtToken);
|
||||||
|
Authentication authentication = createAuthToken(request, username);
|
||||||
|
String jwt = jwtService.generateToken(authentication);
|
||||||
|
|
||||||
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(
|
||||||
|
"JWT authentication failed for request: {} {}",
|
||||||
|
request.getMethod(),
|
||||||
|
request.getRequestURI(),
|
||||||
|
e);
|
||||||
|
|
||||||
|
// Determine specific error message based on exception type
|
||||||
|
String errorMessage = "JWT authentication failed";
|
||||||
|
if (e.getMessage() != null && e.getMessage().contains("expired")) {
|
||||||
|
errorMessage = "JWT token has expired";
|
||||||
|
} else if (e.getMessage() != null && e.getMessage().contains("signature")) {
|
||||||
|
errorMessage = "JWT token signature is invalid";
|
||||||
|
} else if (e.getMessage() != null && e.getMessage().contains("malformed")) {
|
||||||
|
errorMessage = "JWT token is malformed";
|
||||||
|
}
|
||||||
|
|
||||||
|
sendUnauthorizedResponse(response, errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Authentication createAuthToken(HttpServletRequest request, String username) {
|
||||||
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
|
if (userDetails != null) {
|
||||||
|
UsernamePasswordAuthenticationToken authToken =
|
||||||
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
userDetails, null, userDetails.getAuthorities());
|
||||||
|
|
||||||
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
|
||||||
|
// Set authentication in SecurityContext
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||||
|
log.debug("JWT authentication successful for user: {}", username);
|
||||||
|
|
||||||
|
return SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
|
||||||
|
String[] permitAllPatterns = {
|
||||||
|
"/",
|
||||||
|
"/login",
|
||||||
|
"/register",
|
||||||
|
"/error",
|
||||||
|
"/images/",
|
||||||
|
"/public/",
|
||||||
|
"/css/",
|
||||||
|
"/fonts/",
|
||||||
|
"/js/",
|
||||||
|
"/pdfjs/",
|
||||||
|
"/pdfjs-legacy/",
|
||||||
|
"/api/v1/info/status",
|
||||||
|
"/site.webmanifest"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String pattern : permitAllPatterns) {
|
||||||
|
if (uri.startsWith(pattern)
|
||||||
|
|| uri.endsWith(".svg")
|
||||||
|
|| uri.endsWith(".png")
|
||||||
|
|| uri.endsWith(".ico")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendUnauthorizedResponse(HttpServletResponse response, String message)
|
||||||
|
throws IOException {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
String jsonResponse =
|
||||||
|
String.format(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"mesaage": %s,
|
||||||
|
"status": 401
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
message);
|
||||||
|
|
||||||
|
response.getWriter().write(jsonResponse);
|
||||||
|
response.getWriter().flush();
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private final LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties.Security securityProperties;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -60,7 +60,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oAuth = securityProperties.getOauth2();
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
|
@ -30,7 +30,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
private ApplicationProperties applicationProperties;
|
private ApplicationProperties.Security securityProperties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -65,7 +65,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
savedRequest.getRedirectUrl());
|
savedRequest.getRedirectUrl());
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
SAML2 saml2 = securityProperties.getSaml2();
|
||||||
log.debug(
|
log.debug(
|
||||||
"Processing SAML2 authentication with autoCreateUser: {}",
|
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||||
saml2.getAutoCreateUser());
|
saml2.getAutoCreateUser());
|
||||||
|
@ -27,13 +27,13 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
|
|
||||||
private final LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties.Security securityProperties;
|
||||||
|
|
||||||
public CustomOAuth2UserService(
|
public CustomOAuth2UserService(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties.Security securityProperties,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
LoginAttemptService loginAttemptService) {
|
LoginAttemptService loginAttemptService) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.securityProperties = securityProperties;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
try {
|
try {
|
||||||
OidcUser user = delegate.loadUser(userRequest);
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth2 = securityProperties.getOauth2();
|
||||||
UsernameAttribute usernameAttribute =
|
UsernameAttribute usernameAttribute =
|
||||||
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
|
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
|
||||||
String usernameAttributeKey = usernameAttribute.getName();
|
String usernameAttributeKey = usernameAttribute.getName();
|
||||||
|
@ -0,0 +1,261 @@
|
|||||||
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.*;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||||
|
import org.springframework.http.ResponseCookie;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.MalformedJwtException;
|
||||||
|
import io.jsonwebtoken.UnsupportedJwtException;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ConditionalOnBooleanProperty("security.jwt.enabled")
|
||||||
|
public class JWTService implements JWTServiceInterface {
|
||||||
|
|
||||||
|
private static final String JWT_COOKIE_NAME = "STIRLING_JWT_TOKEN";
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
|
private final ApplicationProperties.Security securityProperties;
|
||||||
|
|
||||||
|
private SecretKey signingKey;
|
||||||
|
|
||||||
|
public JWTService(ApplicationProperties applicationProperties) {
|
||||||
|
this.securityProperties = applicationProperties.getSecurity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
if (isJwtEnabled()) {
|
||||||
|
try {
|
||||||
|
initializeSigningKey();
|
||||||
|
log.info("JWT service initialized successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(
|
||||||
|
"Failed to initialize JWT service. JWT authentication will be disabled.",
|
||||||
|
e);
|
||||||
|
throw new RuntimeException("JWT service initialization failed", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("JWT authentication is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeSigningKey() {
|
||||||
|
try {
|
||||||
|
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
||||||
|
String secretKey = jwtProperties.getSecretKey();
|
||||||
|
|
||||||
|
if (isBlank(secretKey)) {
|
||||||
|
log.warn(
|
||||||
|
"JWT secret key is not configured. Generating a temporary key for this session.");
|
||||||
|
secretKey = generateTemporaryKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (jwtProperties.getAlgorithm()) {
|
||||||
|
case "HS256" ->
|
||||||
|
this.signingKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));
|
||||||
|
case "RS256" -> // RSA256 algorithm requires a 2048-bit key. Should load RSA key
|
||||||
|
// pairs configuration
|
||||||
|
// this.signingKey =
|
||||||
|
// Jwts.SIG.RS256.keyPair().build().getPrivate()
|
||||||
|
log.info("Using RSA algorithm: RS256");
|
||||||
|
default -> {
|
||||||
|
log.warn(
|
||||||
|
"Unsupported JWT algorithm: {}. Using default algorithm.",
|
||||||
|
jwtProperties.getAlgorithm());
|
||||||
|
this.signingKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("JWT service initialized with algorithm: {}", jwtProperties.getAlgorithm());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to initialize JWT signing key", e);
|
||||||
|
throw new RuntimeException("JWT service initialization failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateTemporaryKey() {
|
||||||
|
try {
|
||||||
|
// Generate a secure random key for HMAC-SHA256
|
||||||
|
SecureRandom secureRandom = new SecureRandom();
|
||||||
|
byte[] key = new byte[32]; // 256 bits
|
||||||
|
secureRandom.nextBytes(key);
|
||||||
|
return Base64.getEncoder().encodeToString(key);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to generate temporary JWT key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateToken(Authentication authentication) {
|
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
|
return generateToken(userDetails.getUsername(), new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateToken(String username, Map<String, Object> claims) {
|
||||||
|
if (!isJwtEnabled()) {
|
||||||
|
throw new IllegalStateException("JWT is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.claims(claims)
|
||||||
|
.subject(username)
|
||||||
|
.issuer(jwtProperties.getIssuer())
|
||||||
|
.issuedAt(new Date())
|
||||||
|
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
|
||||||
|
.signWith(signingKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
if (!isJwtEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token);
|
||||||
|
return true;
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
log.debug("Invalid JWT signature: {}", e.getMessage());
|
||||||
|
} catch (MalformedJwtException e) {
|
||||||
|
log.debug("Invalid JWT token: {}", e.getMessage());
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
log.debug("JWT token is expired: {}", e.getMessage());
|
||||||
|
} catch (UnsupportedJwtException e) {
|
||||||
|
log.debug("JWT token is unsupported: {}", e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.debug("JWT claims string is empty: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractUsername(String token) {
|
||||||
|
return extractClaim(token, Claims::getSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> extractAllClaims(String token) {
|
||||||
|
Claims claims = extractAllClaimsFromToken(token);
|
||||||
|
return new HashMap<>(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTokenExpired(String token) {
|
||||||
|
return extractExpiration(token).before(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date extractExpiration(String token) {
|
||||||
|
return extractClaim(token, Claims::getExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||||
|
final Claims claims = extractAllClaimsFromToken(token);
|
||||||
|
return claimsResolver.apply(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claims extractAllClaimsFromToken(String token) {
|
||||||
|
if (!isJwtEnabled()) {
|
||||||
|
throw new IllegalStateException("JWT is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token).getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractTokenFromRequest(HttpServletRequest request) {
|
||||||
|
// First, try to get token from Authorization header
|
||||||
|
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
|
||||||
|
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
|
||||||
|
return authHeader.substring(BEARER_PREFIX.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to cookie
|
||||||
|
Cookie[] cookies = request.getCookies();
|
||||||
|
if (cookies != null) {
|
||||||
|
for (Cookie cookie : cookies) {
|
||||||
|
if (JWT_COOKIE_NAME.equals(cookie.getName())) {
|
||||||
|
return cookie.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTokenToResponse(HttpServletResponse response, String token) {
|
||||||
|
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
||||||
|
// Add to Authorization header
|
||||||
|
response.setHeader(AUTHORIZATION_HEADER, BEARER_PREFIX + token);
|
||||||
|
|
||||||
|
// Add as HTTP-only secure cookie
|
||||||
|
ResponseCookie cookie =
|
||||||
|
ResponseCookie.from(JWT_COOKIE_NAME, token)
|
||||||
|
.httpOnly(true)
|
||||||
|
.secure(true) // Only send over HTTPS in production
|
||||||
|
.sameSite("Strict")
|
||||||
|
.maxAge(jwtProperties.getExpiration() / 1000) // Convert to seconds
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
response.addHeader("Set-Cookie", cookie.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearTokenFromResponse(HttpServletResponse response) {
|
||||||
|
response.setHeader(AUTHORIZATION_HEADER, "");
|
||||||
|
|
||||||
|
ResponseCookie cookie =
|
||||||
|
ResponseCookie.from(JWT_COOKIE_NAME, "")
|
||||||
|
.httpOnly(true)
|
||||||
|
.secure(true)
|
||||||
|
.sameSite("Strict")
|
||||||
|
.maxAge(0)
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
response.addHeader("Set-Cookie", cookie.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isJwtEnabled() {
|
||||||
|
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
||||||
|
|
||||||
|
return securityProperties.isJwtActive()
|
||||||
|
&& jwtProperties != null
|
||||||
|
&& jwtProperties.isSettingsValid();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
@ConditionalOnBooleanProperty("security.jwt.enabled")
|
||||||
|
public interface JWTServiceInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a JWT token for the authenticated user
|
||||||
|
*
|
||||||
|
* @param authentication Spring Security authentication object
|
||||||
|
* @return JWT token as a string
|
||||||
|
*/
|
||||||
|
String generateToken(Authentication authentication);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a JWT token with custom claims
|
||||||
|
*
|
||||||
|
* @param username the username
|
||||||
|
* @param claims additional claims to include in the token
|
||||||
|
* @return JWT token as a string
|
||||||
|
*/
|
||||||
|
String generateToken(String username, Map<String, Object> claims);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a JWT token
|
||||||
|
*
|
||||||
|
* @param token the JWT token to validate
|
||||||
|
* @return true if token is valid, false otherwise
|
||||||
|
*/
|
||||||
|
boolean validateToken(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract username from JWT token
|
||||||
|
*
|
||||||
|
* @param token the JWT token
|
||||||
|
* @return username extracted from token
|
||||||
|
*/
|
||||||
|
String extractUsername(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all claims from JWT token
|
||||||
|
*
|
||||||
|
* @param token the JWT token
|
||||||
|
* @return map of claims
|
||||||
|
*/
|
||||||
|
Map<String, Object> extractAllClaims(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if token is expired
|
||||||
|
*
|
||||||
|
* @param token the JWT token
|
||||||
|
* @return true if token is expired, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isTokenExpired(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract JWT token from HTTP request (header or cookie)
|
||||||
|
*
|
||||||
|
* @param request HTTP servlet request
|
||||||
|
* @return JWT token if found, null otherwise
|
||||||
|
*/
|
||||||
|
String extractTokenFromRequest(HttpServletRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add JWT token to HTTP response (header and cookie)
|
||||||
|
*
|
||||||
|
* @param response HTTP servlet response
|
||||||
|
* @param token JWT token to add
|
||||||
|
*/
|
||||||
|
void addTokenToResponse(HttpServletResponse response, String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear JWT token from HTTP response (remove cookie)
|
||||||
|
*
|
||||||
|
* @param response HTTP servlet response
|
||||||
|
*/
|
||||||
|
void clearTokenFromResponse(HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if JWT authentication is enabled
|
||||||
|
*
|
||||||
|
* @return true if JWT is enabled, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isJwtEnabled();
|
||||||
|
}
|
@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
@ -16,7 +15,7 @@ import static org.mockito.Mockito.*;
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class CustomLogoutSuccessHandlerTest {
|
class CustomLogoutSuccessHandlerTest {
|
||||||
|
|
||||||
@Mock private ApplicationProperties applicationProperties;
|
@Mock private ApplicationProperties.Security securityProperties;
|
||||||
|
|
||||||
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken oAuth2AuthenticationToken = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken oAuth2AuthenticationToken = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -51,8 +49,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, oAuth2AuthenticationToken);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, oAuth2AuthenticationToken);
|
||||||
@ -67,7 +64,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -81,8 +77,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
@ -98,7 +93,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -108,8 +102,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
@ -124,7 +117,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -135,8 +127,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
@ -151,7 +142,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -164,8 +154,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
@ -180,7 +169,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -195,8 +183,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
@ -211,7 +198,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -227,8 +213,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
@ -243,7 +228,6 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
|
||||||
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
|
|
||||||
ApplicationProperties.Security.OAUTH2 oauth =
|
ApplicationProperties.Security.OAUTH2 oauth =
|
||||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||||
|
|
||||||
@ -256,8 +240,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
when(request.getServerPort()).thenReturn(8080);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||||
when(security.getOauth2()).thenReturn(oauth);
|
|
||||||
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
|
||||||
|
|
||||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||||
|
@ -5,7 +5,7 @@ logging.level.org.eclipse.jetty=WARN
|
|||||||
#logging.level.org.springframework.security.saml2=TRACE
|
#logging.level.org.springframework.security.saml2=TRACE
|
||||||
#logging.level.org.springframework.security=DEBUG
|
#logging.level.org.springframework.security=DEBUG
|
||||||
#logging.level.org.opensaml=DEBUG
|
#logging.level.org.opensaml=DEBUG
|
||||||
#logging.level.stirling.software.SPDF.config.security: DEBUG
|
#logging.level.stirling.software.proprietary.security: DEBUG
|
||||||
logging.level.com.zaxxer.hikari=WARN
|
logging.level.com.zaxxer.hikari=WARN
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
server.forward-headers-strategy=NATIVE
|
server.forward-headers-strategy=NATIVE
|
||||||
|
@ -59,6 +59,14 @@ security:
|
|||||||
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
|
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
|
||||||
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
|
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
|
||||||
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
||||||
|
jwt:
|
||||||
|
enabled: true # set to 'true' to enable JWT authentication
|
||||||
|
secretKey: 'Uz4BgfMySCz2Uplhp1x9ij19vVV2bXYktROtrlw3CC0=' # secret
|
||||||
|
expiration: 3600000 # Expiration time in milliseconds. Default is 1 hour (3600000 ms)
|
||||||
|
algorithm: HS256 # JWT signing algorithm. Default is HS256
|
||||||
|
issuer: Stirling-PDF # Issuer of the JWT token. Default is 'Stirling-PDF'
|
||||||
|
refreshTokenEnabled: false # Set to 'true' to enable refresh tokens
|
||||||
|
refreshTokenExpiration: 86400000 # Expiration time for refresh tokens in milliseconds. Default is 1 day (86400000 ms)
|
||||||
|
|
||||||
premium:
|
premium:
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
|
Loading…
x
Reference in New Issue
Block a user