merge changes

This commit is contained in:
EthanHealy01 2025-08-15 19:57:46 +01:00
commit 34566cbae9
288 changed files with 7077 additions and 2035 deletions

View File

@ -10,7 +10,8 @@
"Bash(npm test)", "Bash(npm test)",
"Bash(npm test:*)", "Bash(npm test:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(npx tsc:*)" "Bash(npx tsc:*)",
"Bash(sed:*)"
], ],
"deny": [] "deny": []
} }

View File

@ -46,6 +46,9 @@ labels:
- label: 'API' - label: 'API'
title: '.*openapi.*|.*swagger.*|.*api.*' title: '.*openapi.*|.*swagger.*|.*api.*'
- label: 'v2'
base-branch: 'V2'
- label: 'Translation' - label: 'Translation'
files: files:
- 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties' - 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
@ -62,6 +65,7 @@ labels:
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*' - 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*' - 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*' - 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
- 'frontend/**'
- label: 'Java' - label: 'Java'
files: files:
@ -120,6 +124,7 @@ labels:
- 'scripts/installFonts.sh' - 'scripts/installFonts.sh'
- 'test.sh' - 'test.sh'
- 'test2.sh' - 'test2.sh'
- 'docker/**'
- label: 'Devtools' - label: 'Devtools'
files: files:
@ -131,7 +136,6 @@ labels:
- '.github/workflows/pre_commit.yml' - '.github/workflows/pre_commit.yml'
- 'devGuide/.*' - 'devGuide/.*'
- 'devTools/.*' - 'devTools/.*'
- 'devTools/.*'
- label: 'Test' - label: 'Test'
files: files:

1
.github/labels.yml vendored
View File

@ -83,6 +83,7 @@
color: "DEDEDE" color: "DEDEDE"
- name: "v2" - name: "v2"
color: "FFFF00" color: "FFFF00"
description: "Issues or pull requests related to the v2 branch"
- name: "wontfix" - name: "wontfix"
description: "This will not be worked on" description: "This will not be worked on"
color: "FFFFFF" color: "FFFFFF"

View File

@ -318,7 +318,8 @@ jobs:
SYSTEM_MAXFILESIZE: "100" SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true" METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false" SYSTEM_GOOGLEVISIBILITY: "false"
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:${V2_PORT}" SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
restart: on-failure:5 restart: on-failure:5
stirling-pdf-v2-frontend: stirling-pdf-v2-frontend:

View File

@ -47,7 +47,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup GitHub App Bot - name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]' if: github.actor != 'dependabot[bot]'
@ -158,7 +158,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup GitHub App Bot - name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]' if: github.actor != 'dependabot[bot]'
@ -170,7 +170,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with: with:
repository: ${{ needs.check-comment.outputs.pr_repository }} repository: ${{ needs.check-comment.outputs.pr_repository }}
ref: ${{ needs.check-comment.outputs.pr_ref }} ref: ${{ needs.check-comment.outputs.pr_ref }}

View File

@ -26,7 +26,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup GitHub App Bot - name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]' if: github.actor != 'dependabot[bot]'

View File

@ -23,7 +23,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis - name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true' if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis id: ai-title-analysis
uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7 uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
with: with:
model: openai/gpt-4o model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt" system-prompt-file: ".github/config/system-prompt.txt"

View File

@ -17,7 +17,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup GitHub App Bot - name: Setup GitHub App Bot
id: setup-bot id: setup-bot

View File

@ -31,8 +31,7 @@ jobs:
project: ${{ steps.changes.outputs.project }} project: ${{ steps.changes.outputs.project }}
openapi: ${{ steps.changes.outputs.openapi }} openapi: ${{ steps.changes.outputs.openapi }}
steps: steps:
- uses: actions/checkout@v4.3.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check for file changes - name: Check for file changes
uses: dorny/paths-filter@v3.0.2 uses: dorny/paths-filter@v3.0.2
@ -56,14 +55,15 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.3.0
- name: Set up JDK ${{ matrix.jdk-version }} - name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@v4.7.1 uses: actions/setup-java@v4.7.1
with: with:
java-version: ${{ matrix.jdk-version }} java-version: ${{ matrix.jdk-version }}
distribution: "temurin" distribution: "temurin"
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.4.1 uses: gradle/actions/setup-gradle@v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14
- name: Build with Gradle and spring security ${{ matrix.spring-security }} - name: Build with Gradle and spring security ${{ matrix.spring-security }}
@ -106,18 +106,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@v2.12.2 uses: step-security/harden-runner@v2.13.0
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@v4.2.2
- name: Checkout repository
uses: actions/checkout@v4.3.0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4.7.1 uses: actions/setup-java@v4.7.1
with: with:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4.4.1 - uses: gradle/actions/setup-gradle@v4.4.2
- name: Generate OpenAPI documentation - name: Generate OpenAPI documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs run: ./gradlew :stirling-pdf:generateOpenApiDocs
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation - name: Upload OpenAPI Documentation
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
@ -163,7 +168,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.3.0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4.7.1 uses: actions/setup-java@v4.7.1
with: with:
@ -205,7 +210,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up Java 17 - name: Set up Java 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -254,7 +259,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -263,7 +268,7 @@ jobs:
distribution: "temurin" distribution: "temurin"
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14

View File

@ -35,7 +35,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout main branch first - name: Checkout main branch first
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup GitHub App Bot - name: Setup GitHub App Bot
id: setup-bot id: setup-bot

View File

@ -22,6 +22,6 @@ jobs:
egress-policy: audit egress-policy: audit
- name: "Checkout Repository" - name: "Checkout Repository"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: "Dependency Review" - name: "Dependency Review"
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1

View File

@ -151,7 +151,8 @@ jobs:
SYSTEM_MAXFILESIZE: "100" SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true" METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false" SYSTEM_GOOGLEVISIBILITY: "false"
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:3000" SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
baseUrl: "https://demo.stirlingpdf.cloud"
restart: on-failure:5 restart: on-failure:5
frontend: frontend:

View File

@ -36,7 +36,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
@ -54,7 +54,7 @@ jobs:
distribution: "temurin" distribution: "temurin"
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Check licenses for compatibility - name: Check licenses for compatibility
run: ./gradlew clean checkLicense run: ./gradlew clean checkLicense

View File

@ -20,7 +20,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Run Labeler - name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0 uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0

View File

@ -25,7 +25,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -64,7 +64,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -72,7 +72,7 @@ jobs:
java-version: "21" java-version: "21"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14
@ -115,7 +115,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
name: stirling-${{ matrix.file_suffix }}binaries name: stirling-${{ matrix.file_suffix }}binaries
@ -152,7 +152,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -160,7 +160,7 @@ jobs:
java-version: "21" java-version: "21"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14
@ -243,7 +243,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
name: ${{ matrix.platform }}binaries name: ${{ matrix.platform }}binaries
@ -306,7 +306,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Download signed artifacts - name: Download signed artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -R run: ls -R
- name: Upload binaries, attestations and signatures to Release and create GitHub Release - name: Upload binaries, attestations and signatures to Release and create GitHub Release

View File

@ -22,7 +22,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@ -34,7 +34,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -42,7 +42,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14

View File

@ -27,7 +27,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -35,7 +35,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14
@ -88,7 +88,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
name: binaries${{ matrix.file_suffix }} name: binaries${{ matrix.file_suffix }}
- name: Display structure of downloaded files - name: Display structure of downloaded files
@ -166,7 +166,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Download signed artifacts - name: Download signed artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
name: signed${{ matrix.file_suffix }} name: signed${{ matrix.file_suffix }}

View File

@ -39,7 +39,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with: with:
persist-credentials: false persist-credentials: false
@ -74,6 +74,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -34,12 +34,12 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Build and analyze with Gradle - name: Build and analyze with Gradle
env: env:

View File

@ -30,7 +30,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -38,7 +38,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Generate Swagger documentation - name: Generate Swagger documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs run: ./gradlew :stirling-pdf:generateOpenApiDocs

View File

@ -36,7 +36,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup GitHub App Bot - name: Setup GitHub App Bot
id: setup-bot id: setup-bot

View File

@ -29,7 +29,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@ -38,7 +38,7 @@ jobs:
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with: with:
gradle-version: 8.14 gradle-version: 8.14
@ -126,7 +126,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up Node - name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0

View File

@ -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.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -48,6 +49,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;
}
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete /* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
@Bean @Bean
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true") @ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
@ -119,7 +128,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")
@ -139,8 +148,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;
} }

View File

@ -25,6 +25,7 @@ public class InstallationPathConfig {
private static final String STATIC_PATH; private static final String STATIC_PATH;
private static final String TEMPLATES_PATH; private static final String TEMPLATES_PATH;
private static final String SIGNATURES_PATH; private static final String SIGNATURES_PATH;
private static final String PRIVATE_KEY_PATH;
static { static {
BASE_PATH = initializeBasePath(); BASE_PATH = initializeBasePath();
@ -45,6 +46,7 @@ public class InstallationPathConfig {
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator; STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator; TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator; SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
PRIVATE_KEY_PATH = CONFIG_PATH + "db" + File.separator + "keys" + File.separator;
} }
private static String initializeBasePath() { private static String initializeBasePath() {
@ -120,4 +122,8 @@ public class InstallationPathConfig {
public static String getSignaturesPath() { public static String getSignaturesPath() {
return SIGNATURES_PATH; return SIGNATURES_PATH;
} }
public static String getPrivateKeyPath() {
return PRIVATE_KEY_PATH;
}
} }

View File

@ -119,6 +119,7 @@ public class ApplicationProperties {
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
private String loginMethod = "all"; private String loginMethod = "all";
private String customGlobalAPIKey; private String customGlobalAPIKey;
private Jwt jwt = new Jwt();
public Boolean isAltLogin() { public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled(); return saml2.getEnabled() || oauth2.getEnabled();
@ -298,6 +299,15 @@ public class ApplicationProperties {
} }
} }
} }
@Data
public static class Jwt {
private boolean enableKeystore = true;
private boolean enableKeyRotation = false;
private boolean enableKeyCleanup = true;
private int keyRetentionDays = 7;
private boolean secureCookie;
}
} }
@Data @Data
@ -362,7 +372,8 @@ public class ApplicationProperties {
public String getBaseTmpDir() { public String getBaseTmpDir() {
return baseTmpDir != null && !baseTmpDir.isEmpty() return baseTmpDir != null && !baseTmpDir.isEmpty()
? baseTmpDir ? baseTmpDir
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf"; : java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
+ "/stirling-pdf";
} }
@JsonIgnore @JsonIgnore

View File

@ -14,8 +14,10 @@ public class RequestUriUtils {
|| requestURI.startsWith(contextPath + "/images/") || requestURI.startsWith(contextPath + "/images/")
|| requestURI.startsWith(contextPath + "/public/") || requestURI.startsWith(contextPath + "/public/")
|| requestURI.startsWith(contextPath + "/pdfjs/") || requestURI.startsWith(contextPath + "/pdfjs/")
|| requestURI.startsWith(contextPath + "/pdfjs-legacy/")
|| requestURI.startsWith(contextPath + "/login") || requestURI.startsWith(contextPath + "/login")
|| requestURI.startsWith(contextPath + "/error") || requestURI.startsWith(contextPath + "/error")
|| requestURI.startsWith(contextPath + "/favicon")
|| requestURI.endsWith(".svg") || requestURI.endsWith(".svg")
|| requestURI.endsWith(".png") || requestURI.endsWith(".png")
|| requestURI.endsWith(".ico") || requestURI.endsWith(".ico")

View File

@ -37,7 +37,6 @@ public class ConvertHtmlToPDF {
private final CustomHtmlSanitizer customHtmlSanitizer; private final CustomHtmlSanitizer customHtmlSanitizer;
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf") @AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation( @Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description = description =

View File

@ -46,7 +46,6 @@ public class ConvertMarkdownToPdf {
private final CustomHtmlSanitizer customHtmlSanitizer; private final CustomHtmlSanitizer customHtmlSanitizer;
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation( @Operation(
summary = "Convert a Markdown file to PDF", summary = "Convert a Markdown file to PDF",
description = description =

View File

@ -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
@ -56,3 +56,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=false

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=لقد تسجل دخولًا إلى
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى. login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
login.toManySessions=لديك عدة جلسات نشطة login.toManySessions=لديك عدة جلسات نشطة
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=حجب تلقائي autoRedact.title=حجب تلقائي

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Siz artıq daxil olmusunuz
login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin. login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin.
login.toManySessions=Həddindən artıq aktiv sessiyanız var login.toManySessions=Həddindən artıq aktiv sessiyanız var
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Avtomatik Gizlətmə autoRedact.title=Avtomatik Gizlətmə

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Вече сте влезли в
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново. login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
login.toManySessions=Имате твърде много активни сесии login.toManySessions=Имате твърде много активни сесии
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Автоматично редактиране autoRedact.title=Автоматично редактиране

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=ཁྱེད་རང་
login.alreadyLoggedIn2=སྒྲིག་ཆས་ནང་ནང་འཛུལ་བྱས་ཟིན། སྒྲིག་ཆས་ནས་ཕྱིར་འཐེན་བྱས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས། login.alreadyLoggedIn2=སྒྲིག་ཆས་ནང་ནང་འཛུལ་བྱས་ཟིན། སྒྲིག་ཆས་ནས་ཕྱིར་འཐེན་བྱས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།
login.toManySessions=ཁྱེད་ལ་འཛུལ་ཞུགས་བྱས་པའི་གནས་སྐབས་མང་དྲགས་འདུག login.toManySessions=ཁྱེད་ལ་འཛུལ་ཞུགས་བྱས་པའི་གནས་སྐབས་མང་དྲགས་འདུག
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=རང་འགུལ་སྒྲིབ་སྲུང་། autoRedact.title=རང་འགུལ་སྒྲིབ་སྲུང་།

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Ja has iniciat sessió a
login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar. login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar.
login.toManySessions=Tens massa sessions actives login.toManySessions=Tens massa sessions actives
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Redacció Automàtica autoRedact.title=Redacció Automàtica

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Již jste přihlášeni na
login.alreadyLoggedIn2=zařízeních. Odhlaste se prosím z těchto zařízení a zkuste to znovu. login.alreadyLoggedIn2=zařízeních. Odhlaste se prosím z těchto zařízení a zkuste to znovu.
login.toManySessions=Máte příliš mnoho aktivních relací login.toManySessions=Máte příliš mnoho aktivních relací
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatické začernění autoRedact.title=Automatické začernění

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Du er allerede logget ind på
login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen. login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen.
login.toManySessions=Du har for mange aktive sessoner login.toManySessions=Du har for mange aktive sessoner
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Rediger autoRedact.title=Auto Rediger

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Sie sind bereits an
login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut. login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut.
login.toManySessions=Sie haben zu viele aktive Sitzungen login.toManySessions=Sie haben zu viele aktive Sitzungen
login.logoutMessage=Sie wurden erfolgreich abgemeldet. login.logoutMessage=Sie wurden erfolgreich abgemeldet.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatisch zensieren/schwärzen autoRedact.title=Automatisch zensieren/schwärzen

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Είστε ήδη συνδεδεμένοι σε
login.alreadyLoggedIn2=συσκευές. Παρακαλώ αποσυνδεθείτε από τις συσκευές και προσπαθήστε ξανά. login.alreadyLoggedIn2=συσκευές. Παρακαλώ αποσυνδεθείτε από τις συσκευές και προσπαθήστε ξανά.
login.toManySessions=Έχετε πάρα πολλές ενεργές συνεδρίες login.toManySessions=Έχετε πάρα πολλές ενεργές συνεδρίες
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Αυτόματη απόκρυψη autoRedact.title=Αυτόματη απόκρυψη

View File

@ -893,7 +893,7 @@ login.rememberme=Remember me
login.invalid=Invalid username or password. login.invalid=Invalid username or password.
login.locked=Your account has been locked. login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-On
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Ya ha iniciado sesión en
login.alreadyLoggedIn2=dispositivos. Cierre sesión en los dispositivos y vuelva a intentarlo. login.alreadyLoggedIn2=dispositivos. Cierre sesión en los dispositivos y vuelva a intentarlo.
login.toManySessions=Tiene demasiadas sesiones activas login.toManySessions=Tiene demasiadas sesiones activas
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Censurar Texto autoRedact.title=Auto Censurar Texto

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Idatzi autoRedact.title=Auto Idatzi

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=شما قبلاً وارد شده‌اید در
login.alreadyLoggedIn2=دستگاه‌ها. لطفاً از دستگاه‌ها خارج شده و دوباره تلاش کنید. login.alreadyLoggedIn2=دستگاه‌ها. لطفاً از دستگاه‌ها خارج شده و دوباره تلاش کنید.
login.toManySessions=شما تعداد زیادی نشست فعال دارید. login.toManySessions=شما تعداد زیادی نشست فعال دارید.
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=سانسور خودکار autoRedact.title=سانسور خودکار

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Vous êtes déjà connecté sur
login.alreadyLoggedIn2=appareils. Veuillez vous déconnecter des appareils et réessayer. login.alreadyLoggedIn2=appareils. Veuillez vous déconnecter des appareils et réessayer.
login.toManySessions=Vous avez trop de sessions actives. login.toManySessions=Vous avez trop de sessions actives.
login.logoutMessage=Vous avez été déconnecté. login.logoutMessage=Vous avez été déconnecté.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Caviarder automatiquement autoRedact.title=Caviarder automatiquement

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Tá tú logáilte isteach cheana
login.alreadyLoggedIn2=gléasanna. Logáil amach as na gléasanna agus bain triail eile as. login.alreadyLoggedIn2=gléasanna. Logáil amach as na gléasanna agus bain triail eile as.
login.toManySessions=Tá an iomarca seisiún gníomhach agat login.toManySessions=Tá an iomarca seisiún gníomhach agat
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=आप पहले से ही
login.alreadyLoggedIn2=उपकरणों में लॉग इन हैं। कृपया उपकरणों से लॉग आउट करें और पुनः प्रयास करें। login.alreadyLoggedIn2=उपकरणों में लॉग इन हैं। कृपया उपकरणों से लॉग आउट करें और पुनः प्रयास करें।
login.toManySessions=आपके बहुत सारे सक्रिय सत्र हैं login.toManySessions=आपके बहुत सारे सक्रिय सत्र हैं
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=स्वतः गोपनीयकरण autoRedact.title=स्वतः गोपनीयकरण

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Već ste se prijavili na
login.alreadyLoggedIn2=ure. Odjavite se s ure i pokušajte ponovo. login.alreadyLoggedIn2=ure. Odjavite se s ure i pokušajte ponovo.
login.toManySessions=Imate preko mrežne sesije aktivnih login.toManySessions=Imate preko mrežne sesije aktivnih
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatsko uređivanje autoRedact.title=Automatsko uređivanje

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Már be van jelentkezve
login.alreadyLoggedIn2=eszközön. Kérjük, jelentkezzen ki az eszközökről és próbálja újra. login.alreadyLoggedIn2=eszközön. Kérjük, jelentkezzen ki az eszközökről és próbálja újra.
login.toManySessions=Túl sok aktív munkamenet login.toManySessions=Túl sok aktív munkamenet
login.logoutMessage=Sikeresen kijelentkezett. login.logoutMessage=Sikeresen kijelentkezett.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatikus kitakarás autoRedact.title=Automatikus kitakarás

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Anda sudah login ke
login.alreadyLoggedIn2=perangkat. Silakan keluar dari perangkat dan coba lagi. login.alreadyLoggedIn2=perangkat. Silakan keluar dari perangkat dan coba lagi.
login.toManySessions=Anda memiliki terlalu banyak sesi aktif login.toManySessions=Anda memiliki terlalu banyak sesi aktif
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Redaksional Otomatis autoRedact.title=Redaksional Otomatis

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Hai già effettuato l'accesso a
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova. login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
login.toManySessions=Hai troppe sessioni attive login.toManySessions=Hai troppe sessioni attive
login.logoutMessage=Sei stato disconnesso. login.logoutMessage=Sei stato disconnesso.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Redazione automatica autoRedact.title=Redazione automatica
@ -1773,7 +1774,7 @@ audit.dashboard.filter.userPlaceholder=Filtra per utente
audit.dashboard.filter.startDate=Data di inizio audit.dashboard.filter.startDate=Data di inizio
audit.dashboard.filter.endDate=Data di fine audit.dashboard.filter.endDate=Data di fine
audit.dashboard.filter.apply=Applica filtri audit.dashboard.filter.apply=Applica filtri
audit.dashboard.filter.reset=Resetta Filtri audit.dashboard.filter.reset=Resetta filtri
# Table Headers # Table Headers
audit.dashboard.table.id=ID audit.dashboard.table.id=ID

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=すでにログインしています
login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。 login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。
login.toManySessions=アクティブなセッションが多すぎます login.toManySessions=アクティブなセッションが多すぎます
login.logoutMessage=ログアウトしました login.logoutMessage=ログアウトしました
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=自動墨消し autoRedact.title=自動墨消し

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=이미 다음에 로그인되어 있습니다
login.alreadyLoggedIn2=개의 기기. 해당 기기에서 로그아웃한 후 다시 시도하세요. login.alreadyLoggedIn2=개의 기기. 해당 기기에서 로그아웃한 후 다시 시도하세요.
login.toManySessions=활성 세션이 너무 많습니다 login.toManySessions=활성 세션이 너무 많습니다
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=자동 검열 autoRedact.title=자동 검열

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=നിങ്ങൾ ഇതിനകം ലോഗിൻ ച
login.alreadyLoggedIn2=ഉപകരണങ്ങളിൽ. ദയവായി ഉപകരണങ്ങളിൽ നിന്ന് ലോഗ് ഔട്ട് ചെയ്ത് വീണ്ടും ശ്രമിക്കുക. login.alreadyLoggedIn2=ഉപകരണങ്ങളിൽ. ദയവായി ഉപകരണങ്ങളിൽ നിന്ന് ലോഗ് ഔട്ട് ചെയ്ത് വീണ്ടും ശ്രമിക്കുക.
login.toManySessions=നിങ്ങൾക്ക് വളരെയധികം സജീവ സെഷനുകൾ ഉണ്ട് login.toManySessions=നിങ്ങൾക്ക് വളരെയധികം സജീവ സെഷനുകൾ ഉണ്ട്
login.logoutMessage=നിങ്ങൾ ലോഗ് ഔട്ട് ചെയ്തു. login.logoutMessage=നിങ്ങൾ ലോഗ് ഔട്ട് ചെയ്തു.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=സ്വയം റെഡാക്റ്റ് ചെയ്യുക autoRedact.title=സ്വയം റെഡാക്റ്റ് ചെയ്യുക

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=U zit reeds ingelogd bij
login.alreadyLoggedIn2=apparaten. U moet u a.u.b. uitloggen van de apparaten en opnieuw proberen. login.alreadyLoggedIn2=apparaten. U moet u a.u.b. uitloggen van de apparaten en opnieuw proberen.
login.toManySessions=U heeft te veel actieve sessies login.toManySessions=U heeft te veel actieve sessies
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatisch censureren autoRedact.title=Automatisch censureren

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Du er allerede innlogget på
login.alreadyLoggedIn2=enheter. Logg ut og forsøk igjen login.alreadyLoggedIn2=enheter. Logg ut og forsøk igjen
login.toManySessions=Du har for mange aktive økter login.toManySessions=Du har for mange aktive økter
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatisk Sensurering autoRedact.title=Automatisk Sensurering

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Jesteś już zalogowany na
login.alreadyLoggedIn2=urządzeniach. Wyloguj się z tych urządzeń i spróbuj ponownie. login.alreadyLoggedIn2=urządzeniach. Wyloguj się z tych urządzeń i spróbuj ponownie.
login.toManySessions=Masz zbyt wiele aktywnych sesji login.toManySessions=Masz zbyt wiele aktywnych sesji
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatyczne zaciemnienie autoRedact.title=Automatyczne zaciemnienie

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Você já está conectado em
login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente. login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente.
login.toManySessions=Você tem muitas sessões ativas login.toManySessions=Você tem muitas sessões ativas
login.logoutMessage=Você foi desconectado. login.logoutMessage=Você foi desconectado.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Ocultação de Texto Automática autoRedact.title=Ocultação de Texto Automática

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Já tem sessão iniciada em
login.alreadyLoggedIn2=dispositivos. Por favor termine sessão nesses dispositivos e tente novamente. login.alreadyLoggedIn2=dispositivos. Por favor termine sessão nesses dispositivos e tente novamente.
login.toManySessions=Tem demasiadas sessões ativas login.toManySessions=Tem demasiadas sessões ativas
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Redação Automática autoRedact.title=Redação Automática

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Redactare Automată autoRedact.title=Redactare Automată

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Вы уже вошли в
login.alreadyLoggedIn2=устройств(а). Пожалуйста, выйдите из этих устройств и попробуйте снова. login.alreadyLoggedIn2=устройств(а). Пожалуйста, выйдите из этих устройств и попробуйте снова.
login.toManySessions=У вас слишком много активных сессий login.toManySessions=У вас слишком много активных сессий
login.logoutMessage=Вы вышли из системы. login.logoutMessage=Вы вышли из системы.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Автоматическое редактирование autoRedact.title=Автоматическое редактирование

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatické redigovanie autoRedact.title=Automatické redigovanie

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Prijavljeni ste že v
login.alreadyLoggedIn2=naprave. Odjavite se iz naprav in poskusite znova. login.alreadyLoggedIn2=naprave. Odjavite se iz naprav in poskusite znova.
login.toManySessions=Imate preveč aktivnih sej login.toManySessions=Imate preveč aktivnih sej
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Samodejno redigiraj autoRedact.title=Samodejno redigiraj

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Već si prijavljen na
login.alreadyLoggedIn2=uređaja. Odjavi se sa uređaja i pokušaj ponovo. login.alreadyLoggedIn2=uređaja. Odjavi se sa uređaja i pokušaj ponovo.
login.toManySessions=Imaš previše aktivnih sesija login.toManySessions=Imaš previše aktivnih sesija
login.logoutMessage=Odjavljen si. login.logoutMessage=Odjavljen si.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Automatsko cenzurisanje autoRedact.title=Automatsko cenzurisanje

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Du är redan inloggad på
login.alreadyLoggedIn2=enheter. Logga ut från enheterna och försök igen. login.alreadyLoggedIn2=enheter. Logga ut från enheterna och försök igen.
login.toManySessions=Du har för många aktiva sessioner login.toManySessions=Du har för många aktiva sessioner
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto-redigera autoRedact.title=Auto-redigera

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=คุณได้เข้าสู่ระบบใน
login.alreadyLoggedIn2=อุปกรณ์แล้ว กรุณาออกจากระบบจากอุปกรณ์ที่ใช้งานอยู่แล้ว จากนั้นลองใหม่อีกครั้ง login.alreadyLoggedIn2=อุปกรณ์แล้ว กรุณาออกจากระบบจากอุปกรณ์ที่ใช้งานอยู่แล้ว จากนั้นลองใหม่อีกครั้ง
login.toManySessions=คุณมีการเข้าสู่ระบบพร้อมกันเกินกว่ากำหนด login.toManySessions=คุณมีการเข้าสู่ระบบพร้อมกันเกินกว่ากำหนด
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=ซ่อนข้อมูลอัตโนมัติ autoRedact.title=ซ่อนข้อมูลอัตโนมัติ

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Zaten şu cihazlarda oturum açılmış:
login.alreadyLoggedIn2=Lütfen bu cihazlardan çıkış yaparak tekrar deneyin. login.alreadyLoggedIn2=Lütfen bu cihazlardan çıkış yaparak tekrar deneyin.
login.toManySessions=Çok fazla aktif oturumunuz var login.toManySessions=Çok fazla aktif oturumunuz var
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Otomatik Karartma autoRedact.title=Otomatik Karartma

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=Ви вже увійшли до
login.alreadyLoggedIn2=пристроїв (а). Будь ласка, вийдіть із цих пристроїв і спробуйте знову. login.alreadyLoggedIn2=пристроїв (а). Будь ласка, вийдіть із цих пристроїв і спробуйте знову.
login.toManySessions=У вас дуже багато активних сесій login.toManySessions=У вас дуже багато активних сесій
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Автоматичне редагування autoRedact.title=Автоматичне редагування

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Tự động biên tập autoRedact.title=Tự động biên tập

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=您已经登录到了
login.alreadyLoggedIn2=设备,请注销设备后重试。 login.alreadyLoggedIn2=设备,请注销设备后重试。
login.toManySessions=你已经有太多的会话了。请注销一些设备后重试。 login.toManySessions=你已经有太多的会话了。请注销一些设备后重试。
login.logoutMessage=您已退出登录。 login.logoutMessage=您已退出登录。
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=自动删除 autoRedact.title=自动删除

View File

@ -908,6 +908,7 @@ login.alreadyLoggedIn=您已經登入了
login.alreadyLoggedIn2=部裝置。請先從這些裝置登出後再試一次。 login.alreadyLoggedIn2=部裝置。請先從這些裝置登出後再試一次。
login.toManySessions=您有太多使用中的工作階段 login.toManySessions=您有太多使用中的工作階段
login.logoutMessage=您已登出。 login.logoutMessage=您已登出。
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=自動塗黑 autoRedact.title=自動塗黑

View File

@ -59,12 +59,17 @@ 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: # This feature is currently under development and not yet fully supported. Do not use in production.
persistence: true # Set to 'true' to enable JWT key store
enableKeyRotation: true # Set to 'true' to enable key pair rotation
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
secureCookie: false # Set to 'true' to use secure cookies for JWTs
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: false # Enable license key checks for pro/enterprise features
proFeatures: proFeatures:
database: true # Enable database features
SSOAutoLogin: false SSOAutoLogin: false
CustomMetadata: CustomMetadata:
autoUpdateMetadata: false autoUpdateMetadata: false

View File

@ -132,6 +132,13 @@
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
{
"moduleName": "com.github.ben-manes.caffeine:caffeine",
"moduleUrl": "https://github.com/ben-manes/caffeine",
"moduleVersion": "3.2.2",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{ {
"moduleName": "com.github.jai-imageio:jai-imageio-core", "moduleName": "com.github.jai-imageio:jai-imageio-core",
"moduleUrl": "https://github.com/jai-imageio/jai-imageio-core", "moduleUrl": "https://github.com/jai-imageio/jai-imageio-core",
@ -168,7 +175,7 @@
{ {
"moduleName": "com.google.errorprone:error_prone_annotations", "moduleName": "com.google.errorprone:error_prone_annotations",
"moduleUrl": "https://errorprone.info/error_prone_annotations", "moduleUrl": "https://errorprone.info/error_prone_annotations",
"moduleVersion": "2.38.0", "moduleVersion": "2.40.0",
"moduleLicense": "Apache 2.0", "moduleLicense": "Apache 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
@ -549,6 +556,27 @@
"moduleLicense": "MIT License", "moduleLicense": "MIT License",
"moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php" "moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php"
}, },
{
"moduleName": "io.jsonwebtoken:jjwt-api",
"moduleUrl": "https://github.com/jwtk/jjwt",
"moduleVersion": "0.12.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.jsonwebtoken:jjwt-impl",
"moduleUrl": "https://github.com/jwtk/jjwt",
"moduleVersion": "0.12.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.jsonwebtoken:jjwt-jackson",
"moduleUrl": "https://github.com/jwtk/jjwt",
"moduleVersion": "0.12.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "io.micrometer:micrometer-commons", "moduleName": "io.micrometer:micrometer-commons",
"moduleUrl": "https://github.com/micrometer-metrics/micrometer", "moduleUrl": "https://github.com/micrometer-metrics/micrometer",
@ -1507,6 +1535,13 @@
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "org.springframework.boot:spring-boot-starter-cache",
"moduleUrl": "https://spring.io/projects/spring-boot",
"moduleVersion": "3.5.4",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "org.springframework.boot:spring-boot-starter-data-jpa", "moduleName": "org.springframework.boot:spring-boot-starter-data-jpa",
"moduleUrl": "https://spring.io/projects/spring-boot", "moduleUrl": "https://spring.io/projects/spring-boot",

View File

@ -46,10 +46,9 @@ export class DecryptFile {
formData.append('password', password); formData.append('password', password);
} }
// Send decryption request // Send decryption request
const response = await fetch('/api/v1/security/remove-password', { const response = await fetchWithCsrf('/api/v1/security/remove-password', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: csrfToken ? {'X-XSRF-TOKEN': csrfToken} : undefined,
}); });
if (response.ok) { if (response.ok) {

View File

@ -218,7 +218,7 @@
formData.append('password', password); formData.append('password', password);
// Use handleSingleDownload to send the request // Use handleSingleDownload to send the request
const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData}); const decryptionResult = await fetchWithCsrf(removePasswordUrl, {method: 'POST', body: formData});
if (decryptionResult && decryptionResult.blob) { if (decryptionResult && decryptionResult.blob) {
const decryptedBlob = await decryptionResult.blob(); const decryptedBlob = await decryptionResult.blob();

View File

@ -1,3 +1,29 @@
// Authentication utility for cookie-based JWT
window.JWTManager = {
// Logout - clear cookies and redirect to login
logout: function() {
// Clear JWT cookie manually (fallback)
document.cookie = 'stirling_jwt=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=None; Secure';
// Perform logout request to clear server-side session
fetch('/logout', {
method: 'POST',
credentials: 'include'
}).then(response => {
if (response.redirected) {
window.location.href = response.url;
} else {
window.location.href = '/login?logout=true';
}
}).catch(() => {
// If logout fails, let server handle it
window.location.href = '/logout';
});
}
};
window.fetchWithCsrf = async function(url, options = {}) { window.fetchWithCsrf = async function(url, options = {}) {
function getCsrfToken() { function getCsrfToken() {
const cookieValue = document.cookie const cookieValue = document.cookie
@ -24,5 +50,18 @@ window.fetchWithCsrf = async function(url, options = {}) {
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken; fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
} }
return fetch(url, fetchOptions); // Always include credentials to send JWT cookies
fetchOptions.credentials = 'include';
// Make the request
const response = await fetch(url, fetchOptions);
// Handle 401 responses (unauthorized)
if (response.status === 401) {
console.warn('Authentication failed, redirecting to login');
window.JWTManager.logout();
return response;
}
return response;
} }

View File

@ -0,0 +1,44 @@
// JWT Authentication Management Script
// This script handles cookie-based JWT authentication and page access control
(function() {
// Clean up JWT token from URL parameters after OAuth/Login flows
function cleanupTokenFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
const hasToken = urlParams.get('jwt') || urlParams.get('token');
if (hasToken) {
// Clean up URL by removing token parameter
// Token should now be set as cookie by server
urlParams.delete('jwt');
urlParams.delete('token');
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
window.history.replaceState({}, '', newUrl);
}
}
// Initialize JWT handling when page loads
function initializeJWT() {
// Clean up any JWT tokens from URL (OAuth flow)
cleanupTokenFromUrl();
// Authentication is handled server-side
// If user is not authenticated, server will redirect to login
console.log('JWT initialization complete - authentication handled server-side');
}
// No form enhancement needed for cookie-based JWT
// Cookies are automatically sent with form submissions
function enhanceFormSubmissions() {
// Cookie-based JWT is automatically included in form submissions
// No additional processing needed
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initializeJWT();
});
} else {
initializeJWT();
}
})();

View File

@ -138,5 +138,19 @@ document.addEventListener('DOMContentLoaded', () => {
tooltipSetup(); tooltipSetup();
setupDropdowns(); setupDropdowns();
fixNavbarDropdownStyles(); fixNavbarDropdownStyles();
// Setup logout button functionality
const logoutButton = document.querySelector('a[href="/logout"]');
if (logoutButton) {
logoutButton.addEventListener('click', function(event) {
event.preventDefault();
if (window.JWTManager) {
window.JWTManager.logout();
} else {
// Fallback if JWTManager is not available
window.location.href = '/logout';
}
});
}
}); });
window.addEventListener('resize', fixNavbarDropdownStyles); window.addEventListener('resize', fixNavbarDropdownStyles);

View File

@ -102,7 +102,7 @@ async function fetchEndpointData() {
refreshBtn.classList.add('refreshing'); refreshBtn.classList.add('refreshing');
refreshBtn.disabled = true; refreshBtn.disabled = true;
const response = await fetch('/api/v1/info/load/all'); const response = await fetchWithCsrf('/api/v1/info/load/all');
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }

View File

@ -390,8 +390,13 @@
key.includes('clientSubmissionOrder') || key.includes('clientSubmissionOrder') ||
key.includes('lastSubmitTime') || key.includes('lastSubmitTime') ||
key.includes('lastClientId') || key.includes('lastClientId') ||
key.includes('stirling_jwt') ||
key.includes('JSESSIONID') ||
key.includes('XSRF-TOKEN') ||
key.includes('remember-me') ||
key.includes('auth') ||
key.includes('token') ||
key.includes('session') ||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') || key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
key.includes('pageViews'); key.includes('pageViews');
} }

View File

@ -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 'src/**/java/**/*.java' target 'src/**/java/**/*.java'
@ -43,6 +49,8 @@ dependencies {
api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-oauth2-client' api 'org.springframework.boot:spring-boot-starter-oauth2-client'
api 'org.springframework.boot:spring-boot-starter-mail' api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-cache'
api 'com.github.ben-manes.caffeine:caffeine'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.35' api 'io.swagger.core.v3:swagger-core-jakarta:2.2.35'
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0' implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
@ -52,6 +60,10 @@ dependencies {
// implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' // Removed - UI moved to React frontend // implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' // Removed - UI moved to React frontend
api 'io.micrometer:micrometer-registry-prometheus' api 'io.micrometer:micrometer-registry-prometheus'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0' implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0'
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 {

View File

@ -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,8 @@ 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.LoginAttemptService; import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@ -24,13 +27,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,6 +53,15 @@ public class CustomAuthenticationSuccessHandler
} }
loginAttemptService.loginSucceeded(userName); loginAttemptService.loginSucceeded(userName);
if (jwtService.isJwtEnabled()) {
String jwt =
jwtService.generateToken(
authentication, Map.of("authType", AuthenticationType.WEB));
jwtService.addToken(response, jwt);
log.debug("JWT generated for user: {}", userName);
getRedirectStrategy().sendRedirect(request, response, "/");
} else {
// Get the saved request // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
SavedRequest savedRequest = SavedRequest savedRequest =
@ -59,10 +75,9 @@ public class CustomAuthenticationSuccessHandler
// Redirect to the original destination // Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
// Redirect to the root URL (considering context path) // No saved request or it's a static resource, redirect to home page
getRedirectStrategy().sendRedirect(request, response, "/"); getRedirectStrategy().sendRedirect(request, response, "/");
} }
}
// super.onAuthenticationSuccess(request, response, authentication);
} }
} }

View File

@ -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,18 @@ 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 {
if (!response.isCommitted()) { if (!response.isCommitted()) {
if (authentication != null) { if (authentication != null) {
if (authentication instanceof Saml2Authentication samlAuthentication) { if (authentication instanceof Saml2Authentication samlAuthentication) {
@ -67,6 +71,9 @@ 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.extractToken(request).isBlank()) {
jwtService.clearToken(response);
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
} else { } else {
// Redirect to login page after logout // Redirect to login page after logout
String path = checkForErrors(request); String path = checkForErrors(request);
@ -82,7 +89,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 +134,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;

View File

@ -43,7 +43,6 @@ public class InitialSecuritySetup {
} }
} }
userService.migrateOauth2ToSSO();
assignUsersToDefaultTeamIfMissing(); assignUsersToDefaultTeamIfMissing();
initializeInternalApiUser(); initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {

View File

@ -0,0 +1,22 @@
package stirling.software.proprietary.security;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}

View File

@ -74,8 +74,11 @@ public class AccountWebController {
// @GetMapping("/login") // @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
// If the user is already authenticated, redirect them to the home page. // If the user is already authenticated and it's not a logout scenario, redirect them to the
if (authentication != null && authentication.isAuthenticated()) { // home page.
if (authentication != null
&& authentication.isAuthenticated()
&& request.getParameter("logout") == null) {
return "redirect:/"; return "redirect:/";
} }
@ -181,7 +184,7 @@ public class AccountWebController {
errorOAuth = "login.relyingPartyRegistrationNotFound"; errorOAuth = "login.relyingPartyRegistrationNotFound";
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to"; case "invalid_in_response_to" -> errorOAuth = "login.invalidInResponseTo";
case "not_authentication_provider_found" -> case "not_authentication_provider_found" ->
errorOAuth = "login.not_authentication_provider_found"; errorOAuth = "login.not_authentication_provider_found";
} }

View File

@ -0,0 +1,31 @@
package stirling.software.proprietary.security.configuration;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
@Configuration
@EnableCaching
public class CacheConfig {
@Value("${security.jwt.keyRetentionDays}")
private int keyRetentionDays;
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(
Caffeine.newBuilder()
.maximumSize(1000) // Make configurable?
.expireAfterWrite(Duration.ofDays(keyRetentionDays))
.recordStats());
return cacheManager;
}
}

View File

@ -13,6 +13,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
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;
@ -35,10 +36,12 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler; import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler; import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler;
import stirling.software.proprietary.security.CustomLogoutSuccessHandler; import stirling.software.proprietary.security.CustomLogoutSuccessHandler;
import stirling.software.proprietary.security.JwtAuthenticationEntryPoint;
import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl; import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl;
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 +51,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 +68,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 JwtServiceInterface jwtService;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
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 +88,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,
JwtServiceInterface jwtService,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
LoginAttemptService loginAttemptService, LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter, FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry, SessionPersistentRegistry sessionRegistry,
@ -97,8 +105,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.jwtService = jwtService;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
this.firstLoginFilter = firstLoginFilter; this.firstLoginFilter = firstLoginFilter;
this.sessionRegistry = sessionRegistry; this.sessionRegistry = sessionRegistry;
@ -115,14 +125,28 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { if (securityProperties.getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable()); http.csrf(CsrfConfigurer::disable);
} }
if (loginEnabledValue) { if (loginEnabledValue) {
boolean v2Enabled = appConfig.v2Enabled();
if (v2Enabled) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); jwtAuthenticationFilter(),
if (!applicationProperties.getSecurity().getCsrfDisabled()) { UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(
exceptionHandling ->
exceptionHandling.authenticationEntryPoint(
jwtAuthenticationEntryPoint));
}
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class)
.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
if (!securityProperties.getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = CsrfTokenRequestAttributeHandler requestHandler =
@ -156,16 +180,21 @@ public class SecurityConfiguration {
.csrfTokenRepository(cookieRepo) .csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler)); .csrfTokenRequestHandler(requestHandler));
} }
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement( http.sessionManagement(
sessionManagement -> sessionManagement -> {
if (v2Enabled) {
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()));
http.logout( http.logout(
@ -175,10 +204,10 @@ 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("JSESSIONID", "remember-me", "stirling_jwt"));
http.rememberMe( http.rememberMe(
rememberMeConfigurer -> // Use the configurator directly rememberMeConfigurer -> // Use the configurator directly
rememberMeConfigurer rememberMeConfigurer
@ -200,6 +229,7 @@ public class SecurityConfiguration {
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)
@ -217,29 +247,35 @@ public class SecurityConfiguration {
|| trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/") || trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/") || trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith("/pdfjs/")
|| trimmedUri.startsWith("/pdfjs-legacy/")
|| trimmedUri.startsWith("/favicon")
|| trimmedUri.startsWith( || trimmedUri.startsWith(
"/api/v1/info/status"); "/api/v1/info/status")
|| trimmedUri.startsWith("/v1/api-docs")
|| uri.contains("/v1/api-docs");
}) })
.permitAll() .permitAll()
.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 +287,18 @@ public class SecurityConfiguration {
.successHandler( .successHandler(
new CustomOAuth2AuthenticationSuccessHandler( new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
applicationProperties, securityProperties.getOauth2(),
userService)) userService,
jwtService))
.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,8 +306,7 @@ public class SecurityConfiguration {
.permitAll()); .permitAll());
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) { if (securityProperties.isSaml2Active() && runningProOrHigher) {
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider = OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider(); new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter( authenticationProvider.setResponseAuthenticationConverter(
@ -287,8 +323,9 @@ public class SecurityConfiguration {
.successHandler( .successHandler(
new CustomSaml2AuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
applicationProperties, securityProperties.getSaml2(),
userService)) userService,
jwtService))
.failureHandler( .failureHandler(
new CustomSaml2AuthenticationFailureHandler()) new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver( .authenticationRequestResolver(
@ -323,4 +360,14 @@ public class SecurityConfiguration {
public PersistentTokenRepository persistentTokenRepository() { public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(persistentLoginRepository); return new JPATokenRepositoryImpl(persistentLoginRepository);
} }
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(
jwtService,
userService,
userDetailsService,
jwtAuthenticationEntryPoint,
securityProperties);
}
} }

View File

@ -0,0 +1,204 @@
package stirling.software.proprietary.security.filter;
import static stirling.software.common.util.RequestUriUtils.isStaticResource;
import static stirling.software.proprietary.security.model.AuthenticationType.*;
import static stirling.software.proprietary.security.model.AuthenticationType.SAML2;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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.common.model.ApplicationProperties;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import stirling.software.proprietary.security.service.CustomUserDetailsService;
import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.UserService;
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtServiceInterface jwtService;
private final UserService userService;
private final CustomUserDetailsService userDetailsService;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final ApplicationProperties.Security securityProperties;
public JwtAuthenticationFilter(
JwtServiceInterface jwtService,
UserService userService,
CustomUserDetailsService userDetailsService,
AuthenticationEntryPoint authenticationEntryPoint,
ApplicationProperties.Security securityProperties) {
this.jwtService = jwtService;
this.userService = userService;
this.userDetailsService = userDetailsService;
this.authenticationEntryPoint = authenticationEntryPoint;
this.securityProperties = securityProperties;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!jwtService.isJwtEnabled()) {
filterChain.doFilter(request, response);
return;
}
if (isStaticResource(request.getContextPath(), request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
if (!apiKeyExists(request, response)) {
String jwtToken = jwtService.extractToken(request);
if (jwtToken == null) {
// Any unauthenticated requests should redirect to /login
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
if (!requestURI.startsWith(contextPath + "/login")) {
response.sendRedirect("/login");
return;
}
}
try {
jwtService.validateToken(jwtToken);
} catch (AuthenticationFailureException e) {
jwtService.clearToken(response);
handleAuthenticationFailure(request, response, e);
return;
}
Map<String, Object> claims = jwtService.extractClaims(jwtToken);
String tokenUsername = claims.get("sub").toString();
try {
authenticate(request, claims);
} catch (SQLException | UnsupportedProviderException e) {
log.error("Error processing user authentication for user: {}", tokenUsername, e);
handleAuthenticationFailure(
request,
response,
new AuthenticationFailureException(
"Error processing user authentication", e));
return;
}
}
filterChain.doFilter(request, response);
}
private boolean apiKeyExists(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-KEY");
if (apiKey != null && !apiKey.isBlank()) {
try {
Optional<User> user = userService.getUserByApiKey(apiKey);
if (user.isEmpty()) {
handleAuthenticationFailure(
request,
response,
new AuthenticationFailureException("Invalid API Key"));
return false;
}
authentication =
new ApiKeyAuthenticationToken(
user.get(), apiKey, user.get().getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
return true;
} catch (AuthenticationException e) {
handleAuthenticationFailure(
request,
response,
new AuthenticationFailureException("Invalid API Key", e));
return false;
}
}
return false;
}
return true;
}
private void authenticate(HttpServletRequest request, Map<String, Object> claims)
throws SQLException, UnsupportedProviderException {
String username = claims.get("sub").toString();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
processUserAuthenticationType(claims, username);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
} else {
throw new UsernameNotFoundException("User not found: " + username);
}
}
}
private void processUserAuthenticationType(Map<String, Object> claims, String username)
throws SQLException, UnsupportedProviderException {
AuthenticationType authenticationType =
AuthenticationType.valueOf(claims.getOrDefault("authType", WEB).toString());
log.debug("Processing {} login for {} user", authenticationType, username);
switch (authenticationType) {
case OAUTH2 -> {
ApplicationProperties.Security.OAUTH2 oauth2Properties =
securityProperties.getOauth2();
userService.processSSOPostLogin(
username, oauth2Properties.getAutoCreateUser(), OAUTH2);
}
case SAML2 -> {
ApplicationProperties.Security.SAML2 saml2Properties =
securityProperties.getSaml2();
userService.processSSOPostLogin(
username, saml2Properties.getAutoCreateUser(), SAML2);
}
}
}
private void handleAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
authenticationEntryPoint.commence(request, response, authException);
}
}

View File

@ -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;
@ -64,6 +63,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
return; return;
} }
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for session expiration (unsure if needed) // Check for session expiration (unsure if needed)
@ -92,14 +92,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
@ -115,20 +110,19 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
String method = request.getMethod(); String method = request.getMethod();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) { if ("GET".equalsIgnoreCase(method) && !requestURI.startsWith(contextPath + "/login")) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page response.sendRedirect(contextPath + "/login"); // redirect to the login page
return;
} else { } else {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter() response.getWriter()
.write( .write(
"Authentication required. Please provide a X-API-KEY in request" """
+ " header.\n" Authentication required. Please provide a X-API-KEY in request header.
+ "This is found in Settings -> Account Settings -> API Key\n" This is found in Settings -> Account Settings -> API Key
+ "Alternatively you can disable authentication if this is" Alternatively you can disable authentication if this is unexpected.
+ " unexpected"); """);
return;
} }
return;
} }
// Check if the authenticated user is disabled and invalidate their session if so // Check if the authenticated user is disabled and invalidate their session if so
@ -226,11 +220,12 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
} }
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
String[] permitAllPatterns = { String[] permitAllPatterns = {
contextPath + "/login", contextPath + "/login",
contextPath + "/signup",
contextPath + "/register", contextPath + "/register",
contextPath + "/error", contextPath + "/error",
contextPath + "/images/", contextPath + "/images/",
@ -247,6 +242,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
for (String pattern : permitAllPatterns) { for (String pattern : permitAllPatterns) {
if (uri.startsWith(pattern) if (uri.startsWith(pattern)
|| uri.endsWith(".svg") || uri.endsWith(".svg")
|| uri.endsWith(".mjs")
|| uri.endsWith(".png") || uri.endsWith(".png")
|| uri.endsWith(".ico")) { || uri.endsWith(".ico")) {
return true; return true;

View File

@ -2,5 +2,8 @@ package stirling.software.proprietary.security.model;
public enum AuthenticationType { public enum AuthenticationType {
WEB, WEB,
SSO @Deprecated(since = "1.0.2")
SSO,
OAUTH2,
SAML2
} }

View File

@ -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;

View File

@ -0,0 +1,33 @@
package stirling.software.proprietary.security.model;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@NoArgsConstructor
@ToString(onlyExplicitlyIncluded = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class JwtVerificationKey implements Serializable {
@Serial private static final long serialVersionUID = 1L;
@ToString.Include private String keyId;
private String verifyingKey;
@ToString.Include private LocalDateTime createdAt;
public JwtVerificationKey(String keyId, String verifyingKey) {
this.keyId = keyId;
this.verifyingKey = verifyingKey;
this.createdAt = LocalDateTime.now();
}
}

View File

@ -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;

View File

@ -0,0 +1,13 @@
package stirling.software.proprietary.security.model.exception;
import org.springframework.security.core.AuthenticationException;
public class AuthenticationFailureException extends AuthenticationException {
public AuthenticationFailureException(String message) {
super(message);
}
public AuthenticationFailureException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -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 applicationProperties; 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 = applicationProperties.getSecurity().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.addToken(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,22 @@ 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()) { || !userService.isAuthenticationTypeByUsername(username, OAUTH2))
&& 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) {

View File

@ -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;

View File

@ -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 applicationProperties; 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,9 @@ public class CustomSaml2AuthenticationSuccessHandler
savedRequest.getRedirectUrl()); savedRequest.getRedirectUrl());
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
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 +86,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,15 +110,18 @@ public class CustomSaml2AuthenticationSuccessHandler
} }
try { try {
if (saml2.getBlockRegistration() && !userExists) { if (!userExists || saml2Properties.getBlockRegistration()) {
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);
generateJwt(response, authentication);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.debug( log.debug(
@ -124,4 +135,13 @@ public class CustomSaml2AuthenticationSuccessHandler
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} }
} }
private void generateJwt(HttpServletResponse response, Authentication authentication) {
if (jwtService.isJwtEnabled()) {
String jwt =
jwtService.generateToken(
authentication, Map.of("authType", AuthenticationType.SAML2));
jwtService.addToken(response, jwt);
}
}
} }

View File

@ -0,0 +1,135 @@
package stirling.software.proprietary.security.saml2;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@Slf4j
public class JwtSaml2AuthenticationRequestRepository
implements Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest> {
private final Map<String, String> tokenStore;
private final JwtServiceInterface jwtService;
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token";
public JwtSaml2AuthenticationRequestRepository(
Map<String, String> tokenStore,
JwtServiceInterface jwtService,
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
this.tokenStore = tokenStore;
this.jwtService = jwtService;
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
}
@Override
public void saveAuthenticationRequest(
Saml2PostAuthenticationRequest authRequest,
HttpServletRequest request,
HttpServletResponse response) {
if (!jwtService.isJwtEnabled()) {
log.debug("V2 is not enabled, skipping SAMLRequest token storage");
return;
}
if (authRequest == null) {
removeAuthenticationRequest(request, response);
return;
}
Map<String, Object> claims = serializeSamlRequest(authRequest);
String token = jwtService.generateToken("", claims);
String relayState = authRequest.getRelayState();
tokenStore.put(relayState, token);
request.setAttribute(SAML_REQUEST_TOKEN, relayState);
response.addHeader(SAML_REQUEST_TOKEN, relayState);
log.debug("Saved SAMLRequest token with RelayState: {}", relayState);
}
@Override
public Saml2PostAuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
String token = extractTokenFromStore(request);
if (token == null) {
log.debug("No SAMLResponse token found in RelayState");
return null;
}
Map<String, Object> claims = jwtService.extractClaims(token);
return deserializeSamlRequest(claims);
}
@Override
public Saml2PostAuthenticationRequest removeAuthenticationRequest(
HttpServletRequest request, HttpServletResponse response) {
Saml2PostAuthenticationRequest authRequest = loadAuthenticationRequest(request);
String relayStateId = request.getParameter("RelayState");
if (relayStateId != null) {
tokenStore.remove(relayStateId);
log.debug("Removed SAMLRequest token for RelayState ID: {}", relayStateId);
}
return authRequest;
}
private String extractTokenFromStore(HttpServletRequest request) {
String authnRequestId = request.getParameter("RelayState");
if (authnRequestId != null && !authnRequestId.isEmpty()) {
String token = tokenStore.get(authnRequestId);
if (token != null) {
tokenStore.remove(authnRequestId);
log.debug("Retrieved SAMLRequest token for RelayState ID: {}", authnRequestId);
return token;
} else {
log.warn("No SAMLRequest token found for RelayState ID: {}", authnRequestId);
}
}
return null;
}
private Map<String, Object> serializeSamlRequest(Saml2PostAuthenticationRequest authRequest) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", authRequest.getId());
claims.put("relyingPartyRegistrationId", authRequest.getRelyingPartyRegistrationId());
claims.put("authenticationRequestUri", authRequest.getAuthenticationRequestUri());
claims.put("samlRequest", authRequest.getSamlRequest());
claims.put("relayState", authRequest.getRelayState());
return claims;
}
private Saml2PostAuthenticationRequest deserializeSamlRequest(Map<String, Object> claims) {
String relyingPartyRegistrationId = (String) claims.get("relyingPartyRegistrationId");
RelyingPartyRegistration relyingPartyRegistration =
relyingPartyRegistrationRepository.findByRegistrationId(relyingPartyRegistrationId);
if (relyingPartyRegistration == null) {
return null;
}
return Saml2PostAuthenticationRequest.withRelyingPartyRegistration(relyingPartyRegistration)
.id((String) claims.get("id"))
.authenticationRequestUri((String) claims.get("authenticationRequestUri"))
.samlRequest((String) claims.get("samlRequest"))
.relayState((String) claims.get("relayState"))
.build();
}
}

View File

@ -3,6 +3,7 @@ package stirling.software.proprietary.security.saml2;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Collections; import java.util.Collections;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -11,12 +12,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -26,12 +27,13 @@ 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.ApplicationProperties.Security.SAML2;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@Configuration @Configuration
@Slf4j @Slf4j
@ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true") @ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SAML2Configuration { public class Saml2Configuration {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@ -58,6 +60,7 @@ public class SAML2Configuration {
.assertionConsumerServiceBinding(Saml2MessageBinding.POST) .assertionConsumerServiceBinding(Saml2MessageBinding.POST)
.assertionConsumerServiceLocation( .assertionConsumerServiceLocation(
"{baseUrl}/login/saml2/sso/{registrationId}") "{baseUrl}/login/saml2/sso/{registrationId}")
.authnRequestsSigned(true)
.assertingPartyMetadata( .assertingPartyMetadata(
metadata -> metadata ->
metadata.entityId(samlConf.getIdpIssuer()) metadata.entityId(samlConf.getIdpIssuer())
@ -71,6 +74,8 @@ public class SAML2Configuration {
Saml2MessageBinding.POST) Saml2MessageBinding.POST)
.singleLogoutServiceLocation( .singleLogoutServiceLocation(
samlConf.getIdpSingleLogoutUrl()) samlConf.getIdpSingleLogoutUrl())
.singleLogoutServiceResponseLocation(
"http://localhost:8080/login")
.wantAuthnRequestsSigned(true)) .wantAuthnRequestsSigned(true))
.build(); .build();
return new InMemoryRelyingPartyRegistrationRepository(rp); return new InMemoryRelyingPartyRegistrationRepository(rp);
@ -78,8 +83,20 @@ public class SAML2Configuration {
@Bean @Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver( public Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest>
saml2AuthenticationRequestRepository(
JwtServiceInterface jwtService,
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
return new JwtSaml2AuthenticationRequestRepository(
new ConcurrentHashMap<>(), jwtService, relyingPartyRegistrationRepository);
}
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest>
saml2AuthenticationRequestRepository) {
OpenSaml4AuthenticationRequestResolver resolver = OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
@ -87,10 +104,8 @@ public class SAML2Configuration {
customizer -> { customizer -> {
HttpServletRequest request = customizer.getRequest(); HttpServletRequest request = customizer.getRequest();
AuthnRequest authnRequest = customizer.getAuthnRequest(); AuthnRequest authnRequest = customizer.getAuthnRequest();
HttpSessionSaml2AuthenticationRequestRepository requestRepository = Saml2PostAuthenticationRequest saml2AuthenticationRequest =
new HttpSessionSaml2AuthenticationRequestRepository(); saml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
requestRepository.loadAuthenticationRequest(request);
if (saml2AuthenticationRequest != null) { if (saml2AuthenticationRequest != null) {
String sessionId = request.getSession(false).getId(); String sessionId = request.getSession(false).getId();
@ -113,7 +128,6 @@ public class SAML2Configuration {
log.debug("Generating new authentication request ID"); log.debug("Generating new authentication request ID");
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1)); authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
} }
logAuthnRequestDetails(authnRequest); logAuthnRequestDetails(authnRequest);
logHttpRequestDetails(request); logHttpRequestDetails(request);
}); });

Some files were not shown because too many files have changed in this diff Show More