mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
Removed file
This commit is contained in:
parent
006c186694
commit
b53ac89541
120
CLAUDE.md
120
CLAUDE.md
@ -1,120 +0,0 @@
|
|||||||
# 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
|
|
@ -8,6 +8,7 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -51,6 +52,14 @@ public class AppConfig {
|
|||||||
@Value("${server.port:8080}")
|
@Value("${server.port:8080}")
|
||||||
private String serverPort;
|
private String serverPort;
|
||||||
|
|
||||||
|
@Value("${v2}")
|
||||||
|
public boolean v2Enabled;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public boolean v2Enabled() {
|
||||||
|
return v2Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||||
@ -120,7 +129,7 @@ public class AppConfig {
|
|||||||
public boolean rateLimit() {
|
public boolean rateLimit() {
|
||||||
String rateLimit = System.getProperty("rateLimit");
|
String rateLimit = System.getProperty("rateLimit");
|
||||||
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
||||||
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
|
return Boolean.parseBoolean(rateLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "RunningInDocker")
|
@Bean(name = "RunningInDocker")
|
||||||
@ -140,8 +149,8 @@ public class AppConfig {
|
|||||||
if (!Files.exists(mountInfo)) {
|
if (!Files.exists(mountInfo)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try (Stream<String> lines = Files.lines(mountInfo)) {
|
||||||
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
|
return lines.anyMatch(line -> line.contains(" /configs "));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -115,14 +115,13 @@ 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() || jwt.getEnabled();
|
return saml2.getEnabled() || oauth2.getEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LoginMethods {
|
public enum LoginMethods {
|
||||||
@ -160,10 +159,6 @@ 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;
|
||||||
@ -302,20 +297,6 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class JWT {
|
|
||||||
private Boolean enabled = false;
|
|
||||||
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 && expiration != null && expiration > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@ -48,3 +48,6 @@ spring.main.allow-bean-definition-overriding=true
|
|||||||
|
|
||||||
# Set up a consistent temporary directory location
|
# Set up a consistent temporary directory location
|
||||||
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
|
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
|
||||||
|
|
||||||
|
# V2 features
|
||||||
|
v2=true
|
||||||
|
@ -12,13 +12,13 @@
|
|||||||
|
|
||||||
security:
|
security:
|
||||||
enableLogin: true # set to 'true' to enable login
|
enableLogin: true # set to 'true' to enable login
|
||||||
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
|
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
||||||
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||||
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
|
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
|
||||||
initialLogin:
|
initialLogin:
|
||||||
username: 'admin' # initial username for the first login
|
username: '' # initial username for the first login
|
||||||
password: 'stirling' # initial password for the first login
|
password: '' # initial password for the first login
|
||||||
oauth2:
|
oauth2:
|
||||||
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
client:
|
client:
|
||||||
@ -47,29 +47,24 @@ security:
|
|||||||
scopes: openid, profile, email # specify the scopes for which the application will request permissions
|
scopes: openid, profile, email # specify the scopes for which the application will request permissions
|
||||||
provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak'
|
provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak'
|
||||||
saml2:
|
saml2:
|
||||||
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
|
enabled: true # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
|
||||||
provider: '' # The name of your Provider
|
provider: authentik # The name of your Provider
|
||||||
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
|
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
|
||||||
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
||||||
registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs
|
registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs
|
||||||
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata
|
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata
|
||||||
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider
|
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider
|
||||||
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider
|
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider
|
||||||
idpIssuer: '' # The ID of your Provider
|
idpIssuer: authentik # The ID of your Provider
|
||||||
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
|
idpCert: classpath:authentik-Self_Signed_Certificate.pem # 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: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:cert.crt # Your signing certificate. Generated from your keypair
|
||||||
jwt:
|
|
||||||
enabled: true # set to 'true' to enable JWT authentication
|
|
||||||
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'
|
|
||||||
|
|
||||||
premium:
|
premium:
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
enabled: false # Enable license key checks for pro/enterprise features
|
enabled: true # Enable license key checks for pro/enterprise features
|
||||||
proFeatures:
|
proFeatures:
|
||||||
database: true # Enable database features
|
database: false # Enable database features
|
||||||
SSOAutoLogin: false
|
SSOAutoLogin: false
|
||||||
CustomMetadata:
|
CustomMetadata:
|
||||||
autoUpdateMetadata: false
|
autoUpdateMetadata: false
|
||||||
@ -105,7 +100,7 @@ legal:
|
|||||||
system:
|
system:
|
||||||
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||||
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
|
enableAlphaFunctionality: true # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
|
||||||
showUpdate: false # see when a new update is available
|
showUpdate: false # see when a new update is available
|
||||||
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package stirling.software.proprietary.security;
|
package stirling.software.proprietary.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
@ -17,6 +18,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.model.AuthenticationType;
|
||||||
import stirling.software.proprietary.security.service.JWTServiceInterface;
|
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;
|
||||||
@ -51,20 +53,17 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
loginAttemptService.loginSucceeded(userName);
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Generate JWT token if JWT authentication is enabled
|
if (jwtService.isJwtEnabled()) {
|
||||||
boolean jwtEnabled = jwtService.isJwtEnabled();
|
|
||||||
if (jwtEnabled) {
|
|
||||||
try {
|
try {
|
||||||
String jwt = jwtService.generateToken(authentication);
|
String jwt =
|
||||||
|
jwtService.generateToken(
|
||||||
|
authentication, Map.of("authType", AuthenticationType.WEB));
|
||||||
jwtService.addTokenToResponse(response, jwt);
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
log.debug("JWT generated for user: {}", userName);
|
log.debug("JWT generated for user: {}", userName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to generate JWT token for user: {}", userName, e);
|
log.error("Failed to generate JWT token for user: {}", userName, e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (jwtEnabled) {
|
|
||||||
// JWT mode: stateless authentication, redirect after setting token
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/");
|
getRedirectStrategy().sendRedirect(request, response, "/");
|
||||||
} else {
|
} else {
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
|
@ -71,10 +71,8 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
authentication.getClass().getSimpleName());
|
authentication.getClass().getSimpleName());
|
||||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
}
|
}
|
||||||
} else if (jwtService.isJwtEnabled()) {
|
} else if (!jwtService.extractTokenFromRequest(request).isBlank()) {
|
||||||
// Clear JWT token if JWT authentication is enabled
|
|
||||||
jwtService.clearTokenFromResponse(response);
|
jwtService.clearTokenFromResponse(response);
|
||||||
log.debug("Cleared JWT from response");
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login page after logout
|
// Redirect to login page after logout
|
||||||
|
@ -127,15 +127,14 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
boolean jwtEnabled = securityProperties.isJwtActive();
|
if (securityProperties.getCsrfDisabled() || !loginEnabledValue) {
|
||||||
|
|
||||||
// Disable CSRF if explicitly disabled, login is disabled, or JWT is enabled (stateless)
|
|
||||||
if (securityProperties.getCsrfDisabled() || !loginEnabledValue || jwtEnabled) {
|
|
||||||
http.csrf(CsrfConfigurer::disable);
|
http.csrf(CsrfConfigurer::disable);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
if (jwtEnabled) {
|
boolean v2Enabled = appConfig.v2Enabled();
|
||||||
|
|
||||||
|
if (v2Enabled) {
|
||||||
http.addFilterBefore(
|
http.addFilterBefore(
|
||||||
jwtAuthenticationFilter(),
|
jwtAuthenticationFilter(),
|
||||||
UsernamePasswordAuthenticationFilter.class)
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
@ -143,13 +142,10 @@ public class SecurityConfiguration {
|
|||||||
exceptionHandling ->
|
exceptionHandling ->
|
||||||
exceptionHandling.authenticationEntryPoint(
|
exceptionHandling.authenticationEntryPoint(
|
||||||
jwtAuthenticationEntryPoint));
|
jwtAuthenticationEntryPoint));
|
||||||
} else {
|
|
||||||
http.addFilterBefore(
|
|
||||||
userAuthenticationFilter,
|
|
||||||
UsernamePasswordAuthenticationFilter.class)
|
|
||||||
.addFilterBefore(userAuthenticationFilter, firstLoginFilter.getClass());
|
|
||||||
}
|
}
|
||||||
http.addFilterAfter(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
http.addFilterAt(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterAfter(rateLimitingFilter(), userAuthenticationFilter.getClass())
|
||||||
|
.addFilterAfter(firstLoginFilter, rateLimitingFilter().getClass());
|
||||||
|
|
||||||
if (!securityProperties.getCsrfDisabled()) {
|
if (!securityProperties.getCsrfDisabled()) {
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
CookieCsrfTokenRepository cookieRepo =
|
||||||
@ -189,7 +185,7 @@ public class SecurityConfiguration {
|
|||||||
// Configure session management based on JWT setting
|
// Configure session management based on JWT setting
|
||||||
http.sessionManagement(
|
http.sessionManagement(
|
||||||
sessionManagement -> {
|
sessionManagement -> {
|
||||||
if (jwtEnabled) {
|
if (v2Enabled) {
|
||||||
sessionManagement.sessionCreationPolicy(
|
sessionManagement.sessionCreationPolicy(
|
||||||
SessionCreationPolicy.STATELESS);
|
SessionCreationPolicy.STATELESS);
|
||||||
} else {
|
} else {
|
||||||
@ -290,8 +286,9 @@ public class SecurityConfiguration {
|
|||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomOAuth2AuthenticationSuccessHandler(
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
securityProperties,
|
securityProperties.getOauth2(),
|
||||||
userService))
|
userService,
|
||||||
|
jwtService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomOAuth2AuthenticationFailureHandler())
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
// Add existing Authorities from the database
|
// Add existing Authorities from the database
|
||||||
@ -326,8 +323,9 @@ public class SecurityConfiguration {
|
|||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomSaml2AuthenticationSuccessHandler(
|
new CustomSaml2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
securityProperties,
|
securityProperties.getSaml2(),
|
||||||
userService))
|
userService,
|
||||||
|
jwtService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomSaml2AuthenticationFailureHandler())
|
new CustomSaml2AuthenticationFailureHandler())
|
||||||
.authenticationRequestResolver(
|
.authenticationRequestResolver(
|
||||||
@ -372,6 +370,10 @@ public class SecurityConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
public JWTAuthenticationFilter jwtAuthenticationFilter() {
|
public JWTAuthenticationFilter jwtAuthenticationFilter() {
|
||||||
return new JWTAuthenticationFilter(
|
return new JWTAuthenticationFilter(
|
||||||
jwtService, userDetailsService, jwtAuthenticationEntryPoint);
|
jwtService,
|
||||||
|
userService,
|
||||||
|
userDetailsService,
|
||||||
|
jwtAuthenticationEntryPoint,
|
||||||
|
securityProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -92,14 +91,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<SimpleGrantedAuthority> authorities =
|
authentication =
|
||||||
user.get().getAuthorities().stream()
|
new ApiKeyAuthenticationToken(
|
||||||
.map(
|
user.get(), apiKey, user.get().getAuthorities());
|
||||||
authority ->
|
|
||||||
new SimpleGrantedAuthority(
|
|
||||||
authority.getAuthority()))
|
|
||||||
.toList();
|
|
||||||
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
// If API key authentication fails, deny the request
|
// If API key authentication fails, deny the request
|
||||||
|
@ -2,5 +2,8 @@ package stirling.software.proprietary.security.model;
|
|||||||
|
|
||||||
public enum AuthenticationType {
|
public enum AuthenticationType {
|
||||||
WEB,
|
WEB,
|
||||||
SSO
|
SSO,
|
||||||
|
// TODO: Worth making a distinction between OAuth2 and SAML2?
|
||||||
|
OAUTH2,
|
||||||
|
SAML2
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package stirling.software.proprietary.security.model;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
@ -18,7 +20,7 @@ import lombok.Setter;
|
|||||||
@Table(name = "authorities")
|
@Table(name = "authorities")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class Authority implements Serializable {
|
public class Authority implements GrantedAuthority, Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -25,7 +27,7 @@ import stirling.software.proprietary.model.Team;
|
|||||||
@Setter
|
@Setter
|
||||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||||
@ToString(onlyExplicitlyIncluded = true)
|
@ToString(onlyExplicitlyIncluded = true)
|
||||||
public class User implements Serializable {
|
public class User implements UserDetails, Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package stirling.software.proprietary.security.oauth2;
|
package stirling.software.proprietary.security.oauth2;
|
||||||
|
|
||||||
|
import static stirling.software.proprietary.security.model.AuthenticationType.OAUTH2;
|
||||||
|
import static stirling.software.proprietary.security.model.AuthenticationType.SSO;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@ -18,10 +22,10 @@ import jakarta.servlet.http.HttpSession;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
|
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;
|
||||||
|
|
||||||
@ -30,8 +34,9 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private final LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
private final ApplicationProperties.Security securityProperties;
|
private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final JWTServiceInterface jwtService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
@ -60,8 +65,6 @@ 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 = securityProperties.getOauth2();
|
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
@ -69,7 +72,12 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
|
if (jwtService.isJwtEnabled()) {
|
||||||
|
String jwt =
|
||||||
|
jwtService.generateToken(
|
||||||
|
authentication, Map.of("authType", AuthenticationType.OAUTH2));
|
||||||
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
|
}
|
||||||
if (userService.isUserDisabled(username)) {
|
if (userService.isUserDisabled(username)) {
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
@ -77,20 +85,21 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
if (userService.usernameExistsIgnoreCase(username)
|
if (userService.usernameExistsIgnoreCase(username)
|
||||||
&& userService.hasPassword(username)
|
&& userService.hasPassword(username)
|
||||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
&& !userService.isAuthenticationTypeByUsername(username, SSO)
|
||||||
&& oAuth.getAutoCreateUser()) {
|
&& oauth2Properties.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (oAuth.getBlockRegistration()
|
if (oauth2Properties.getBlockRegistration()
|
||||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (principal instanceof OAuth2User) {
|
if (principal instanceof OAuth2User) {
|
||||||
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
userService.processSSOPostLogin(
|
||||||
|
username, oauth2Properties.getAutoCreateUser(), OAUTH2);
|
||||||
}
|
}
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
|
@ -34,6 +34,7 @@ import stirling.software.common.model.oauth2.GitHubProvider;
|
|||||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||||
import stirling.software.common.model.oauth2.Provider;
|
import stirling.software.common.model.oauth2.Provider;
|
||||||
|
import stirling.software.proprietary.security.model.Authority;
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.model.exception.NoProviderFoundException;
|
import stirling.software.proprietary.security.model.exception.NoProviderFoundException;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
@ -239,12 +240,14 @@ public class OAuth2Configuration {
|
|||||||
Optional<User> userOpt =
|
Optional<User> userOpt =
|
||||||
userService.findByUsernameIgnoreCase(
|
userService.findByUsernameIgnoreCase(
|
||||||
(String) oAuth2Auth.getAttributes().get(useAsUsername));
|
(String) oAuth2Auth.getAttributes().get(useAsUsername));
|
||||||
if (userOpt.isPresent()) {
|
userOpt.ifPresent(
|
||||||
User user = userOpt.get();
|
user ->
|
||||||
mappedAuthorities.add(
|
mappedAuthorities.add(
|
||||||
new SimpleGrantedAuthority(
|
new Authority(
|
||||||
userService.findRole(user).getAuthority()));
|
userService
|
||||||
}
|
.findRole(user)
|
||||||
|
.getAuthority(),
|
||||||
|
user)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return mappedAuthorities;
|
return mappedAuthorities;
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package stirling.software.proprietary.security.saml2;
|
package stirling.software.proprietary.security.saml2;
|
||||||
|
|
||||||
|
import static stirling.software.proprietary.security.model.AuthenticationType.SAML2;
|
||||||
|
import static stirling.software.proprietary.security.model.AuthenticationType.SSO;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@ -17,10 +21,10 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
|
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;
|
||||||
|
|
||||||
@ -30,8 +34,9 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
private ApplicationProperties.Security securityProperties;
|
private ApplicationProperties.Security.SAML2 saml2Properties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
private final JWTServiceInterface jwtService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
@ -65,10 +70,20 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
savedRequest.getRedirectUrl());
|
savedRequest.getRedirectUrl());
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
SAML2 saml2 = securityProperties.getSaml2();
|
if (jwtService.isJwtEnabled()) {
|
||||||
|
String jwt =
|
||||||
|
jwtService.generateToken(
|
||||||
|
authentication, Map.of("authType", AuthenticationType.SAML2));
|
||||||
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
|
|
||||||
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
|
// getRedirectStrategy().sendRedirect(request, response,
|
||||||
|
// "/");
|
||||||
|
// return;
|
||||||
|
}
|
||||||
log.debug(
|
log.debug(
|
||||||
"Processing SAML2 authentication with autoCreateUser: {}",
|
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||||
saml2.getAutoCreateUser());
|
saml2Properties.getAutoCreateUser());
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
log.debug("User {} is blocked due to too many login attempts", username);
|
log.debug("User {} is blocked due to too many login attempts", username);
|
||||||
@ -82,17 +97,21 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
||||||
boolean hasPassword = userExists && userService.hasPassword(username);
|
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||||
boolean isSSOUser =
|
boolean isSSOUser =
|
||||||
userExists
|
userExists && userService.isAuthenticationTypeByUsername(username, SSO);
|
||||||
&& userService.isAuthenticationTypeByUsername(
|
boolean isSAML2User =
|
||||||
username, AuthenticationType.SSO);
|
userExists && userService.isAuthenticationTypeByUsername(username, SAML2);
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
|
"User status - Exists: {}, Has password: {}, Is SSO user: {}, Is SAML2 user: {}",
|
||||||
userExists,
|
userExists,
|
||||||
hasPassword,
|
hasPassword,
|
||||||
isSSOUser);
|
isSSOUser,
|
||||||
|
isSAML2User);
|
||||||
|
|
||||||
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
|
if (userExists
|
||||||
|
&& hasPassword
|
||||||
|
&& (!isSSOUser || !isSAML2User)
|
||||||
|
&& saml2Properties.getAutoCreateUser()) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||||
username);
|
username);
|
||||||
@ -102,14 +121,15 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (saml2.getBlockRegistration() && !userExists) {
|
if (saml2Properties.getBlockRegistration() && !userExists) {
|
||||||
log.debug("Registration blocked for new user: {}", username);
|
log.debug("Registration blocked for new user: {}", username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug("Processing SSO post-login for user: {}", username);
|
log.debug("Processing SSO post-login for user: {}", username);
|
||||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
userService.processSSOPostLogin(
|
||||||
|
username, saml2Properties.getAutoCreateUser(), SAML2);
|
||||||
log.debug("Successfully processed authentication for user: {}", username);
|
log.debug("Successfully processed authentication for user: {}", username);
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
|
@ -54,7 +54,7 @@ public class SAML2Configuration {
|
|||||||
.entityId(samlConf.getIdpIssuer())
|
.entityId(samlConf.getIdpIssuer())
|
||||||
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
||||||
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
||||||
.singleLogoutServiceResponseLocation("http://localhost:8080/login")
|
.singleLogoutServiceResponseLocation("{baseUrl}:{basePort}/login")
|
||||||
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
||||||
.assertionConsumerServiceLocation(
|
.assertionConsumerServiceLocation(
|
||||||
"{baseUrl}/login/saml2/sso/{registrationId}")
|
"{baseUrl}/login/saml2/sso/{registrationId}")
|
||||||
@ -76,10 +76,17 @@ public class SAML2Configuration {
|
|||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
|
public HttpSessionSaml2AuthenticationRequestRepository saml2AuthenticationRequestRepository() {
|
||||||
|
return new HttpSessionSaml2AuthenticationRequestRepository();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
|
||||||
|
HttpSessionSaml2AuthenticationRequestRepository saml2AuthenticationRequestRepository) {
|
||||||
OpenSaml4AuthenticationRequestResolver resolver =
|
OpenSaml4AuthenticationRequestResolver resolver =
|
||||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||||
|
|
||||||
@ -87,10 +94,8 @@ public class SAML2Configuration {
|
|||||||
customizer -> {
|
customizer -> {
|
||||||
HttpServletRequest request = customizer.getRequest();
|
HttpServletRequest request = customizer.getRequest();
|
||||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||||
HttpSessionSaml2AuthenticationRequestRepository requestRepository =
|
|
||||||
new HttpSessionSaml2AuthenticationRequestRepository();
|
|
||||||
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
|
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
|
||||||
requestRepository.loadAuthenticationRequest(request);
|
saml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
|
||||||
|
|
||||||
if (saml2AuthenticationRequest != null) {
|
if (saml2AuthenticationRequest != null) {
|
||||||
String sessionId = request.getSession(false).getId();
|
String sessionId = request.getSession(false).getId();
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
package stirling.software.proprietary.security.service;
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
@ -14,7 +9,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
import stirling.software.proprietary.security.model.Authority;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -34,26 +29,18 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
() ->
|
() ->
|
||||||
new UsernameNotFoundException(
|
new UsernameNotFoundException(
|
||||||
"No user found with username: " + username));
|
"No user found with username: " + username));
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
if (!user.hasPassword()) {
|
|
||||||
|
AuthenticationType userAuthenticationType =
|
||||||
|
AuthenticationType.valueOf(user.getAuthenticationType().toUpperCase());
|
||||||
|
if (!user.hasPassword() && userAuthenticationType == AuthenticationType.WEB) {
|
||||||
throw new IllegalArgumentException("Password must not be null");
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
}
|
}
|
||||||
return new org.springframework.security.core.userdetails.User(
|
|
||||||
user.getUsername(),
|
|
||||||
user.getPassword(),
|
|
||||||
user.isEnabled(),
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
getAuthorities(user.getAuthorities()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
return user;
|
||||||
return authorities.stream()
|
|
||||||
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import org.springframework.context.i18n.LocaleContextHolder;
|
|||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -73,7 +72,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
public void processSSOPostLogin(String username, boolean autoCreateUser)
|
public void processSSOPostLogin(
|
||||||
|
String username, boolean autoCreateUser, AuthenticationType type)
|
||||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return;
|
return;
|
||||||
@ -83,7 +83,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (autoCreateUser) {
|
if (autoCreateUser) {
|
||||||
saveUser(username, AuthenticationType.SSO);
|
saveUser(username, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,10 +100,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
||||||
// Convert each Authority object into a SimpleGrantedAuthority object.
|
return user.getAuthorities();
|
||||||
return user.getAuthorities().stream()
|
|
||||||
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateApiKey() {
|
private String generateApiKey() {
|
||||||
|
@ -29,10 +29,12 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
void testSuccessfulLogout() throws IOException {
|
void testSuccessfulLogout() throws IOException {
|
||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
String logoutPath = "logout=true";
|
String token = "token";
|
||||||
|
String logoutPath = "/login?logout=true";
|
||||||
|
|
||||||
when(response.isCommitted()).thenReturn(false);
|
when(response.isCommitted()).thenReturn(false);
|
||||||
when(jwtService.isJwtEnabled()).thenReturn(false);
|
when(jwtService.extractTokenFromRequest(request)).thenReturn(token);
|
||||||
|
doNothing().when(jwtService).clearTokenFromResponse(response);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||||
|
|
||||||
@ -46,9 +48,11 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
String logoutPath = "/login?logout=true";
|
String logoutPath = "/login?logout=true";
|
||||||
|
String token = "token";
|
||||||
|
|
||||||
when(response.isCommitted()).thenReturn(false);
|
when(response.isCommitted()).thenReturn(false);
|
||||||
when(jwtService.isJwtEnabled()).thenReturn(true);
|
when(jwtService.extractTokenFromRequest(request)).thenReturn(token);
|
||||||
|
doNothing().when(jwtService).clearTokenFromResponse(response);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user