mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 05:54:01 +03:00
Compare commits
18 Commits
v2.9.0
...
feat/add_f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
881d3df24e | ||
|
|
d620bc6818 | ||
|
|
b5424d49f3 | ||
|
|
5c3bd875ac | ||
|
|
119454bf24 | ||
|
|
43b66437ce | ||
|
|
1d1a792043 | ||
|
|
def2c9264a | ||
|
|
7e0eae9abc | ||
|
|
108866b470 | ||
|
|
5f9b94742d | ||
|
|
45887686a7 | ||
|
|
b42087dbfc | ||
|
|
3d491e0fe0 | ||
|
|
503bd8de74 | ||
|
|
74f49b0ff6 | ||
|
|
e42bfc52cc | ||
|
|
1ea5eec7d2 |
3
.github/ISSUE_TEMPLATE/feature.yml
vendored
3
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -1,8 +1,7 @@
|
||||
name: 🚀 Feature
|
||||
description: "Submit a proposal for a new feature"
|
||||
title: "🚀 Feature: "
|
||||
type: "Feature"
|
||||
labels: ["needs more upvotes"]
|
||||
type: 'Feature'
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
|
||||
91
.github/dependabot.yml
vendored
91
.github/dependabot.yml
vendored
@@ -1,83 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
|
||||
multi-ecosystem-groups:
|
||||
all-dependencies:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
time: "17:00"
|
||||
timezone: "Europe/Zurich"
|
||||
labels:
|
||||
- "dependencies"
|
||||
open-pull-requests-limit: 2
|
||||
pull-request-branch-name:
|
||||
separator: "-"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "all-dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/backend"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "all-dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "all-dependencies"
|
||||
versioning-strategy: "increase-if-necessary"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
- "/docker"
|
||||
- "/tests/setup"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "all-dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "docker-compose"
|
||||
directories:
|
||||
- "/"
|
||||
- "/tests/setup"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "all-dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "all-dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,12 +0,0 @@
|
||||
## What does this PR do?
|
||||
|
||||
## Screenshots
|
||||
|
||||
## AI Usage
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
Have you read the [Contributing Guidelines](https://github.com/pocket-id/pocket-id/blob/main/CONTRIBUTING.md)?
|
||||
|
||||
- [ ] If I'm implementing a new feature, I have opened an issue first, or commented on an existing one, to discuss the implementation details.
|
||||
- [ ] I have read the [AI Usage Guidelines](https://github.com/pocket-id/pocket-id/blob/main/CONTRIBUTING.md#ai-usage-policy).
|
||||
9
.github/workflows/backend-linter.yml
vendored
9
.github/workflows/backend-linter.yml
vendored
@@ -17,15 +17,14 @@ permissions:
|
||||
pull-requests: read
|
||||
# Optional: allow write access to checks to allow the action to annotate code in the PR.
|
||||
checks: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
golangci-lint:
|
||||
name: Run Golangci-lint
|
||||
runs-on: depot-ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -33,9 +32,9 @@ jobs:
|
||||
go-version-file: backend/go.mod
|
||||
|
||||
- name: Run Golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9.2.1
|
||||
uses: golangci/golangci-lint-action@v9.0.0
|
||||
with:
|
||||
version: v2.11.4
|
||||
version: v2.9.0
|
||||
args: --build-tags=exclude_frontend
|
||||
working-directory: backend
|
||||
only-new-issues: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
98
.github/workflows/build-next.yml
vendored
98
.github/workflows/build-next.yml
vendored
@@ -9,40 +9,43 @@ concurrency:
|
||||
group: build-next-image
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
|
||||
jobs:
|
||||
build-next:
|
||||
runs-on: depot-ubuntu-latest
|
||||
|
||||
env:
|
||||
CONTAINER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/pocket-id
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "backend/go.mod"
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set DOCKER_IMAGE_NAME
|
||||
run: |
|
||||
# Lowercase REPO_OWNER which is required for containers
|
||||
REPO_OWNER=${{ github.repository_owner }}
|
||||
DOCKER_IMAGE_NAME="ghcr.io/${REPO_OWNER,,}/pocket-id"
|
||||
echo "DOCKER_IMAGE_NAME=${DOCKER_IMAGE_NAME}" >>${GITHUB_ENV}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -51,40 +54,6 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Container Image Metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.CONTAINER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=next
|
||||
labels: |
|
||||
org.opencontainers.image.authors=Pocket ID
|
||||
org.opencontainers.image.url=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.documentation=https://github.com/pocket-id/pocket-id/blob/main/README.md
|
||||
org.opencontainers.image.source=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.version=next
|
||||
org.opencontainers.image.licenses=BSD-2-Clause
|
||||
org.opencontainers.image.ref.name=pocket-id
|
||||
org.opencontainers.image.title=Pocket ID
|
||||
|
||||
- name: Container Image Metadata
|
||||
id: distroless-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.CONTAINER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=next-distroless
|
||||
labels: |
|
||||
org.opencontainers.image.authors=Pocket ID
|
||||
org.opencontainers.image.url=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.documentation=https://github.com/pocket-id/pocket-id/blob/main/README.md
|
||||
org.opencontainers.image.source=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.version=next-distroless
|
||||
org.opencontainers.image.licenses=BSD-2-Clause
|
||||
org.opencontainers.image.ref.name=pocket-id
|
||||
org.opencontainers.image.title=Pocket ID
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -97,40 +66,31 @@ jobs:
|
||||
|
||||
- name: Build and push container image
|
||||
id: build-push-image
|
||||
uses: depot/build-push-action@v1
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile-prebuilt
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
sbom: false
|
||||
provenance: true
|
||||
|
||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:next
|
||||
file: docker/Dockerfile-prebuilt
|
||||
- name: Build and push container image (distroless)
|
||||
uses: depot/build-push-action@v1
|
||||
uses: docker/build-push-action@v6
|
||||
id: container-build-push-distroless
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile-distroless
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.distroless-meta.outputs.tags }}
|
||||
labels: ${{ steps.distroless-meta.outputs.labels }}
|
||||
sbom: false
|
||||
provenance: true
|
||||
|
||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:next-distroless
|
||||
file: docker/Dockerfile-distroless
|
||||
- name: Container image attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.CONTAINER_IMAGE_NAME }}"
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-digest: ${{ steps.build-push-image.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Container image attestation (distroless)
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.CONTAINER_IMAGE_NAME }}"
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
122
.github/workflows/e2e-tests.yml
vendored
122
.github/workflows/e2e-tests.yml
vendored
@@ -1,27 +1,59 @@
|
||||
name: E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- ".github/**"
|
||||
pull_request:
|
||||
branches: [ main, breaking/** ]
|
||||
branches: [main, breaking/**]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- ".github/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and export
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: false
|
||||
load: false
|
||||
tags: pocket-id:test
|
||||
outputs: type=docker,dest=/tmp/docker-image.tar
|
||||
build-args: BUILD_TAGS=e2etest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Upload Docker image artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp/docker-image.tar
|
||||
retention-days: 1
|
||||
|
||||
test:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
runs-on: depot-ubuntu-24.04-32
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -38,33 +70,25 @@ jobs:
|
||||
storage: database
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Set up Depot Docker builder
|
||||
run: depot configure-docker
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Cache PostgreSQL Docker image
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: postgres-cache
|
||||
with:
|
||||
path: /tmp/postgres-image.tar
|
||||
@@ -78,8 +102,23 @@ jobs:
|
||||
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/postgres-image.tar
|
||||
|
||||
- name: Cache LLDAP Docker image
|
||||
uses: actions/cache@v4
|
||||
id: lldap-cache
|
||||
with:
|
||||
path: /tmp/lldap-image.tar
|
||||
key: lldap-stable-${{ runner.os }}
|
||||
- name: Pull and save LLDAP image
|
||||
if: steps.lldap-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull lldap/lldap:2025-05-19
|
||||
docker save lldap/lldap:2025-05-19 > /tmp/lldap-image.tar
|
||||
- name: Load LLDAP image
|
||||
if: steps.lldap-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/lldap-image.tar
|
||||
|
||||
- name: Cache SCIM Test Server Docker image
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: scim-cache
|
||||
with:
|
||||
path: /tmp/scim-test-server-image.tar
|
||||
@@ -95,20 +134,45 @@ jobs:
|
||||
|
||||
- name: Cache Localstack S3 Docker image
|
||||
if: matrix.storage == 's3'
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: s3-cache
|
||||
with:
|
||||
path: /tmp/localstack-s3-image.tar
|
||||
key: localstack-4.14.0-${{ runner.os }}
|
||||
key: localstack-s3-latest-${{ runner.os }}
|
||||
- name: Pull and save Localstack S3 image
|
||||
if: matrix.storage == 's3' && steps.s3-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull localstack/localstack:4.14.0
|
||||
docker save localstack/localstack:4.14.0 > /tmp/localstack-s3-image.tar
|
||||
docker pull localstack/localstack:s3-latest
|
||||
docker save localstack/localstack:s3-latest > /tmp/localstack-s3-image.tar
|
||||
- name: Load Localstack S3 image
|
||||
if: matrix.storage == 's3' && steps.s3-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/localstack-s3-image.tar
|
||||
|
||||
- name: Cache AWS CLI Docker image
|
||||
if: matrix.storage == 's3'
|
||||
uses: actions/cache@v4
|
||||
id: aws-cli-cache
|
||||
with:
|
||||
path: /tmp/aws-cli-image.tar
|
||||
key: aws-cli-latest-${{ runner.os }}
|
||||
- name: Pull and save AWS CLI image
|
||||
if: matrix.storage == 's3' && steps.aws-cli-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull amazon/aws-cli:latest
|
||||
docker save amazon/aws-cli:latest > /tmp/aws-cli-image.tar
|
||||
- name: Load AWS CLI image
|
||||
if: matrix.storage == 's3' && steps.aws-cli-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/aws-cli-image.tar
|
||||
|
||||
- name: Download Docker image artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/docker-image.tar
|
||||
|
||||
- name: Install test dependencies
|
||||
run: pnpm --filter pocket-id-tests install --frozen-lockfile
|
||||
|
||||
@@ -134,7 +198,7 @@ jobs:
|
||||
DOCKER_COMPOSE_FILE=docker-compose-s3.yml
|
||||
fi
|
||||
|
||||
docker compose -f "$DOCKER_COMPOSE_FILE" up -d --build
|
||||
docker compose -f "$DOCKER_COMPOSE_FILE" up -d
|
||||
|
||||
{
|
||||
LOG_FILE="/tmp/backend.log"
|
||||
@@ -155,7 +219,7 @@ jobs:
|
||||
run: pnpm exec playwright test
|
||||
|
||||
- name: Upload Test Report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: playwright-report-${{ matrix.db }}-${{ matrix.storage }}
|
||||
@@ -164,7 +228,7 @@ jobs:
|
||||
retention-days: 15
|
||||
|
||||
- name: Upload Backend Test Report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: backend-${{ matrix.db }}-${{ matrix.storage }}
|
||||
|
||||
2
.github/workflows/pr-quality.yml
vendored
2
.github/workflows/pr-quality.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
# PR Template Checks
|
||||
require-pr-template: true
|
||||
strict-pr-template-sections: "Contributing Guidelines"
|
||||
strict-pr-template-sections: ""
|
||||
optional-pr-template-sections: "Issues"
|
||||
max-additional-pr-template-sections: 3
|
||||
|
||||
|
||||
95
.github/workflows/release.yml
vendored
95
.github/workflows/release.yml
vendored
@@ -5,140 +5,105 @@ on:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
artifact-metadata: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: depot-ubuntu-24.04-16
|
||||
|
||||
env:
|
||||
CONTAINER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/pocket-id
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "backend/go.mod"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set DOCKER_IMAGE_NAME
|
||||
run: |
|
||||
# Lowercase REPO_OWNER which is required for containers
|
||||
REPO_OWNER=${{ github.repository_owner }}
|
||||
DOCKER_IMAGE_NAME="ghcr.io/${REPO_OWNER,,}/pocket-id"
|
||||
echo "DOCKER_IMAGE_NAME=${DOCKER_IMAGE_NAME}" >>${GITHUB_ENV}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.repository_owner}}
|
||||
password: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTAINER_IMAGE_NAME }}
|
||||
${{ env.DOCKER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
labels: |
|
||||
org.opencontainers.image.authors=Pocket ID
|
||||
org.opencontainers.image.url=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.documentation=https://github.com/pocket-id/pocket-id/blob/main/README.md
|
||||
org.opencontainers.image.source=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.version=next
|
||||
org.opencontainers.image.licenses=BSD-2-Clause
|
||||
org.opencontainers.image.ref.name=pocket-id
|
||||
org.opencontainers.image.title=Pocket ID
|
||||
|
||||
- name: Docker metadata (distroless)
|
||||
id: meta-distroless
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTAINER_IMAGE_NAME }}
|
||||
${{ env.DOCKER_IMAGE_NAME }}
|
||||
flavor: |
|
||||
suffix=-distroless,onlatest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
labels: |
|
||||
org.opencontainers.image.authors=Pocket ID
|
||||
org.opencontainers.image.url=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.documentation=https://github.com/pocket-id/pocket-id/blob/main/README.md
|
||||
org.opencontainers.image.source=https://github.com/pocket-id/pocket-id
|
||||
org.opencontainers.image.version=next-distroless
|
||||
org.opencontainers.image.licenses=BSD-2-Clause
|
||||
org.opencontainers.image.ref.name=pocket-id
|
||||
org.opencontainers.image.title=Pocket ID
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm --filter pocket-id-frontend build
|
||||
|
||||
- name: Build binaries
|
||||
run: sh scripts/development/build-binaries.sh
|
||||
|
||||
- name: Build and push container image
|
||||
uses: depot/build-push-action@v1
|
||||
uses: docker/build-push-action@v6
|
||||
id: container-build-push
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile-prebuilt
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
sbom: false
|
||||
provenance: true
|
||||
|
||||
file: docker/Dockerfile-prebuilt
|
||||
- name: Build and push container image (distroless)
|
||||
uses: depot/build-push-action@v1
|
||||
uses: docker/build-push-action@v6
|
||||
id: container-build-push-distroless
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile-distroless
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-distroless.outputs.tags }}
|
||||
labels: ${{ steps.meta-distroless.outputs.labels }}
|
||||
sbom: false
|
||||
provenance: true
|
||||
|
||||
file: docker/Dockerfile-distroless
|
||||
- name: Binary attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: "backend/.bin/pocket-id-**"
|
||||
|
||||
- name: Container image attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.CONTAINER_IMAGE_NAME }}"
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-digest: ${{ steps.container-build-push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Container image attestation (distroless)
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "${{ env.CONTAINER_IMAGE_NAME }}"
|
||||
subject-name: "${{ env.DOCKER_IMAGE_NAME }}"
|
||||
subject-digest: ${{ steps.container-build-push-distroless.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Upload binaries to release
|
||||
@@ -147,12 +112,14 @@ jobs:
|
||||
run: gh release upload ${{ github.ref_name }} backend/.bin/*
|
||||
|
||||
publish-release:
|
||||
runs-on: depot-ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: Mark release as published
|
||||
run: gh release edit ${{ github.ref_name }} --draft=false
|
||||
|
||||
21
.github/workflows/svelte-check.yml
vendored
21
.github/workflows/svelte-check.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- "frontend/tsconfig.json"
|
||||
- "frontend/svelte.config.js"
|
||||
pull_request:
|
||||
branches: [main, breaking/**]
|
||||
branches: [main]
|
||||
paths:
|
||||
- "frontend/src/**"
|
||||
- ".github/svelte-check-matcher.json"
|
||||
@@ -21,31 +21,28 @@ on:
|
||||
- "frontend/svelte.config.js"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
name: Run Svelte Check
|
||||
# Don't run on dependabot branches
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: depot-ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
|
||||
16
.github/workflows/unit-tests.yml
vendored
16
.github/workflows/unit-tests.yml
vendored
@@ -5,20 +5,18 @@ on:
|
||||
paths:
|
||||
- "backend/**"
|
||||
pull_request:
|
||||
branches: [main, breaking/**]
|
||||
branches: [main]
|
||||
paths:
|
||||
- "backend/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: depot-ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "backend/go.mod"
|
||||
@@ -32,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
go test -tags=exclude_frontend -v ./... | tee /tmp/TestResults.log
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: backend-unit-tests
|
||||
|
||||
5
.github/workflows/update-aaguids.yml
vendored
5
.github/workflows/update-aaguids.yml
vendored
@@ -8,15 +8,14 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
update-aaguids:
|
||||
runs-on: depot-ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Fetch JSON data
|
||||
run: |
|
||||
|
||||
198
CHANGELOG.md
198
CHANGELOG.md
@@ -1,201 +1,3 @@
|
||||
## v2.9.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- issues with loading new font ([#1496](https://github.com/pocket-id/pocket-id/pull/1496) by @MelvinSnijders)
|
||||
- PAR parameters not respected by authorize page ([3d9d4de](https://github.com/pocket-id/pocket-id/commit/3d9d4de61940747c64f4a5a4ba24010be2a2fbb6) by @stonith404)
|
||||
- add responsive css for api/user/group/client page add buttons ([#1508](https://github.com/pocket-id/pocket-id/pull/1508) by @James18232)
|
||||
- load Gloock font from static path instead of inline ([89b4abb](https://github.com/pocket-id/pocket-id/commit/89b4abb8b5cc71b62f458f966f2778259b167838) by @stonith404)
|
||||
- update terminology from public key code exchange to proof key co… ([#1515](https://github.com/pocket-id/pocket-id/pull/1515) by @maxwassiljew)
|
||||
- callback URL validation not validated if prompt=none ([8a75774](https://github.com/pocket-id/pocket-id/commit/8a7577497131229badb35cb4b3a4227b1300afff) by @stonith404)
|
||||
- passkey card not rounded ([84678c3](https://github.com/pocket-id/pocket-id/commit/84678c3a7bfa1f601261943f03aa935626fe54a2) by @stonith404)
|
||||
|
||||
### Documentation
|
||||
|
||||
- add AI Usage Policy to CONTRIBUTING.md ([fea933b](https://github.com/pocket-id/pocket-id/commit/fea933b62d6a2899bf1e46fc75dab8b6bca81a0f) by @stonith404)
|
||||
- add PR template ([bf9f76b](https://github.com/pocket-id/pocket-id/commit/bf9f76bbd5b8d49dabda74f1c3de5120ea5db698) by @stonith404)
|
||||
- add "needs more upvotes" label as default ([fc42f62](https://github.com/pocket-id/pocket-id/commit/fc42f6211d410ea8db75018292df8f311078a7fe) by @stonith404)
|
||||
- fix link to watchtower repo ([#1500](https://github.com/pocket-id/pocket-id/pull/1500) by @wollew)
|
||||
|
||||
### Features
|
||||
|
||||
- add support for Pushed Authorization Requests (RFC9126) ([#1404](https://github.com/pocket-id/pocket-id/pull/1404) by @Zenithar)
|
||||
|
||||
### Other
|
||||
|
||||
- fix linter issues ([4f97cd4](https://github.com/pocket-id/pocket-id/commit/4f97cd4188b6028e955c8dc62e122ec409b32db3) by @stonith404)
|
||||
- update AAGUIDs ([#1511](https://github.com/pocket-id/pocket-id/pull/1511) by @github-actions[bot])
|
||||
- run svelte check and unit tests in breaking branches ([83b45f6](https://github.com/pocket-id/pocket-id/commit/83b45f682dc89b2019b1812157157f431dfd3ad7) by @stonith404)
|
||||
- run formatter ([2726ddd](https://github.com/pocket-id/pocket-id/commit/2726ddd0c23ab6ae22f307bf8d377fdfd73e1c14) by @stonith404)
|
||||
- bump vite from 8.0.13 to 8.0.16 ([#1521](https://github.com/pocket-id/pocket-id/pull/1521) by @dependabot[bot])
|
||||
- npm dependabot not run from root ([c0d96a0](https://github.com/pocket-id/pocket-id/commit/c0d96a0b1349d7dd0dec65c78dcdc2e043713ff8) by @stonith404)
|
||||
- fix wrong assertion ([effd2a1](https://github.com/pocket-id/pocket-id/commit/effd2a146f9b85d8bf1df203b4970aafc7ae24f8) by @stonith404)
|
||||
- Bump the "all-dependencies" group with 4 updates across multiple ecosystems ([#1523](https://github.com/pocket-id/pocket-id/pull/1523) by @dependabot[bot])
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.8.0...v2.9.0
|
||||
|
||||
## v2.8.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- delete refresh tokens on end-session to prevent reuse after logout ([#1458](https://github.com/pocket-id/pocket-id/pull/1458) by @wucm667)
|
||||
- return 404 status code for `.well-known` routes if not found ([714b5b3](https://github.com/pocket-id/pocket-id/commit/714b5b3307bb012b94e66e8dcb1de93a2d62eceb) by @stonith404)
|
||||
- add `email_verified` to reserved claims list ([8b22fca](https://github.com/pocket-id/pocket-id/commit/8b22fcaaa475984bb4c42306487035073d7c47e3) by @stonith404)
|
||||
- reject unknown PKCE code challenge methods ([ce6bdb9](https://github.com/pocket-id/pocket-id/commit/ce6bdb9d7e6c0f1eaaa21ddb6d808e41088a38b8) by @stonith404)
|
||||
- make stream of downloaded logos seekable for S3 checksum calculation ([cc9163f](https://github.com/pocket-id/pocket-id/commit/cc9163f577280d7c278dc744e20d0505dbe83ede) by @stonith404)
|
||||
- scope confirmation wasn't shown if account selection was prompted ([272d147](https://github.com/pocket-id/pocket-id/commit/272d1479bd64b4a068bfe4dfa5ba4cd3c5e90505) by @stonith404)
|
||||
- add support for unix socket mode in healthcheck ([a712196](https://github.com/pocket-id/pocket-id/commit/a71219637af1746e9faba5274b6095da676ec555) by @stonith404)
|
||||
- restore cross-platform binary builds ([7027296](https://github.com/pocket-id/pocket-id/commit/7027296632ab82cd9930f2fbe60054c2421688c4) by @stonith404)
|
||||
|
||||
### Documentation
|
||||
|
||||
- update SECURITY.md ([bb5a111](https://github.com/pocket-id/pocket-id/commit/bb5a111e3d51ad56fc074df7083022e3d4e25369) by @stonith404)
|
||||
|
||||
### Features
|
||||
|
||||
- delete OAuth refresh token on RP initiated logout ([#1480](https://github.com/pocket-id/pocket-id/pull/1480) by @stonith404)
|
||||
- remove EXIF/XMP metadata from uploaded images ([#1477](https://github.com/pocket-id/pocket-id/pull/1477) by @stonith404)
|
||||
- add support for `response_mode=fragment` ([0c95b7c](https://github.com/pocket-id/pocket-id/commit/0c95b7c3cc171ed77b51f236009b5ff659da0bec) by @stonith404)
|
||||
- add support for systemd socket activation ([#1479](https://github.com/pocket-id/pocket-id/pull/1479) by @deviant)
|
||||
- improve design trough the whole application ([b3d40a4](https://github.com/pocket-id/pocket-id/commit/b3d40a476b35cfa2451749e9a3d307b7babaeb6b) by @stonith404)
|
||||
|
||||
### Other
|
||||
|
||||
- update AAGUIDs ([#1476](https://github.com/pocket-id/pocket-id/pull/1476) by @github-actions[bot])
|
||||
- upgrade dependencies ([91c2ea2](https://github.com/pocket-id/pocket-id/commit/91c2ea2a6616a500922365d1789f1fa1ba66c112) by @stonith404)
|
||||
- remove deprecated http2 package ([e56dc12](https://github.com/pocket-id/pocket-id/commit/e56dc124cee4d3c176c89e7d574ddfde089bcbd1) by @stonith404)
|
||||
- apply go 1.26.0 syntax updated ([8ad95b8](https://github.com/pocket-id/pocket-id/commit/8ad95b8af17ba8cbb62168ed0bb54c87e3df379c) by @stonith404)
|
||||
- delete refresh tokens on end-session to prevent reuse after logout ([b27a52a](https://github.com/pocket-id/pocket-id/commit/b27a52a5915228383dbdbf0a2c353452cd351d0f) by @stonith404)
|
||||
- use dependabot for automatic dependency upgrades ([b9fdd53](https://github.com/pocket-id/pocket-id/commit/b9fdd530c0f370f2fccb6251d700aa6f9bdf4124) by @stonith404)
|
||||
- fix invalid schema ([e8c398f](https://github.com/pocket-id/pocket-id/commit/e8c398ffbd6545f76c983f01556bb3b73d40a728) by @stonith404)
|
||||
- update AAGUIDs ([#1487](https://github.com/pocket-id/pocket-id/pull/1487) by @github-actions[bot])
|
||||
- use custom Playwright route for callback URL checks ([f134247](https://github.com/pocket-id/pocket-id/commit/f13424720b7f5da3978988f0f9169cf303971a13) by @stonith404)
|
||||
- fix linter issues ([bc4f75c](https://github.com/pocket-id/pocket-id/commit/bc4f75c44f12a71380042ad4c140d41f4dddc7dd) by @stonith404)
|
||||
- don't compare hashes of profile pictures ([9ad2bfc](https://github.com/pocket-id/pocket-id/commit/9ad2bfc7b30e0eb2d7ae1406874b8caba5a1f121) by @stonith404)
|
||||
- run formatter ([0616aba](https://github.com/pocket-id/pocket-id/commit/0616abaf7f079d0a35543258e13aa5b4acb4adc0) by @stonith404)
|
||||
- use fixed minor version of Go ([e046a03](https://github.com/pocket-id/pocket-id/commit/e046a03364f00001f8699aaa7e902b33c2661118) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.7.0...v2.8.0
|
||||
|
||||
## v2.7.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add `_FILE` support for `S3_SECRET_ACCESS_KEY_FILE` env var ([#1452](https://github.com/pocket-id/pocket-id/pull/1452) by @ItalyPaleAle)
|
||||
- invalidate cache when changing image ([#1462](https://github.com/pocket-id/pocket-id/pull/1462) by @GameTec-live)
|
||||
- fall back to Basic auth when PKCE puts client_id in body ([#1466](https://github.com/pocket-id/pocket-id/pull/1466) by @mgabor3141)
|
||||
|
||||
### Documentation
|
||||
|
||||
- add missing /api prefix to app config swagger routes ([#1454](https://github.com/pocket-id/pocket-id/pull/1454) by @aclerici38)
|
||||
|
||||
### Features
|
||||
|
||||
- add support for response_mode=form_post ([#1360](https://github.com/pocket-id/pocket-id/pull/1360) by @Johnwulp)
|
||||
- add support for "select_account" prompt ([#1453](https://github.com/pocket-id/pocket-id/pull/1453) by @ItalyPaleAle)
|
||||
|
||||
### Other
|
||||
|
||||
- add script to update deps ([f9f93f0](https://github.com/pocket-id/pocket-id/commit/f9f93f0ef164426d4d4cfb58fcf59ce30b686130) by @stonith404)
|
||||
- upgrade dependencies ([20df033](https://github.com/pocket-id/pocket-id/commit/20df033c1fc3d737927447206b4f5e65d3ab4a40) by @stonith404)
|
||||
- post dependency upgrade fixes ([e33a9b8](https://github.com/pocket-id/pocket-id/commit/e33a9b8c888988f7b2e6c500e72d53b57db01f34) by @stonith404)
|
||||
- migrate github actions runners to depot runners ([#1329](https://github.com/pocket-id/pocket-id/pull/1329) by @kmendell)
|
||||
- fix caching of ldap-cli e2e tests docker build ([#1457](https://github.com/pocket-id/pocket-id/pull/1457) by @kmendell)
|
||||
- fix incorrect container name variable ([5c7e5f6](https://github.com/pocket-id/pocket-id/commit/5c7e5f680bb70d3056b11f1a33669cc4bd9a1e17) by @kmendell)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.6.2...v2.7.0
|
||||
|
||||
## v2.6.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- return correct byte count in HEAD request writer ([#1443](https://github.com/pocket-id/pocket-id/pull/1443) by @ahampal)
|
||||
- improve keyboard navigation and screen-reader labels ([#1445](https://github.com/pocket-id/pocket-id/pull/1445) by @bjoernch)
|
||||
|
||||
### Other
|
||||
|
||||
- upgrade to vite 8.0 and pnpm 10.33.0 ([#1446](https://github.com/pocket-id/pocket-id/pull/1446) by @kmendell)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.6.1...v2.6.2
|
||||
|
||||
## v2.6.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- restore login screen background from not showing up ([975d3c7](https://github.com/pocket-id/pocket-id/commit/975d3c79c6a882291c69b31d25bfcd8b7896528c) by @kmendell)
|
||||
|
||||
### Other
|
||||
|
||||
- ignore webauthn type for swagger generation ([ce4b89d](https://github.com/pocket-id/pocket-id/commit/ce4b89da650f025747fd0dd45eab5cebe29f5a93) by @kmendell)
|
||||
- update golangci-lint ([#1440](https://github.com/pocket-id/pocket-id/pull/1440) by @ItalyPaleAle)
|
||||
- Add catalan language ([#1436](https://github.com/pocket-id/pocket-id/pull/1436) by @mcasellas)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.6.0...v2.6.1
|
||||
|
||||
## v2.6.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- disable callback URLs with protocols "javascript" and "data" ([#1397](https://github.com/pocket-id/pocket-id/pull/1397) by @ItalyPaleAle)
|
||||
- strip Root prefix from S3 List() returned paths ([#1413](https://github.com/pocket-id/pocket-id/pull/1413) by @vtmocanu)
|
||||
- use valid Tailwind v4 transition class for auth animation squares ([#1415](https://github.com/pocket-id/pocket-id/pull/1415) by @CoolShades)
|
||||
- resolve posixGroup memberUid as bare usernames ([#1422](https://github.com/pocket-id/pocket-id/pull/1422) by @gucong3000)
|
||||
- prevent flickering if no background image is set on login page ([027e6f0](https://github.com/pocket-id/pocket-id/commit/027e6f078da0eec712ae22a04b37c86110cb262b) by @stonith404)
|
||||
- improve form input layout if description next to it is multi col ([9ec4683](https://github.com/pocket-id/pocket-id/commit/9ec4683d18036ba1945bffd4bce14ec4c2dff7f9) by @stonith404)
|
||||
- access token renewal bypasses important checks ([978ac87](https://github.com/pocket-id/pocket-id/commit/978ac87deffec58beaccd15aead975e91b94c8a5) by @stonith404)
|
||||
|
||||
### Features
|
||||
|
||||
- add ability to revoke passkeys of users as admin ([#1386](https://github.com/pocket-id/pocket-id/pull/1386) by @jose-d)
|
||||
- add auth method claim (`amr`) to tokens ([#1433](https://github.com/pocket-id/pocket-id/pull/1433) by @stonith404)
|
||||
- add TLS support for HTTP/2 server ([#1429](https://github.com/pocket-id/pocket-id/pull/1429) by @IngmarStein)
|
||||
- add OpenID Connect `prompt` Parameter Handling ([#1299](https://github.com/pocket-id/pocket-id/pull/1299) by @rjaakke)
|
||||
- return not found. on `/setup` if already completed ([444f7ff](https://github.com/pocket-id/pocket-id/commit/444f7ff2b0269c12f1dba334a37d7db2007e172f) by @stonith404)
|
||||
|
||||
### Other
|
||||
|
||||
- update AAGUIDs ([#1403](https://github.com/pocket-id/pocket-id/pull/1403) by @github-actions[bot])
|
||||
- upgrade dependencies ([f8f7222](https://github.com/pocket-id/pocket-id/commit/f8f7222468dad90f630ae18f7c3fd78e37ba3f77) by @stonith404)
|
||||
- combobox not closed in e2e test ([fbdb93f](https://github.com/pocket-id/pocket-id/commit/fbdb93f1a768a05e6e3f2c6fd32b5de50a745bc6) by @stonith404)
|
||||
- Security upgrade alpine from latest to 3.23.4 ([#1431](https://github.com/pocket-id/pocket-id/pull/1431) by @stonith404)
|
||||
- security upgrade alpine from latest to 3.23.4 ([#1432](https://github.com/pocket-id/pocket-id/pull/1432) by @stonith404)
|
||||
- add Catalan language files ([4f09de2](https://github.com/pocket-id/pocket-id/commit/4f09de2cfc7d1e92632116821493a670fc7ee80d) by @stonith404)
|
||||
- reduce complexity of `ValidateEnvConfig` and `initRouter` ([a0cb574](https://github.com/pocket-id/pocket-id/commit/a0cb57431372c2bcc59904342597845e92a42a93) by @stonith404)
|
||||
- pass context to `shutdownServer` ([ff26c42](https://github.com/pocket-id/pocket-id/commit/ff26c4273a061b7d2c84e7b74f1e0f9e0acc6eb0) by @stonith404)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.5.0...v2.6.0
|
||||
|
||||
## v2.5.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- better error messages when there's another instance of Pocket ID running ([#1370](https://github.com/pocket-id/pocket-id/pull/1370) by @ItalyPaleAle)
|
||||
- move tooltip inside of form input to prevent shifting ([#1369](https://github.com/pocket-id/pocket-id/pull/1369) by @GameTec-live)
|
||||
- derive LDAP admin access from group membership ([#1374](https://github.com/pocket-id/pocket-id/pull/1374) by @kmendell)
|
||||
- avoid fmt.Sprintf on custom GeoLiteDBUrl without %s placeholder ([#1384](https://github.com/pocket-id/pocket-id/pull/1384) by @choyri)
|
||||
- show a warning when SQLite DB is stored on NFS/SMB/FUSE ([#1381](https://github.com/pocket-id/pocket-id/pull/1381) by @ItalyPaleAle)
|
||||
- empty background restore after reboot ([#1379](https://github.com/pocket-id/pocket-id/pull/1379) by @taoso)
|
||||
- allow one-char username on signup ([#1378](https://github.com/pocket-id/pocket-id/pull/1378) by @taoso)
|
||||
|
||||
### Features
|
||||
|
||||
- allow use of svg, png, and ico images types for favicon ([#1289](https://github.com/pocket-id/pocket-id/pull/1289) by @taoso)
|
||||
- allow clearing background image ([#1290](https://github.com/pocket-id/pocket-id/pull/1290) by @taoso)
|
||||
- add `token_endpoint_auth_methods_supported` to `.well-known` ([#1388](https://github.com/pocket-id/pocket-id/pull/1388) by @owenvoke)
|
||||
- add TRUSTED_PLATFORM environment variable for gin ([#1372](https://github.com/pocket-id/pocket-id/pull/1372) by @choyri)
|
||||
|
||||
### Other
|
||||
|
||||
- add pr quality action ([e3905cf](https://github.com/pocket-id/pocket-id/commit/e3905cf3159fe0370778b0d7d3be64b4246d19be) by @stonith404)
|
||||
- separate querying LDAP and updating DB during sync ([#1371](https://github.com/pocket-id/pocket-id/pull/1371) by @ItalyPaleAle)
|
||||
- bump google.golang.org/grpc from 1.79.1 to 1.79.3 in /backend in the go_modules group across 1 directory ([#1391](https://github.com/pocket-id/pocket-id/pull/1391) by @dependabot[bot])
|
||||
- Improve Latvian translations in lv.json ([#1382](https://github.com/pocket-id/pocket-id/pull/1382) by @Raito00)
|
||||
- ignore linter on app image bootstrap ([5251cd9](https://github.com/pocket-id/pocket-id/commit/5251cd97994177c96cb6f9ab3f88ca31367b5b55) by @kmendell)
|
||||
- upgrade dependencies ([e7e0176](https://github.com/pocket-id/pocket-id/commit/e7e0176316857186b9683e2f0cb0686189f86cfb) by @kmendell)
|
||||
- upgrade dependencies ([3c42a71](https://github.com/pocket-id/pocket-id/commit/3c42a713ce91b4061ffcf86d92cbb19294359ff8) by @kmendell)
|
||||
|
||||
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.4.0...v2.5.0
|
||||
|
||||
## v2.4.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,35 +1,12 @@
|
||||
# Contributing
|
||||
|
||||
We are happy that you want to contribute to Pocket ID and help to make it better!
|
||||
|
||||
## Before you start
|
||||
|
||||
Before starting to work on a new feature, please open an issue first, or comment on an existing one, to discuss the implementation details. This saves you time and avoids the disappointment of having a PR closed later because the change doesn't fit the project's direction.
|
||||
|
||||
### AI Usage Policy
|
||||
|
||||
We have nothing against using AI tools to assist your development. But we've seen a growing number of pull requests that appear to be fully or largely AI-generated and submitted without genuine human review. These are difficult and time-consuming for maintainers to review, and they often waste everyone's time.
|
||||
|
||||
To keep contributions reviewable and high-quality, please follow these guidelines when using AI tools.
|
||||
|
||||
#### Guidelines for Using AI Tools
|
||||
|
||||
1. **Understand every line.** You must be able to explain what your code does and why, in your own words. "The AI wrote it" is not an acceptable answer to a reviewer's question.
|
||||
2. **Test before submitting.** Review and test all code manually, as a human, before opening a PR. Don't trust the AI's claim that it works.
|
||||
3. **Write your own words.** Don't paste AI-generated text into issues, comments, or PR descriptions. Walls of generated text make discussions harder, not easier.
|
||||
4. **Disclose your usage.** Note in the PR description how you used AI (see below).
|
||||
|
||||
PRs that appear to be low effort AI output may be closed without a detailed review.
|
||||
|
||||
#### Example disclosure
|
||||
|
||||
> Used GitHub Copilot for autocomplete, and asked an LLM to help draft the test cases in `parser_test.go`. I reviewed, edited, and tested everything myself and understand how it works.
|
||||
|
||||
> Used an LLM to generate the initial implementation of the new API endpoint, but I manually reviewed and tested it before submitting.
|
||||
I am happy that you want to contribute to Pocket ID and help to make it better! All contributions are welcome, including issues, suggestions, pull requests and more.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Submit a Pull Request
|
||||
You've found a bug, have suggestion or something else, just create an issue on GitHub and we can get in touch.
|
||||
|
||||
## Submit a Pull Request
|
||||
|
||||
Before you submit the pull request for review please ensure that
|
||||
|
||||
@@ -52,15 +29,15 @@ Before you submit the pull request for review please ensure that
|
||||
- Your pull request has a detailed description
|
||||
- You run `pnpm format` to format the code
|
||||
|
||||
### Development Environment
|
||||
## Development Environment
|
||||
|
||||
Pocket ID consists of a frontend and backend. In production the frontend gets statically served by the backend, but in development they run as separate processes to enable hot reloading.
|
||||
|
||||
There are two ways to get the development environment setup:
|
||||
|
||||
#### 1. Install required tools
|
||||
### 1. Install required tools
|
||||
|
||||
##### With Dev Containers
|
||||
#### With Dev Containers
|
||||
|
||||
If you use [Dev Containers](https://code.visualstudio.com/docs/remote/containers) in VS Code, you don't need to install anything manually, just follow the steps below.
|
||||
|
||||
@@ -69,7 +46,7 @@ If you use [Dev Containers](https://code.visualstudio.com/docs/remote/containers
|
||||
3. VS Code will detect .devcontainer and will prompt you to open the folder in devcontainer
|
||||
4. If the auto prompt does not work, hit `F1` and select `Dev Containers: Open Folder in Container.`, then select the pocket-id repo root folder and it'll open in container.
|
||||
|
||||
##### Without Dev Containers
|
||||
#### Without Dev Containers
|
||||
|
||||
If you don't use Dev Containers, you need to install the following tools manually:
|
||||
|
||||
@@ -77,9 +54,9 @@ If you don't use Dev Containers, you need to install the following tools manuall
|
||||
- [Go](https://golang.org/doc/install) >= 1.26
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
#### 2. Setup
|
||||
### 2. Setup
|
||||
|
||||
##### Backend
|
||||
#### Backend
|
||||
|
||||
The backend is built with [Gin](https://gin-gonic.com) and written in Go. To set it up, follow these steps:
|
||||
|
||||
@@ -87,7 +64,7 @@ The backend is built with [Gin](https://gin-gonic.com) and written in Go. To set
|
||||
2. Copy the `.env.development-example` file to `.env` and edit the variables as needed
|
||||
3. Start the backend with `go run -tags exclude_frontend ./cmd`
|
||||
|
||||
##### Frontend
|
||||
### Frontend
|
||||
|
||||
The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in TypeScript. To set it up, follow these steps:
|
||||
|
||||
@@ -100,13 +77,9 @@ You're all set! The application is now listening on `localhost:3000`. The backen
|
||||
|
||||
### Testing
|
||||
|
||||
If you are contributing to a new feature please ensure that you add tests for it.
|
||||
|
||||
#### End-to-end tests
|
||||
|
||||
We are using [Playwright](https://playwright.dev) for end-to-end testing.
|
||||
|
||||
The tests are located in the `tests` folder at the root of the project.
|
||||
If you are contributing to a new feature please ensure that you add tests for it. The tests are located in the `tests` folder at the root of the project.
|
||||
|
||||
The tests can be run like this:
|
||||
|
||||
@@ -120,9 +93,3 @@ The tests can be run like this:
|
||||
5. Run the tests with `pnpm dlx playwright test` or from the root project folder `pnpm test`
|
||||
|
||||
If you make any changes to the application, you have to rebuild the test environment by running `docker compose up -d --build` again.
|
||||
|
||||
#### Unit tests
|
||||
|
||||
In the backend we are using unit tests with the built-in Go testing framework. The tests are located in the same folder as the code they are testing and have the `_test.go` suffix.
|
||||
|
||||
To run the tests, simply run `go test ./...` from the root of the `backend` folder.
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
It's recommended to always use the latest version of Pocket ID. We will provide security updates for the latest version.
|
||||
|
||||
> [!NOTE]
|
||||
> Updates can be automated with e.g [Watchtower](https://github.com/nicholas-fedor/watchtower/). Upgrading between non major versions is safe but you shouldn't upgrade between major versions before checking the release notes.
|
||||
As Pocket ID is in its early stages, older versions don't get security updates. Please consider to update Pocket ID regularly. Updates can be automated with e.g [Watchtower](https://github.com/containrrr/watchtower).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Thank you for taking the time to report a vulnerability. Please DO NOT create an issue on GitHub because the vulnerability could get exploited. Instead please create a Security Advisory [here](https://github.com/pocket-id/pocket-id/security/advisories/new). You can learn more about how to report a vulnerability in the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/security-advisories/creating-a-security-advisory).
|
||||
Thank you for taking the time to report a vulnerability. Please DO NOT create an issue on GitHub because the vulnerability could get exploited. Instead please write an email to [elias@eliasschneider.com](mailto:elias@eliasschneider.com).
|
||||
|
||||
@@ -6,7 +6,4 @@ APP_ENV=development
|
||||
# In the development environment the backend gets proxied by the frontend
|
||||
APP_URL=http://localhost:3000
|
||||
PORT=1411
|
||||
MAXMIND_LICENSE_KEY=your_license_key
|
||||
|
||||
# Set the ENCRYPTION_KEY to a random sequence of characters, for example generated with `openssl rand -base64 32`
|
||||
ENCRYPTION_KEY=
|
||||
MAXMIND_LICENSE_KEY=your_license_key
|
||||
@@ -7,6 +7,6 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
func RegisterFrontend(router *gin.Engine, oidcService *service.OidcService) error {
|
||||
func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc, oidcService *service.OidcService) error {
|
||||
return ErrFrontendNotIncluded
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,7 +18,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
//go:embed all:dist/*
|
||||
@@ -55,7 +55,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterFrontend(router *gin.Engine, oidcService *service.OidcService) error {
|
||||
func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc, oidcService *service.OidcService) error {
|
||||
distFS, err := fs.Sub(frontendFS, "dist")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sub FS: %w", err)
|
||||
@@ -79,21 +79,27 @@ func RegisterFrontend(router *gin.Engine, oidcService *service.OidcService) erro
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "api/") || strings.HasPrefix(path, ".well-known/") {
|
||||
if strings.HasPrefix(path, "api/") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if isSPARequest(path, distFS) {
|
||||
// If path is / or does not exist, serve index.html
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
} else if _, err := fs.Stat(distFS, path); os.IsNotExist(err) {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
if path == "index.html" {
|
||||
nonce := middleware.GetCSPNonce(c)
|
||||
|
||||
// For an authorization request that responds via response_mode=form_post, the
|
||||
// consent page auto-submits a form to the client's callback, so that callback
|
||||
// must be allowed in the CSP form-action directive
|
||||
isAuthPostRequest, redirectURI := isOAuth2AuthorizationPostRequest(c, oidcService)
|
||||
if isAuthPostRequest {
|
||||
clientID := c.Query("client_id")
|
||||
// In that case, we need to validate and allow form submissions to the redirect_uri
|
||||
// Check if this is an OAuth2 authorization request with response_mode=form_post
|
||||
// In that case, we need to validate and allow form submissions to the redirect_uri
|
||||
responseMode := c.Query("response_mode")
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
clientID := c.Query("client_id")
|
||||
if responseMode == "form_post" && redirectURI != "" && clientID != "" {
|
||||
validatedRedirectURI, err := oidcService.ResolveAllowedCallbackURL(c.Request.Context(), clientID, redirectURI)
|
||||
if err == nil {
|
||||
c.Header("Content-Security-Policy", middleware.BuildCSP(nonce, validatedRedirectURI))
|
||||
@@ -115,67 +121,11 @@ func RegisterFrontend(router *gin.Engine, oidcService *service.OidcService) erro
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(300*time.Millisecond), 50)
|
||||
router.NoRoute(rateLimitOnlyForOAuth2AuthorizationPostRequest(rateLimitMiddleware, oidcService, distFS), handler)
|
||||
router.NoRoute(rateLimitMiddleware, handler)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rateLimitOnlyForOAuth2AuthorizationPostRequest(rateLimitMiddleware gin.HandlerFunc, oidcService *service.OidcService, distFS fs.FS) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||
|
||||
if isSPARequest(path, distFS) {
|
||||
isAuthPostRequest, _ := isOAuth2AuthorizationPostRequest(c, oidcService)
|
||||
if isAuthPostRequest {
|
||||
rateLimitMiddleware(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// isOAuth2AuthorizationRequest checks if this is an OAuth2 authorization request with response_mode=form_post
|
||||
// In that case, we need to validate and allow form submissions to the redirect_uri
|
||||
func isOAuth2AuthorizationPostRequest(c *gin.Context, oidcService *service.OidcService) (bool, string) {
|
||||
responseMode := c.Query("response_mode")
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
clientID := c.Query("client_id")
|
||||
requestUri := c.Query("request_uri")
|
||||
|
||||
if c.Request.URL.Path != "/authorize" {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if requestUri != "" && clientID != "" {
|
||||
par, err := oidcService.GetPushedAuthorizationRequest(c.Request.Context(), clientID, requestUri)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
responseMode = par.Parameters.ResponseMode
|
||||
redirectURI = par.Parameters.RedirectURI
|
||||
|
||||
}
|
||||
|
||||
ok := responseMode == "form_post" && redirectURI != "" && clientID != ""
|
||||
return ok, redirectURI
|
||||
}
|
||||
|
||||
func isSPARequest(path string, distFS fs.FS) bool {
|
||||
if path == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, err := fs.Stat(distFS, path); err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FileServerWithCaching wraps http.FileServer to add caching headers
|
||||
type FileServerWithCaching struct {
|
||||
root http.FileSystem
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
//go:build !exclude_frontend
|
||||
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSPARequest(t *testing.T) {
|
||||
distFS := fstest.MapFS{
|
||||
"assets/app.js": &fstest.MapFile{Data: []byte("console.log('test')")},
|
||||
}
|
||||
|
||||
t.Run("root path is spa request", func(t *testing.T) {
|
||||
assert.True(t, isSPARequest("", distFS))
|
||||
})
|
||||
|
||||
t.Run("existing bundled asset is not spa request", func(t *testing.T) {
|
||||
assert.False(t, isSPARequest("assets/app.js", distFS))
|
||||
})
|
||||
|
||||
t.Run("unknown path is spa request", func(t *testing.T) {
|
||||
assert.True(t, isSPARequest("authorize", distFS))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRateLimitOnlyForOAuth2AuthorizationPostRequest(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
distFS := fstest.MapFS{
|
||||
"assets/app.js": &fstest.MapFile{Data: []byte("console.log('test')")},
|
||||
}
|
||||
|
||||
t.Run("rate limits spa form_post request", func(t *testing.T) {
|
||||
rateLimited := false
|
||||
nextCalled := false
|
||||
middleware := rateLimitOnlyForOAuth2AuthorizationPostRequest(func(c *gin.Context) {
|
||||
rateLimited = true
|
||||
c.Abort()
|
||||
}, nil, distFS)
|
||||
|
||||
router := gin.New()
|
||||
router.NoRoute(
|
||||
middleware,
|
||||
func(c *gin.Context) {
|
||||
nextCalled = true
|
||||
},
|
||||
)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/authorize?response_mode=form_post&client_id=test&redirect_uri=https://example.com/callback", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.True(t, rateLimited)
|
||||
assert.False(t, nextCalled)
|
||||
})
|
||||
|
||||
t.Run("does not rate limit page request with no form_post params", func(t *testing.T) {
|
||||
rateLimited := false
|
||||
nextCalled := false
|
||||
middleware := rateLimitOnlyForOAuth2AuthorizationPostRequest(func(c *gin.Context) {
|
||||
rateLimited = true
|
||||
c.Abort()
|
||||
}, nil, distFS)
|
||||
|
||||
router := gin.New()
|
||||
router.NoRoute(
|
||||
middleware,
|
||||
func(c *gin.Context) {
|
||||
nextCalled = true
|
||||
},
|
||||
)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/authorize", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.False(t, rateLimited)
|
||||
assert.True(t, nextCalled)
|
||||
})
|
||||
|
||||
t.Run("does not rate limit static asset request with form_post params", func(t *testing.T) {
|
||||
rateLimited := false
|
||||
nextCalled := false
|
||||
middleware := rateLimitOnlyForOAuth2AuthorizationPostRequest(func(c *gin.Context) {
|
||||
rateLimited = true
|
||||
c.Abort()
|
||||
}, nil, distFS)
|
||||
|
||||
router := gin.New()
|
||||
router.NoRoute(
|
||||
middleware,
|
||||
func(c *gin.Context) {
|
||||
nextCalled = true
|
||||
},
|
||||
)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/assets/app.js?response_mode=form_post&client_id=test&redirect_uri=https://example.com/callback", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.False(t, rateLimited)
|
||||
assert.True(t, nextCalled)
|
||||
})
|
||||
|
||||
t.Run("does not rate limit non-authorize spa path with form_post params", func(t *testing.T) {
|
||||
rateLimited := false
|
||||
nextCalled := false
|
||||
// oidcService is nil: a non-/authorize path is rejected by the path guard
|
||||
// before the request_uri branch that would dereference it.
|
||||
middleware := rateLimitOnlyForOAuth2AuthorizationPostRequest(func(c *gin.Context) {
|
||||
rateLimited = true
|
||||
c.Abort()
|
||||
}, nil, distFS)
|
||||
|
||||
router := gin.New()
|
||||
router.NoRoute(
|
||||
middleware,
|
||||
func(c *gin.Context) {
|
||||
nextCalled = true
|
||||
},
|
||||
)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/settings?response_mode=form_post&client_id=test&redirect_uri=https://example.com/callback", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.False(t, rateLimited)
|
||||
assert.True(t, nextCalled)
|
||||
})
|
||||
}
|
||||
198
backend/go.mod
198
backend/go.mod
@@ -1,125 +1,112 @@
|
||||
module github.com/pocket-id/pocket-id/backend
|
||||
|
||||
go 1.26
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.24
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.23
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3
|
||||
github.com/aws/smithy-go v1.27.1
|
||||
github.com/caarlos0/env/v11 v11.4.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
||||
github.com/aws/smithy-go v1.24.1
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||
github.com/emersion/go-smtp v0.24.0
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/gin-contrib/slog v1.2.1
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/gin-contrib/slog v1.2.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-co-op/gocron/v2 v2.21.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/go-playground/validator/v10 v10.30.3
|
||||
github.com/go-webauthn/webauthn v0.17.4
|
||||
github.com/go-co-op/gocron/v2 v2.19.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.6
|
||||
github.com/lestrrat-go/jwx/v3 v3.1.1
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13
|
||||
github.com/lmittmann/tint v1.1.3
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.4.0
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/zitadel/exifremove v0.1.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
go.opentelemetry.io/otel/log v0.20.0
|
||||
go.opentelemetry.io/otel/metric v1.44.0
|
||||
go.opentelemetry.io/otel/sdk v1.44.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/image v0.42.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.org/x/time v0.15.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/log v0.16.0
|
||||
go.opentelemetry.io/otel/metric v1.40.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/time v0.14.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.14.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/disintegration/gift v1.2.1 // indirect
|
||||
github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect
|
||||
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect
|
||||
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
|
||||
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.6 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/go-webauthn/x v0.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 // indirect
|
||||
github.com/google/go-github/v39 v39.2.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/h2non/filetype v1.1.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.1 // indirect
|
||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@@ -128,65 +115,60 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.2.1 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.42 // indirect
|
||||
github.com/lib/pq v1.11.2 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nlnwa/whatwg-url v0.6.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/nlnwa/whatwg-url v0.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valyala/fastjson v1.6.10 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.66.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/arch v0.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.71.0 // indirect
|
||||
modernc.org/libc v1.68.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.48.2 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
)
|
||||
|
||||
476
backend/go.sum
476
backend/go.sum
@@ -6,74 +6,74 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
|
||||
github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8=
|
||||
github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
|
||||
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw=
|
||||
github.com/caarlos0/env/v11 v11.4.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA=
|
||||
github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
@@ -91,32 +91,6 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk=
|
||||
github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8=
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
|
||||
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 h1:P1dsxzctGkmG6Zf7gH2xrZhNXWP5/FuLDI7xbCGsWTo=
|
||||
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0=
|
||||
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A=
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
|
||||
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA=
|
||||
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw=
|
||||
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
|
||||
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4=
|
||||
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
|
||||
github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1 h1:RW22Y3QjGrb97NUA8yupdFcaqg//+hMI2fZrETBvQ4s=
|
||||
github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1/go.mod h1:mnVcdqOeYg0HvT6veRo7wINa1mJ+lC/R4ig2lWcapSI=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -127,33 +101,26 @@ github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/slog v1.2.1 h1:tQbsmllW/PNgtvHRdVlI38jLfpN4IFLS7Pb4HgTeiYw=
|
||||
github.com/gin-contrib/slog v1.2.1/go.mod h1:f/Ke0A3h4DUh0cQnjR2b/l+i0EmVJ+6VY6GIw3RKtxA=
|
||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/gin-contrib/slog v1.2.0 h1:vAxZfr7knD1ZYK5+pMJLP52sZXIkJXkcRPa/0dx9hSk=
|
||||
github.com/gin-contrib/slog v1.2.0/go.mod h1:vYK6YltmpsEFkO0zfRMLTKHrWS3DwUSn0TMpT+kMagI=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
|
||||
github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
||||
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -165,19 +132,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
||||
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
||||
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
|
||||
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
|
||||
github.com/go-webauthn/x v0.2.1 h1:/oB8i0FhSANuoN+YJF5XHMtppa7zGEYaQrrf6ytotjc=
|
||||
github.com/go-webauthn/x v0.2.1/go.mod h1:Wm0X0zXkzznit4gHj4m82GiBZRMEm+TDUIoJWIQLsE4=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -186,10 +150,6 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 h1:kG/6mhO8OwbQrA/0XEPwKJs3D3jG0m1rNH/ZRKDA/pQ=
|
||||
github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -206,17 +166,13 @@ github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfh
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
@@ -227,8 +183,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -243,7 +199,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -270,26 +225,26 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc=
|
||||
github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc=
|
||||
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.6 h1:4FpLQ18KK/ypPbVU3NLWJNRvH3kcYiqKqWfKGqNWxxI=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.6/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||
github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw=
|
||||
github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -307,20 +262,18 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nlnwa/whatwg-url v0.6.2 h1:jU61lU2ig4LANydbEJmA2nPrtCGiKdtgT0rmMd2VZ/Q=
|
||||
github.com/nlnwa/whatwg-url v0.6.2/go.mod h1:x0FPXJzzOEieQtsBT/AKvbiBbQ46YlL6Xa7m02M1ECk=
|
||||
github.com/nlnwa/whatwg-url v0.5.0 h1:l71cqfqG44+VCQZQX3wD4bwheFWicPxuwaCimLEfpDo=
|
||||
github.com/nlnwa/whatwg-url v0.5.0/go.mod h1:X/ejnFFVbaOWdSul+cnlsSHviCzGZJdvPkgc9zD8IY8=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
|
||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.4.0 h1:3ftnrR1/XwiQ788bWIRhsE1DK3GOgJ6tm6S2qTktLm8=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.4.0/go.mod h1:7jcFtmhWVDEV+UopVv9NjcPm200uMyEHN14LIVV4hW8=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -334,12 +287,12 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@@ -366,8 +319,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
@@ -377,190 +328,155 @@ github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz3
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8aKg=
|
||||
github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek=
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0 h1:5RgvxieNq9tS3ewrV1vnODvbHPfKUIJcYtF9Cvz+6aQ=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0/go.mod h1:iTBIdNwx/xmUhfgJs6+84S4dIK059811cO1eUBjKcHY=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.69.0 h1:saQoWg5845Q8TojpqeVStS7zGwVZ6bc5W2PJavTPiBM=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.69.0/go.mod h1:AAaS6xs5AyqMdR3Ir0nSWK+QudL2XM8Vbw5INzUxNc8=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0 h1:R3jsCoTIzv0BiYNhW0axyswn/6SMJ8xL1OuGxvni1Kw=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.69.0/go.mod h1:m07gqyr2QhQxKOKb5vqKCCBtLH3uqlNYR7PU/FISXVU=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0 h1:u5gsfBL8t1Km4ROhQKAs0cA0t9CzUE7nfkASj/UjAtI=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0/go.mod h1:W6FFYCZQuntC5hxVesXpu7Ppd9sT0a84njildAijc+k=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0/go.mod h1:earQ25dooT0Hhspq59DZ8YCC50jWfOlFEeWoxy/P444=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.66.0 h1:vkrK8PAznv2NKt2r+kdu252ccGzkEqLc2aSXbQIALYQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.66.0/go.mod h1:V/UB6D3vMF/UBOL5igAsAYnk1nG/bzYYTzvsB16cy7o=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 h1:aZfdmtI6QU/DAPD4b7YZ5zuJgewxO1EW9miOZklqleU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0/go.mod h1:isNl10/Om5CBWu9jj8WOb2+tJLbCVXDgqwzCaJMnJ6w=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
|
||||
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
|
||||
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 h1:yOYhGNPZseueTTvWp5iBD3/CthrmvayUXYEX862dDi4=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0/go.mod h1:CvaNVqIfcybc+7xqZNubbE+26K6P7AKZF/l0lE2kdCk=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0 h1:LSJsvNqhj2sBNFb5NWHbyDK4QJ/skQ2ydjeOZ9OYNZ4=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0/go.mod h1:0Q5ocj6h/+C6KYq8cnl4tDFVd4I1HBdsJ440aeagHos=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.27.0 h1:0WNVcR8u9yFz8j5FvdHpgwNp3FS5U4guYdzHwEiGjoU=
|
||||
golang.org/x/arch v0.27.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
|
||||
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -568,20 +484,20 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
||||
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.71.0 h1:bu0djXJGhqed3DnBzyzu3sY0fv432lesyz99ecEahyA=
|
||||
modernc.org/libc v1.71.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -590,8 +506,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -2,22 +2,21 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
sloggin "github.com/gin-contrib/slog"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"golang.org/x/time/rate"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -33,47 +32,6 @@ import (
|
||||
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services)
|
||||
|
||||
func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
r, err := initEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = registerRoutes(r, db, svc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverConfig, err := initServer(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runFn := func(ctx context.Context) error {
|
||||
return runServer(ctx, serverConfig)
|
||||
}
|
||||
|
||||
return runFn, nil
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
addr string
|
||||
certProvider *tlsCertProvider
|
||||
listener net.Listener
|
||||
server *http.Server
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func initEngine() (*gin.Engine, error) {
|
||||
setGinMode()
|
||||
|
||||
r := gin.New()
|
||||
initLogger(r)
|
||||
configureEngine(r)
|
||||
registerGlobalMiddleware(r)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func setGinMode() {
|
||||
// Set the appropriate Gin mode based on the environment
|
||||
switch common.EnvConfig.AppEnv {
|
||||
case common.AppEnvProduction:
|
||||
@@ -83,9 +41,10 @@ func setGinMode() {
|
||||
case common.AppEnvTest:
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
}
|
||||
|
||||
func configureEngine(r *gin.Engine) {
|
||||
r := gin.New()
|
||||
initLogger(r)
|
||||
|
||||
if !common.EnvConfig.TrustProxy {
|
||||
_ = r.SetTrustedProxies(nil)
|
||||
}
|
||||
@@ -97,35 +56,34 @@ func configureEngine(r *gin.Engine) {
|
||||
if common.EnvConfig.TracingEnabled {
|
||||
r.Use(otelgin.Middleware(common.Name))
|
||||
}
|
||||
}
|
||||
|
||||
func registerGlobalMiddleware(r *gin.Engine) {
|
||||
// Setup global middleware
|
||||
r.Use(middleware.HeadMiddleware())
|
||||
r.Use(middleware.NewCacheControlMiddleware().Add())
|
||||
r.Use(middleware.NewCorsMiddleware().Add())
|
||||
r.Use(middleware.NewCspMiddleware().Add())
|
||||
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||
}
|
||||
|
||||
func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services) error {
|
||||
|
||||
err := frontend.RegisterFrontend(r, svc.oidcService)
|
||||
frontendRateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(100*time.Millisecond), 300)
|
||||
err := frontend.RegisterFrontend(r, frontendRateLimitMiddleware, svc.oidcService)
|
||||
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to register frontend: %w", err)
|
||||
return nil, fmt.Errorf("failed to register frontend: %w", err)
|
||||
}
|
||||
|
||||
// Initialize middleware for specific routes
|
||||
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
|
||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||
|
||||
apiRateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 100)
|
||||
|
||||
// Set up API routes
|
||||
apiGroup := r.Group("/api", apiRateLimitMiddleware)
|
||||
controller.NewApiKeyController(apiGroup, authMiddleware, svc.apiKeyService)
|
||||
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.webauthnService, svc.appConfigService)
|
||||
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.oneTimeAccessService, svc.webauthnService, svc.appConfigService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.oneTimeAccessService, svc.appConfigService)
|
||||
controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService)
|
||||
controller.NewAppImagesController(apiGroup, authMiddleware, svc.appImagesService)
|
||||
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
|
||||
@@ -135,86 +93,31 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services) error {
|
||||
controller.NewScimController(apiGroup, authMiddleware, svc.scimService)
|
||||
controller.NewUserSignupController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userSignUpService, svc.appConfigService)
|
||||
|
||||
registerTestRoutes(apiGroup, db, svc)
|
||||
// Add test controller in non-production environments
|
||||
if !common.EnvConfig.AppEnv.IsProduction() {
|
||||
for _, f := range registerTestControllers {
|
||||
f(apiGroup, db, svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up base routes
|
||||
baseGroup := r.Group("/", apiRateLimitMiddleware)
|
||||
controller.NewWellKnownController(baseGroup, svc.jwtService)
|
||||
|
||||
// These are not rate-limited.
|
||||
// Set up healthcheck routes
|
||||
// These are not rate-limited
|
||||
controller.NewHealthzController(r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerTestRoutes(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
|
||||
if common.EnvConfig.AppEnv.IsProduction() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range registerTestControllers {
|
||||
f(apiGroup, db, svc)
|
||||
}
|
||||
}
|
||||
|
||||
func initServer(r *gin.Engine) (*serverConfig, error) {
|
||||
protocols, tlsConfig, certProvider, err := initServerProtocols()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var socketFn func() (*socket, error)
|
||||
switch {
|
||||
case common.EnvConfig.SystemdSocket:
|
||||
socketFn = systemdSocket
|
||||
case common.EnvConfig.UnixSocket != "":
|
||||
socketFn = unixSocket
|
||||
default:
|
||||
socketFn = tcpSocket
|
||||
}
|
||||
|
||||
socket, err := socketFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := socket.addr
|
||||
listener := socket.listener
|
||||
server := newHTTPServer(r, protocols)
|
||||
|
||||
return &serverConfig{addr, certProvider, listener, server, tlsConfig}, nil
|
||||
}
|
||||
|
||||
func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, error) {
|
||||
protocols := new(http.Protocols)
|
||||
var protocols http.Protocols
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
|
||||
if common.EnvConfig.TLSCertFile == "" || common.EnvConfig.TLSKeyFile == "" {
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
return protocols, nil, nil, nil
|
||||
}
|
||||
|
||||
protocols.SetHTTP2(true)
|
||||
certProvider, err := newCertProvider(common.EnvConfig.TLSCertFile, common.EnvConfig.TLSKeyFile)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to load TLS certificate: %w", err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
GetCertificate: certProvider.GetCertificate,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
|
||||
slog.Info("TLS enabled")
|
||||
return protocols, tlsConfig, certProvider, nil
|
||||
}
|
||||
|
||||
func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
|
||||
return &http.Server{
|
||||
// Set up the server
|
||||
srv := &http.Server{
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
Protocols: protocols,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
Protocols: &protocols,
|
||||
Handler: h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// HEAD requests don't get matched by Gin routes, so we convert them to GET
|
||||
// middleware.HeadMiddleware will convert them back to HEAD later
|
||||
if req.Method == http.MethodHead {
|
||||
@@ -224,94 +127,75 @@ func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
|
||||
}
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
}),
|
||||
}), &http2.Server{}),
|
||||
}
|
||||
}
|
||||
|
||||
func runServer(ctx context.Context, config *serverConfig) error {
|
||||
slog.Info("Server listening", slog.String("addr", config.addr), slog.Bool("tls", config.tlsConfig != nil))
|
||||
// Set up the listener
|
||||
network := "tcp"
|
||||
addr := net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
|
||||
if common.EnvConfig.UnixSocket != "" {
|
||||
network = "unix"
|
||||
addr = common.EnvConfig.UnixSocket
|
||||
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
|
||||
}
|
||||
|
||||
certWatcher, err := startCertWatcher(ctx, config.certProvider)
|
||||
listener, err := net.Listen(network, addr) //nolint:noctx
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeCertWatcher(certWatcher)
|
||||
|
||||
startHTTPServer(config)
|
||||
notifySystemdReady()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
// We do not pass the context because it's already been canceled
|
||||
//nolint:contextcheck
|
||||
return shutdownServer(config.server)
|
||||
}
|
||||
|
||||
func startCertWatcher(ctx context.Context, certProvider *tlsCertProvider) (*fsnotify.Watcher, error) {
|
||||
if certProvider == nil {
|
||||
return nil, nil
|
||||
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
||||
}
|
||||
|
||||
certWatcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate watcher: %w", err)
|
||||
}
|
||||
|
||||
if err := certWatcher.Add(common.EnvConfig.TLSCertFile); err != nil {
|
||||
certWatcher.Close()
|
||||
return nil, fmt.Errorf("failed to watch TLS certificate: %w", err)
|
||||
}
|
||||
if err := certWatcher.Add(common.EnvConfig.TLSKeyFile); err != nil {
|
||||
certWatcher.Close()
|
||||
return nil, fmt.Errorf("failed to watch TLS key: %w", err)
|
||||
}
|
||||
|
||||
go certProvider.StartWatching(ctx, certWatcher)
|
||||
return certWatcher, nil
|
||||
}
|
||||
|
||||
func closeCertWatcher(certWatcher *fsnotify.Watcher) {
|
||||
if certWatcher != nil {
|
||||
certWatcher.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func startHTTPServer(config *serverConfig) {
|
||||
go func() {
|
||||
defer config.listener.Close()
|
||||
|
||||
listener := config.listener
|
||||
if config.tlsConfig != nil {
|
||||
listener = tls.NewListener(config.listener, config.tlsConfig)
|
||||
// Set the socket mode if using a Unix socket
|
||||
if network == "unix" && common.EnvConfig.UnixSocketMode != "" {
|
||||
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
srvErr := config.server.Serve(listener)
|
||||
|
||||
if srvErr != http.ErrServerClosed {
|
||||
slog.Error("Error starting app server", "error", srvErr)
|
||||
os.Exit(1)
|
||||
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
|
||||
return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func notifySystemdReady() {
|
||||
err := systemd.SdNotifyReady()
|
||||
if err != nil {
|
||||
// Log the error only
|
||||
slog.Warn("Unable to notify systemd that the service is ready", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func shutdownServer(srv *http.Server) error {
|
||||
// Note we use the background context here as ctx has been canceled already
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck
|
||||
shutdownCancel()
|
||||
if shutdownErr != nil {
|
||||
// Log the error only (could be context canceled)
|
||||
slog.Warn("App server shutdown error", "error", shutdownErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Service runner function
|
||||
runFn := func(ctx context.Context) error {
|
||||
slog.Info("Server listening", slog.String("addr", addr))
|
||||
|
||||
// Start the server in a background goroutine
|
||||
go func() {
|
||||
defer listener.Close()
|
||||
|
||||
// Next call blocks until the server is shut down
|
||||
srvErr := srv.Serve(listener)
|
||||
if srvErr != http.ErrServerClosed {
|
||||
slog.Error("Error starting app server", "error", srvErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Notify systemd that we are ready
|
||||
err = systemd.SdNotifyReady()
|
||||
if err != nil {
|
||||
// Log the error only
|
||||
slog.Warn("Unable to notify systemd that the service is ready", "error", err)
|
||||
}
|
||||
|
||||
// Block until the context is canceled
|
||||
<-ctx.Done()
|
||||
|
||||
// Handle graceful shutdown
|
||||
// Note we use the background context here as ctx has been canceled already
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck
|
||||
shutdownCancel()
|
||||
if shutdownErr != nil {
|
||||
// Log the error only (could be context canceled)
|
||||
slog.Warn("App server shutdown error", "error", shutdownErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return runFn, nil
|
||||
}
|
||||
|
||||
func initLogger(r *gin.Engine) {
|
||||
@@ -340,99 +224,3 @@ func initLogger(r *gin.Engine) {
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
// tlsCertProvider holds certificates that can be dynamically reloaded
|
||||
type tlsCertProvider struct {
|
||||
certMutex sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
certFile string
|
||||
keyFile string
|
||||
forceReload atomic.Bool
|
||||
}
|
||||
|
||||
// GetCertificate implements tls.GetCertificate interface for dynamic certificate loading
|
||||
func (p *tlsCertProvider) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if p.forceReload.Load() {
|
||||
p.certMutex.Lock()
|
||||
p.forceReload.Store(false)
|
||||
p.certMutex.Unlock()
|
||||
}
|
||||
|
||||
p.certMutex.RLock()
|
||||
defer p.certMutex.RUnlock()
|
||||
return p.cert, nil
|
||||
}
|
||||
|
||||
// newCertProvider creates a new certificate provider with initial certificates loaded
|
||||
func newCertProvider(certFile, keyFile string) (*tlsCertProvider, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tlsCertProvider{
|
||||
cert: &cert,
|
||||
certFile: certFile,
|
||||
keyFile: keyFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// reloadCertificate reloads the certificate from disk
|
||||
func (p *tlsCertProvider) reloadCertificate() error {
|
||||
cert, err := tls.LoadX509KeyPair(p.certFile, p.keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reload TLS certificate: %w", err)
|
||||
}
|
||||
|
||||
p.certMutex.Lock()
|
||||
p.cert = &cert
|
||||
p.certMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartWatching begins monitoring the certificate files for changes with debouncing
|
||||
func (p *tlsCertProvider) StartWatching(ctx context.Context, watcher *fsnotify.Watcher) {
|
||||
debounceDuration := 1 * time.Second
|
||||
reloadTimer := time.NewTimer(debounceDuration)
|
||||
reloadTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Only process write/rename events for certificate/key files
|
||||
if event.Has(fsnotify.Write | fsnotify.Rename) {
|
||||
// Reset the debounce timer whenever we get a relevant event
|
||||
reloadTimer.Stop()
|
||||
// Drain the channel if there's a pending value
|
||||
select {
|
||||
case <-reloadTimer.C:
|
||||
default:
|
||||
}
|
||||
reloadTimer.Reset(debounceDuration)
|
||||
slog.Debug("TLS file change detected, debouncing", slog.String("path", event.Name))
|
||||
}
|
||||
case <-reloadTimer.C:
|
||||
// Timer fired - no more events in 500ms, so reload
|
||||
slog.Info("Reloading TLS certificate")
|
||||
|
||||
if err := p.reloadCertificate(); err != nil {
|
||||
slog.Error("Failed to reload TLS certificate", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
p.forceReload.Store(true)
|
||||
slog.Info("TLS certificate reloaded successfully")
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
slog.Error("Certificate watcher error", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
)
|
||||
|
||||
type socket struct {
|
||||
addr string
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func unixSocket() (*socket, error) {
|
||||
addr := common.EnvConfig.UnixSocket
|
||||
os.Remove(addr) // remove dangling the socket file to avoid file-exist error
|
||||
|
||||
listener, err := net.Listen("unix", addr) //nolint:noctx
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create UNIX socket: %w", err)
|
||||
}
|
||||
|
||||
if common.EnvConfig.UnixSocketMode != "" {
|
||||
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
|
||||
listener.Close()
|
||||
return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &socket{addr, listener}, nil
|
||||
}
|
||||
|
||||
func tcpSocket() (*socket, error) {
|
||||
addr := net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
|
||||
|
||||
listener, err := net.Listen("tcp", addr) //nolint:noctx
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TCP socket: %w", err)
|
||||
}
|
||||
|
||||
return &socket{addr, listener}, nil
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/go-systemd/activation"
|
||||
)
|
||||
|
||||
func systemdSocket() (*socket, error) {
|
||||
listeners, err := activation.Listeners()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to receive socket from systemd: %w", err)
|
||||
}
|
||||
|
||||
if len(listeners) == 0 {
|
||||
return nil, errors.New("did not receive any sockets from systemd")
|
||||
}
|
||||
|
||||
if len(listeners) > 1 {
|
||||
return nil, errors.New("received too many sockets from systemd")
|
||||
}
|
||||
|
||||
return &socket{"(systemd)", listeners[0]}, nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package bootstrap
|
||||
|
||||
import "errors"
|
||||
|
||||
func systemdSocket() (*socket, error) {
|
||||
return nil, errors.New("systemd socket activation is only supported on Linux")
|
||||
}
|
||||
@@ -2,12 +2,9 @@ package cmds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -16,14 +13,8 @@ import (
|
||||
)
|
||||
|
||||
type healthcheckFlags struct {
|
||||
Endpoint string
|
||||
UnixSocket string
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
type healthcheckResult struct {
|
||||
StatusCode int
|
||||
URL string
|
||||
Endpoint string
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -38,26 +29,47 @@ func init() {
|
||||
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if flags.UnixSocket == "" && !cmd.Flags().Changed("endpoint") {
|
||||
flags.UnixSocket = common.EnvConfig.UnixSocket
|
||||
}
|
||||
|
||||
result, err := healthcheck(ctx, flags)
|
||||
url := flags.Endpoint + "/healthz"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx,
|
||||
"Healthcheck failed",
|
||||
"Failed to create request object",
|
||||
"error", err,
|
||||
"url", url,
|
||||
"ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx,
|
||||
"Failed to perform request",
|
||||
"error", err,
|
||||
"url", url,
|
||||
"ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx,
|
||||
"Healthcheck failed",
|
||||
"status", res.StatusCode,
|
||||
"url", url,
|
||||
"ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if flags.Verbose {
|
||||
slog.InfoContext(ctx,
|
||||
"Healthcheck succeeded",
|
||||
"status", result.StatusCode,
|
||||
"url", result.URL,
|
||||
"unixSocket", flags.UnixSocket,
|
||||
"status", res.StatusCode,
|
||||
"url", url,
|
||||
"ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
}
|
||||
@@ -65,42 +77,7 @@ func init() {
|
||||
}
|
||||
|
||||
healthcheckCmd.Flags().StringVarP(&flags.Endpoint, "endpoint", "e", "http://localhost:"+common.EnvConfig.Port, "Endpoint for Pocket ID")
|
||||
healthcheckCmd.Flags().StringVar(&flags.UnixSocket, "unix-socket", "", "UNIX socket path for Pocket ID")
|
||||
healthcheckCmd.Flags().BoolVarP(&flags.Verbose, "verbose", "v", false, "Enable verbose mode")
|
||||
|
||||
rootCmd.AddCommand(healthcheckCmd)
|
||||
}
|
||||
|
||||
func healthcheck(ctx context.Context, flags healthcheckFlags) (*healthcheckResult, error) {
|
||||
url := strings.TrimRight(flags.Endpoint, "/") + "/healthz"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request object for %q: %w", url, err)
|
||||
}
|
||||
|
||||
client := http.DefaultClient
|
||||
if flags.UnixSocket != "" {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := net.Dialer{}
|
||||
return dialer.DialContext(ctx, "unix", flags.UnixSocket)
|
||||
}
|
||||
client = &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform request to %q: %w", url, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("unexpected status %d from %q", res.StatusCode, url)
|
||||
}
|
||||
|
||||
return &healthcheckResult{
|
||||
StatusCode: res.StatusCode,
|
||||
URL: url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHealthcheckTCPSuccess(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/healthz", r.URL.Path)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := healthcheck(t.Context(), healthcheckFlags{
|
||||
Endpoint: server.URL,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, result.StatusCode)
|
||||
require.Equal(t, server.URL+"/healthz", result.URL)
|
||||
}
|
||||
|
||||
func TestHealthcheckFailsOnUnexpectedStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := healthcheck(t.Context(), healthcheckFlags{
|
||||
Endpoint: server.URL,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unexpected status 500")
|
||||
}
|
||||
|
||||
func TestHealthcheckUnixSocket(t *testing.T) {
|
||||
socketPath := filepath.Join(t.TempDir(), "pocket-id.sock")
|
||||
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", socketPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/healthz", r.URL.Path)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}),
|
||||
ReadHeaderTimeout: time.Second,
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- server.Serve(listener)
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, server.Shutdown(ctx))
|
||||
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
|
||||
})
|
||||
|
||||
result, err := healthcheck(t.Context(), healthcheckFlags{
|
||||
Endpoint: "http://localhost:1411",
|
||||
UnixSocket: socketPath,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, result.StatusCode)
|
||||
require.Equal(t, "http://localhost:1411/healthz", result.URL)
|
||||
}
|
||||
@@ -60,7 +60,7 @@ type EnvConfigSchema struct {
|
||||
S3Region string `env:"S3_REGION"`
|
||||
S3Endpoint string `env:"S3_ENDPOINT"`
|
||||
S3AccessKeyID string `env:"S3_ACCESS_KEY_ID"`
|
||||
S3SecretAccessKey string `env:"S3_SECRET_ACCESS_KEY" options:"file"`
|
||||
S3SecretAccessKey string `env:"S3_SECRET_ACCESS_KEY"`
|
||||
S3ForcePathStyle bool `env:"S3_FORCE_PATH_STYLE"`
|
||||
S3DisableDefaultIntegrityChecks bool `env:"S3_DISABLE_DEFAULT_INTEGRITY_CHECKS"`
|
||||
|
||||
@@ -68,12 +68,8 @@ type EnvConfigSchema struct {
|
||||
Host string `env:"HOST" options:"toLower"`
|
||||
UnixSocket string `env:"UNIX_SOCKET"`
|
||||
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
|
||||
SystemdSocket bool `env:"SYSTEMD_SOCKET"`
|
||||
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
|
||||
|
||||
TLSCertFile string `env:"TLS_CERT" options:"file"`
|
||||
TLSKeyFile string `env:"TLS_KEY" options:"file"`
|
||||
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
|
||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
|
||||
@@ -146,34 +142,6 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
|
||||
return errors.New("ENCRYPTION_KEY must be at least 16 bytes long")
|
||||
}
|
||||
|
||||
prepareDbConfig(config)
|
||||
|
||||
if err := validateAppURLs(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFileBackend(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.SystemdSocket && config.UnixSocket != "" {
|
||||
return errors.New("SYSTEMD_SOCKET and UNIX_SOCKET are mutually exclusive")
|
||||
}
|
||||
if err := validateLocalIPv6Ranges(config.LocalIPv6Ranges); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.AuditLogRetentionDays <= 0 {
|
||||
return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0")
|
||||
}
|
||||
|
||||
if config.StaticApiKey != "" && len(config.StaticApiKey) < 16 {
|
||||
return errors.New("STATIC_API_KEY must be at least 16 characters long")
|
||||
}
|
||||
|
||||
return validateTLSConfig(config)
|
||||
|
||||
}
|
||||
|
||||
func prepareDbConfig(config *EnvConfigSchema) {
|
||||
switch {
|
||||
case config.DbConnectionString == "":
|
||||
config.DbProvider = DbProviderSqlite
|
||||
@@ -183,95 +151,64 @@ func prepareDbConfig(config *EnvConfigSchema) {
|
||||
default:
|
||||
config.DbProvider = DbProviderSqlite
|
||||
}
|
||||
}
|
||||
|
||||
func validateAppURLs(config *EnvConfigSchema) error {
|
||||
if err := validateURLWithoutPath(config.AppURL, "APP_URL"); err != nil {
|
||||
return err
|
||||
parsedAppUrl, err := url.Parse(config.AppURL)
|
||||
if err != nil {
|
||||
return errors.New("APP_URL is not a valid URL")
|
||||
}
|
||||
if parsedAppUrl.Path != "" {
|
||||
return errors.New("APP_URL must not contain a path")
|
||||
}
|
||||
|
||||
// Derive INTERNAL_APP_URL from APP_URL if not set; validate only when provided
|
||||
if config.InternalAppURL == "" {
|
||||
config.InternalAppURL = config.AppURL
|
||||
return nil
|
||||
} else {
|
||||
parsedInternalAppUrl, err := url.Parse(config.InternalAppURL)
|
||||
if err != nil {
|
||||
return errors.New("INTERNAL_APP_URL is not a valid URL")
|
||||
}
|
||||
if parsedInternalAppUrl.Path != "" {
|
||||
return errors.New("INTERNAL_APP_URL must not contain a path")
|
||||
}
|
||||
}
|
||||
|
||||
return validateURLWithoutPath(config.InternalAppURL, "INTERNAL_APP_URL")
|
||||
}
|
||||
|
||||
func validateURLWithoutPath(rawURL, envName string) error {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is not a valid URL", envName)
|
||||
}
|
||||
if parsedURL.Path != "" {
|
||||
return fmt.Errorf("%s must not contain a path", envName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFileBackend(config *EnvConfigSchema) error {
|
||||
switch config.FileBackend {
|
||||
case "s3", "database":
|
||||
return nil
|
||||
// All good, these are valid values
|
||||
case "", "filesystem":
|
||||
if config.UploadPath == "" {
|
||||
config.UploadPath = defaultFsUploadPath
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid FILE_BACKEND value. Must be 'filesystem', 'database', or 's3'")
|
||||
}
|
||||
}
|
||||
|
||||
func validateLocalIPv6Ranges(localIPv6Ranges string) error {
|
||||
ranges := strings.SplitSeq(localIPv6Ranges, ",")
|
||||
// Validate LOCAL_IPV6_RANGES
|
||||
ranges := strings.SplitSeq(config.LocalIPv6Ranges, ",")
|
||||
for rangeStr := range ranges {
|
||||
rangeStr = strings.TrimSpace(rangeStr)
|
||||
if rangeStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateLocalIPv6Range(rangeStr); err != nil {
|
||||
return err
|
||||
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid LOCAL_IPV6_RANGES '%s': %w", rangeStr, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLocalIPv6Range(rangeStr string) error {
|
||||
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid LOCAL_IPV6_RANGES '%s': %w", rangeStr, err)
|
||||
}
|
||||
|
||||
if ipNet.IP.To4() != nil {
|
||||
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTLSConfig(config *EnvConfigSchema) error {
|
||||
switch {
|
||||
case config.TLSCertFile != "" && config.TLSKeyFile == "":
|
||||
return errors.New("TLS_KEY_FILE must be set when TLS_CERT_FILE is set")
|
||||
case config.TLSCertFile == "" && config.TLSKeyFile != "":
|
||||
return errors.New("TLS_CERT_FILE must be set when TLS_KEY_FILE is set")
|
||||
}
|
||||
|
||||
if config.TLSCertFile != "" && config.TLSKeyFile != "" {
|
||||
if _, err := os.Stat(config.TLSCertFile); err != nil {
|
||||
return fmt.Errorf("TLS_CERT_FILE not found: %w", err)
|
||||
if ipNet.IP.To4() != nil {
|
||||
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if config.TLSCertFile != "" && config.TLSKeyFile != "" {
|
||||
if _, err := os.Stat(config.TLSKeyFile); err != nil {
|
||||
return fmt.Errorf("TLS_KEY_FILE not found: %w", err)
|
||||
}
|
||||
if config.AuditLogRetentionDays <= 0 {
|
||||
return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0")
|
||||
}
|
||||
|
||||
if config.StaticApiKey != "" && len(config.StaticApiKey) < 16 {
|
||||
return errors.New("STATIC_API_KEY must be at least 16 characters long")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -351,7 +288,6 @@ func resolveFileBasedEnvVariable(field reflect.Value, fieldType reflect.StructFi
|
||||
return nil
|
||||
}
|
||||
|
||||
// #nosec G703 - Path is passed by the admin
|
||||
fileContent, err := os.ReadFile(envVarFileValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file for env var %s: %w", envVarFileName, err)
|
||||
|
||||
@@ -207,58 +207,6 @@ func TestParseEnvConfig(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "invalid FILE_BACKEND value")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS cert is set without key", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
t.Setenv("TLS_CERT", "/path/to/cert.pem")
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_KEY_FILE must be set when TLS_CERT_FILE is set")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS key is set without cert", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
t.Setenv("TLS_KEY", "/path/to/key.pem")
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_CERT_FILE must be set when TLS_KEY_FILE is set")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS cert file does not exist", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
t.Setenv("TLS_CERT", "/nonexistent/cert.pem")
|
||||
|
||||
keyFile := t.TempDir() + "/key.pem"
|
||||
require.NoError(t, os.WriteFile(keyFile, []byte("key"), 0600))
|
||||
t.Setenv("TLS_KEY", keyFile)
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_CERT_FILE not found")
|
||||
})
|
||||
|
||||
t.Run("should fail when TLS key file does not exist", func(t *testing.T) {
|
||||
EnvConfig = defaultConfig()
|
||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||
t.Setenv("APP_URL", "http://localhost:3000")
|
||||
|
||||
certFile := t.TempDir() + "/cert.pem"
|
||||
require.NoError(t, os.WriteFile(certFile, []byte("cert"), 0600))
|
||||
t.Setenv("TLS_CERT", certFile)
|
||||
t.Setenv("TLS_KEY", "/nonexistent/key.pem")
|
||||
|
||||
err := parseAndValidateEnvConfig(t)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "TLS_KEY_FILE not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
||||
@@ -272,7 +220,7 @@ func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dbConnFile := tempDir + "/db_connection.txt"
|
||||
dbConnContent := "postgres://user:pass@localhost/testdb" // #nosec G101 - test credential
|
||||
dbConnContent := "postgres://user:pass@localhost/testdb"
|
||||
err = os.WriteFile(dbConnFile, []byte(dbConnContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -306,26 +254,4 @@ func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, binaryKeyContent, config.EncryptionKey)
|
||||
})
|
||||
|
||||
t.Run("should load TLS cert and key file contents", func(t *testing.T) {
|
||||
config := defaultConfig()
|
||||
|
||||
certFile := tempDir + "/cert.pem"
|
||||
certContent := "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"
|
||||
err := os.WriteFile(certFile, []byte(certContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyFile := tempDir + "/key.pem"
|
||||
keyContent := "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----"
|
||||
err = os.WriteFile(keyFile, []byte(keyContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv("TLS_CERT_FILE", certFile)
|
||||
t.Setenv("TLS_KEY_FILE", keyFile)
|
||||
|
||||
err = prepareEnvConfig(&config)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, certContent, config.TLSCertFile)
|
||||
assert.Equal(t, keyContent, config.TLSKeyFile)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,409 +8,443 @@ import (
|
||||
|
||||
type AppError interface {
|
||||
error
|
||||
|
||||
HttpStatusCode() int
|
||||
}
|
||||
|
||||
type AppErrorDescription interface {
|
||||
AppError
|
||||
|
||||
Description() string
|
||||
}
|
||||
|
||||
// Custom error types for various conditions
|
||||
|
||||
type AlreadyInUseError struct {
|
||||
Property string
|
||||
}
|
||||
|
||||
func (e AlreadyInUseError) Error() string {
|
||||
func (e *AlreadyInUseError) Error() string {
|
||||
return e.Property + " is already in use"
|
||||
}
|
||||
func (e AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
func (e AlreadyInUseError) Is(target error) bool {
|
||||
func (e *AlreadyInUseError) Is(target error) bool {
|
||||
// Ignore the field property when checking if an error is of the type AlreadyInUseError
|
||||
x := &AlreadyInUseError{}
|
||||
return errors.As(target, &x)
|
||||
}
|
||||
|
||||
type SetupNotAvailableError struct{}
|
||||
type SetupAlreadyCompletedError struct{}
|
||||
|
||||
func (e SetupNotAvailableError) Error() string { return "not found" }
|
||||
func (e SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
func (e *SetupAlreadyCompletedError) Error() string { return "setup already completed" }
|
||||
func (e *SetupAlreadyCompletedError) HttpStatusCode() int { return http.StatusConflict }
|
||||
|
||||
type TokenInvalidOrExpiredError struct{}
|
||||
|
||||
func (e TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" }
|
||||
func (e TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" }
|
||||
func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type DeviceCodeInvalid struct{}
|
||||
|
||||
func (e DeviceCodeInvalid) Error() string {
|
||||
func (e *DeviceCodeInvalid) Error() string {
|
||||
return "one time access code must be used on the device it was generated for"
|
||||
}
|
||||
func (e DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type TokenInvalidError struct{}
|
||||
|
||||
func (e TokenInvalidError) Error() string { return "Token is invalid" }
|
||||
func (e TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *TokenInvalidError) Error() string {
|
||||
return "Token is invalid"
|
||||
}
|
||||
func (e *TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OidcMissingAuthorizationError struct{}
|
||||
|
||||
func (e OidcMissingAuthorizationError) Error() string { return "missing authorization" }
|
||||
func (e OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *OidcMissingAuthorizationError) Error() string { return "missing authorization" }
|
||||
func (e *OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcGrantTypeNotSupportedError struct{}
|
||||
|
||||
func (e OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" }
|
||||
func (e OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" }
|
||||
func (e *OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingClientCredentialsError struct{}
|
||||
|
||||
func (e OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" }
|
||||
func (e OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" }
|
||||
func (e *OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcClientSecretInvalidError struct{}
|
||||
|
||||
func (e OidcClientSecretInvalidError) Error() string { return "invalid client secret" }
|
||||
func (e OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *OidcClientSecretInvalidError) Error() string { return "invalid client secret" }
|
||||
func (e *OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OidcClientAssertionInvalidError struct{}
|
||||
|
||||
func (e OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" }
|
||||
func (e OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" }
|
||||
func (e *OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OidcInvalidAuthorizationCodeError struct{}
|
||||
|
||||
func (e OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" }
|
||||
func (e OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" }
|
||||
func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcClientNotFoundError struct{}
|
||||
|
||||
func (e OidcClientNotFoundError) Error() string { return "client not found" }
|
||||
func (e OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
func (e *OidcClientNotFoundError) Error() string { return "client not found" }
|
||||
func (e *OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type OidcMissingCallbackURLError struct{}
|
||||
|
||||
func (e OidcMissingCallbackURLError) Error() string {
|
||||
func (e *OidcMissingCallbackURLError) Error() string {
|
||||
return "unable to detect callback url, it might be necessary for an admin to fix this"
|
||||
}
|
||||
func (e OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidCallbackURLError struct{}
|
||||
|
||||
func (e OidcInvalidCallbackURLError) Error() string {
|
||||
func (e *OidcInvalidCallbackURLError) Error() string {
|
||||
return "invalid callback URL, it might be necessary for an admin to fix this"
|
||||
}
|
||||
func (e OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type FileTypeNotSupportedError struct{}
|
||||
|
||||
func (e FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
||||
func (e FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
||||
func (e *FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type FileTooLargeError struct {
|
||||
MaxSize string
|
||||
}
|
||||
|
||||
func (e FileTooLargeError) Error() string {
|
||||
func (e *FileTooLargeError) Error() string {
|
||||
return fmt.Sprintf("The file can't be larger than %s", e.MaxSize)
|
||||
}
|
||||
func (e FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge }
|
||||
func (e *FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge }
|
||||
|
||||
type NotSignedInError struct{}
|
||||
|
||||
func (e NotSignedInError) Error() string { return "You are not signed in" }
|
||||
func (e NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *NotSignedInError) Error() string { return "You are not signed in" }
|
||||
func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type MissingAccessToken struct{}
|
||||
|
||||
func (e MissingAccessToken) Error() string { return "Missing access token" }
|
||||
func (e MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *MissingAccessToken) Error() string { return "Missing access token" }
|
||||
func (e *MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type MissingPermissionError struct{}
|
||||
|
||||
func (e MissingPermissionError) Error() string {
|
||||
func (e *MissingPermissionError) Error() string {
|
||||
return "You don't have permission to perform this action"
|
||||
}
|
||||
func (e MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type TooManyRequestsError struct{}
|
||||
|
||||
func (e TooManyRequestsError) Error() string { return "Too many requests" }
|
||||
func (e TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||
func (e *TooManyRequestsError) Error() string {
|
||||
return "Too many requests"
|
||||
}
|
||||
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||
|
||||
type UserIdNotProvidedError struct{}
|
||||
|
||||
func (e UserIdNotProvidedError) Error() string { return "User id not provided" }
|
||||
func (e UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *UserIdNotProvidedError) Error() string {
|
||||
return "User id not provided"
|
||||
}
|
||||
func (e *UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UserNotFoundError struct{}
|
||||
|
||||
func (e UserNotFoundError) Error() string { return "User not found" }
|
||||
func (e UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
func (e *UserNotFoundError) Error() string {
|
||||
return "User not found"
|
||||
}
|
||||
func (e *UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type ClientIdOrSecretNotProvidedError struct{}
|
||||
|
||||
func (e ClientIdOrSecretNotProvidedError) Error() string { return "Client id or secret not provided" }
|
||||
func (e ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *ClientIdOrSecretNotProvidedError) Error() string {
|
||||
return "Client id or secret not provided"
|
||||
}
|
||||
func (e *ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type WrongFileTypeError struct {
|
||||
ExpectedFileType string
|
||||
}
|
||||
|
||||
func (e WrongFileTypeError) Error() string {
|
||||
func (e *WrongFileTypeError) Error() string {
|
||||
return fmt.Sprintf("File must be of type %s", e.ExpectedFileType)
|
||||
}
|
||||
func (e WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type MissingSessionIdError struct{}
|
||||
|
||||
func (e MissingSessionIdError) Error() string { return "Missing session id" }
|
||||
func (e MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *MissingSessionIdError) Error() string {
|
||||
return "Missing session id"
|
||||
}
|
||||
func (e *MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type ReservedClaimError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e ReservedClaimError) Error() string {
|
||||
func (e *ReservedClaimError) Error() string {
|
||||
return fmt.Sprintf("Claim %s is reserved and can't be used", e.Key)
|
||||
}
|
||||
func (e ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type DuplicateClaimError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e DuplicateClaimError) Error() string {
|
||||
func (e *DuplicateClaimError) Error() string {
|
||||
return fmt.Sprintf("Claim %s is already defined", e.Key)
|
||||
}
|
||||
func (e DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidCodeVerifierError struct{}
|
||||
|
||||
func (e OidcInvalidCodeVerifierError) Error() string { return "Invalid code verifier" }
|
||||
func (e OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcInvalidCodeVerifierError) Error() string {
|
||||
return "Invalid code verifier"
|
||||
}
|
||||
func (e *OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingCodeChallengeError struct{}
|
||||
|
||||
func (e OidcMissingCodeChallengeError) Error() string { return "Missing code challenge" }
|
||||
func (e OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcMissingCodeChallengeError) Error() string {
|
||||
return "Missing code challenge"
|
||||
}
|
||||
func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type LdapUserUpdateError struct{}
|
||||
|
||||
func (e LdapUserUpdateError) Error() string { return "LDAP users can't be updated" }
|
||||
func (e LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *LdapUserUpdateError) Error() string {
|
||||
return "LDAP users can't be updated"
|
||||
}
|
||||
func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type LdapUserGroupUpdateError struct{}
|
||||
|
||||
func (e LdapUserGroupUpdateError) Error() string { return "LDAP user groups can't be updated" }
|
||||
func (e LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *LdapUserGroupUpdateError) Error() string {
|
||||
return "LDAP user groups can't be updated"
|
||||
}
|
||||
func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcAccessDeniedError struct{}
|
||||
|
||||
func (e OidcAccessDeniedError) Error() string { return "You're not allowed to access this service" }
|
||||
func (e OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *OidcAccessDeniedError) Error() string {
|
||||
return "You're not allowed to access this service"
|
||||
}
|
||||
func (e *OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcClientIdNotMatchingError struct{}
|
||||
|
||||
func (e OidcClientIdNotMatchingError) Error() string {
|
||||
func (e *OidcClientIdNotMatchingError) Error() string {
|
||||
return "Client id in request doesn't match client id in token"
|
||||
}
|
||||
func (e OidcClientIdNotMatchingError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcClientIdNotMatchingError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcNoCallbackURLError struct{}
|
||||
|
||||
func (e OidcNoCallbackURLError) Error() string {
|
||||
func (e *OidcNoCallbackURLError) Error() string {
|
||||
return "No callback URL provided"
|
||||
}
|
||||
func (e OidcNoCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OidcNoCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UiConfigDisabledError struct{}
|
||||
|
||||
func (e UiConfigDisabledError) Error() string {
|
||||
func (e *UiConfigDisabledError) Error() string {
|
||||
return "The configuration can't be changed since the UI configuration is disabled"
|
||||
}
|
||||
func (e UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type InvalidUUIDError struct{}
|
||||
|
||||
func (e InvalidUUIDError) Error() string { return "Invalid UUID" }
|
||||
func (e InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *InvalidUUIDError) Error() string {
|
||||
return "Invalid UUID"
|
||||
}
|
||||
func (e *InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OneTimeAccessDisabledError struct{}
|
||||
|
||||
func (e OneTimeAccessDisabledError) Error() string { return "One-time access is disabled" }
|
||||
func (e OneTimeAccessDisabledError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *OneTimeAccessDisabledError) Error() string {
|
||||
return "One-time access is disabled"
|
||||
}
|
||||
func (e *OneTimeAccessDisabledError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type InvalidAPIKeyError struct{}
|
||||
|
||||
func (e InvalidAPIKeyError) Error() string { return "Invalid Api Key" }
|
||||
func (e InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *InvalidAPIKeyError) Error() string {
|
||||
return "Invalid Api Key"
|
||||
}
|
||||
func (e *InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type NoAPIKeyProvidedError struct{}
|
||||
|
||||
func (e NoAPIKeyProvidedError) Error() string { return "No API Key Provided" }
|
||||
func (e NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *NoAPIKeyProvidedError) Error() string {
|
||||
return "No API Key Provided"
|
||||
}
|
||||
func (e *NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type APIKeyNotFoundError struct{}
|
||||
|
||||
func (e APIKeyNotFoundError) Error() string { return "API Key Not Found" }
|
||||
func (e APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e *APIKeyNotFoundError) Error() string {
|
||||
return "API Key Not Found"
|
||||
}
|
||||
func (e *APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type APIKeyNotExpiredError struct{}
|
||||
|
||||
func (e APIKeyNotExpiredError) Error() string { return "API Key is not expired yet" }
|
||||
func (e APIKeyNotExpiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *APIKeyNotExpiredError) Error() string {
|
||||
return "API Key is not expired yet"
|
||||
}
|
||||
func (e *APIKeyNotExpiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type APIKeyExpirationDateError struct{}
|
||||
|
||||
func (e APIKeyExpirationDateError) Error() string {
|
||||
func (e *APIKeyExpirationDateError) Error() string {
|
||||
return "API Key expiration time must be in the future"
|
||||
}
|
||||
func (e APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type APIKeyAuthNotAllowedError struct{}
|
||||
|
||||
func (e APIKeyAuthNotAllowedError) Error() string {
|
||||
func (e *APIKeyAuthNotAllowedError) Error() string {
|
||||
return "API key authentication is not allowed for this endpoint"
|
||||
}
|
||||
func (e APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e *APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcInvalidRefreshTokenError struct{}
|
||||
|
||||
func (e OidcInvalidRefreshTokenError) Error() string { return "refresh token is invalid or expired" }
|
||||
func (e OidcInvalidRefreshTokenError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingRefreshTokenError struct{}
|
||||
|
||||
func (e OidcMissingRefreshTokenError) Error() string { return "refresh token is required" }
|
||||
func (e OidcMissingRefreshTokenError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingAuthorizationCodeError struct{}
|
||||
|
||||
func (e OidcMissingAuthorizationCodeError) Error() string { return "authorization code is required" }
|
||||
func (e OidcMissingAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UserDisabledError struct{}
|
||||
|
||||
func (e UserDisabledError) Error() string { return "User account is disabled" }
|
||||
func (e UserDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type ValidationError struct{ Message string }
|
||||
|
||||
func (e ValidationError) Error() string { return e.Message }
|
||||
|
||||
func (e ValidationError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcDeviceCodeExpiredError struct{}
|
||||
|
||||
func (e OidcDeviceCodeExpiredError) Error() string { return "device code has expired" }
|
||||
func (e OidcDeviceCodeExpiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidDeviceCodeError struct{}
|
||||
|
||||
func (e OidcInvalidDeviceCodeError) Error() string { return "invalid device code" }
|
||||
func (e OidcInvalidDeviceCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcSlowDownError struct{}
|
||||
|
||||
func (e OidcSlowDownError) Error() string { return "polling too frequently" }
|
||||
func (e OidcSlowDownError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||
|
||||
type OidcAuthorizationPendingError struct{}
|
||||
|
||||
func (e OidcAuthorizationPendingError) Error() string { return "authorization is still pending" }
|
||||
func (e OidcAuthorizationPendingError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type ReauthenticationRequiredError struct{}
|
||||
|
||||
func (e ReauthenticationRequiredError) Error() string { return "reauthentication required" }
|
||||
func (e ReauthenticationRequiredError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OpenSignupDisabledError struct{}
|
||||
|
||||
func (e OpenSignupDisabledError) Error() string { return "Open user signup is not enabled" }
|
||||
|
||||
func (e OpenSignupDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type ClientIdAlreadyExistsError struct{}
|
||||
|
||||
func (e ClientIdAlreadyExistsError) Error() string { return "Client ID already in use" }
|
||||
|
||||
func (e ClientIdAlreadyExistsError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UserEmailNotSetError struct{}
|
||||
|
||||
func (e UserEmailNotSetError) Error() string { return "The user does not have an email address set" }
|
||||
|
||||
func (e UserEmailNotSetError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type ImageNotFoundError struct{}
|
||||
|
||||
func (e ImageNotFoundError) Error() string { return "Image not found" }
|
||||
|
||||
func (e ImageNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type OidcPARNotSupportedForPublicClientsError struct{}
|
||||
|
||||
func (e OidcPARNotSupportedForPublicClientsError) Error() string {
|
||||
return "pushed authorization requests are not supported for public clients"
|
||||
func (e *OidcInvalidRefreshTokenError) Error() string {
|
||||
return "refresh token is invalid or expired"
|
||||
}
|
||||
func (e OidcPARNotSupportedForPublicClientsError) HttpStatusCode() int {
|
||||
func (e *OidcInvalidRefreshTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcInvalidRequestURIError struct{}
|
||||
type OidcMissingRefreshTokenError struct{}
|
||||
|
||||
func (e OidcInvalidRequestURIError) Error() string {
|
||||
return "invalid or expired request_uri"
|
||||
func (e *OidcMissingRefreshTokenError) Error() string {
|
||||
return "refresh token is required"
|
||||
}
|
||||
func (e OidcInvalidRequestURIError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcPARRequiredError struct{}
|
||||
|
||||
func (e OidcPARRequiredError) Error() string {
|
||||
return "this client requires pushed authorization requests"
|
||||
func (e *OidcMissingRefreshTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcMissingAuthorizationCodeError struct{}
|
||||
|
||||
func (e *OidcMissingAuthorizationCodeError) Error() string {
|
||||
return "authorization code is required"
|
||||
}
|
||||
func (e *OidcMissingAuthorizationCodeError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type UserDisabledError struct{}
|
||||
|
||||
func (e *UserDisabledError) Error() string {
|
||||
return "User account is disabled"
|
||||
}
|
||||
func (e *UserDisabledError) HttpStatusCode() int {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *ValidationError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcDeviceCodeExpiredError struct{}
|
||||
|
||||
func (e *OidcDeviceCodeExpiredError) Error() string {
|
||||
return "device code has expired"
|
||||
}
|
||||
func (e *OidcDeviceCodeExpiredError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcInvalidDeviceCodeError struct{}
|
||||
|
||||
func (e *OidcInvalidDeviceCodeError) Error() string {
|
||||
return "invalid device code"
|
||||
}
|
||||
func (e *OidcInvalidDeviceCodeError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcSlowDownError struct{}
|
||||
|
||||
func (e *OidcSlowDownError) Error() string {
|
||||
return "polling too frequently"
|
||||
}
|
||||
func (e *OidcSlowDownError) HttpStatusCode() int {
|
||||
return http.StatusTooManyRequests
|
||||
}
|
||||
|
||||
type OidcAuthorizationPendingError struct{}
|
||||
|
||||
func (e *OidcAuthorizationPendingError) Error() string {
|
||||
return "authorization is still pending"
|
||||
}
|
||||
func (e *OidcAuthorizationPendingError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type ReauthenticationRequiredError struct{}
|
||||
|
||||
func (e *ReauthenticationRequiredError) Error() string {
|
||||
return "reauthentication required"
|
||||
}
|
||||
func (e *ReauthenticationRequiredError) HttpStatusCode() int {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
|
||||
type OpenSignupDisabledError struct{}
|
||||
|
||||
func (e *OpenSignupDisabledError) Error() string {
|
||||
return "Open user signup is not enabled"
|
||||
}
|
||||
|
||||
func (e *OpenSignupDisabledError) HttpStatusCode() int {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
type ClientIdAlreadyExistsError struct{}
|
||||
|
||||
func (e *ClientIdAlreadyExistsError) Error() string {
|
||||
return "Client ID already in use"
|
||||
}
|
||||
|
||||
func (e *ClientIdAlreadyExistsError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type UserEmailNotSetError struct{}
|
||||
|
||||
func (e *UserEmailNotSetError) Error() string {
|
||||
return "The user does not have an email address set"
|
||||
}
|
||||
|
||||
func (e *UserEmailNotSetError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type ImageNotFoundError struct{}
|
||||
|
||||
func (e *ImageNotFoundError) Error() string {
|
||||
return "Image not found"
|
||||
}
|
||||
|
||||
func (e *ImageNotFoundError) HttpStatusCode() int {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
func (e OidcPARRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type InvalidEmailVerificationTokenError struct{}
|
||||
|
||||
func (e InvalidEmailVerificationTokenError) Error() string { return "Invalid email verification token" }
|
||||
|
||||
func (e InvalidEmailVerificationTokenError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
// OIDC prompt parameter errors - used for redirect error responses
|
||||
|
||||
type OidcLoginRequiredError struct{}
|
||||
|
||||
func (e OidcLoginRequiredError) Error() string { return "login_required" }
|
||||
func (e OidcLoginRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcConsentRequiredError struct{}
|
||||
|
||||
func (e OidcConsentRequiredError) Error() string { return "consent_required" }
|
||||
func (e OidcConsentRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInteractionRequiredError struct{}
|
||||
|
||||
func (e OidcInteractionRequiredError) Error() string { return "interaction_required" }
|
||||
func (e OidcInteractionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidRequestError struct{ description string }
|
||||
|
||||
func NewOidcInvalidRequestError(description string) *OidcInvalidRequestError {
|
||||
return &OidcInvalidRequestError{description: description}
|
||||
func (e *InvalidEmailVerificationTokenError) Error() string {
|
||||
return "Invalid email verification token"
|
||||
}
|
||||
|
||||
func (e OidcInvalidRequestError) Error() string { return "invalid_request" }
|
||||
func (e OidcInvalidRequestError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcInvalidRequestError) Description() string { return e.description }
|
||||
|
||||
type OidcAccountSelectionRequiredError struct{}
|
||||
|
||||
func (e OidcAccountSelectionRequiredError) Error() string { return "account_selection_required" }
|
||||
func (e OidcAccountSelectionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e *InvalidEmailVerificationTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ type AppConfigController struct {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.PublicAppConfigVariableDto
|
||||
// @Router /api/application-configuration [get]
|
||||
// @Router /application-configuration [get]
|
||||
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||
configuration := acc.appConfigService.ListAppConfig(false)
|
||||
|
||||
@@ -76,7 +76,7 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.AppConfigVariableDto
|
||||
// @Router /api/application-configuration/all [get]
|
||||
// @Router /application-configuration/all [get]
|
||||
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
||||
configuration := acc.appConfigService.ListAppConfig(true)
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
// Add device information to the logs
|
||||
for i, logsDto := range logsDtos {
|
||||
logsDto.Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDto.ActorUsername = logsDto.Data["actorUsername"]
|
||||
logsDtos[i] = logsDto
|
||||
}
|
||||
|
||||
@@ -102,7 +101,6 @@ func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
||||
for i, logsDto := range logsDtos {
|
||||
logsDto.Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDto.Username = logs[i].User.Username
|
||||
logsDto.ActorUsername = logsDto.Data["actorUsername"]
|
||||
logsDtos[i] = logsDto
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,9 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
}
|
||||
|
||||
group.POST("/oidc/authorize", authMiddleware.WithAdminNotRequired().Add(), oc.authorizeHandler)
|
||||
group.POST("/oidc/authorize/callback-url", oc.authorizeCallbackURLHandler)
|
||||
group.POST("/oidc/authorization-required", authMiddleware.WithAdminNotRequired().Add(), oc.authorizationConfirmationRequiredHandler)
|
||||
group.GET("/oidc/par-request-info", authMiddleware.WithAdminNotRequired().Add(), oc.parRequestInfoHandler)
|
||||
|
||||
group.POST("/oidc/token", oc.createTokensHandler)
|
||||
group.POST("/oidc/par", oc.pushedAuthorizationRequestHandler)
|
||||
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
||||
group.POST("/oidc/userinfo", oc.userInfoHandler)
|
||||
group.POST("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
@@ -92,30 +89,13 @@ type OidcController struct {
|
||||
// @Router /api/oidc/authorize [post]
|
||||
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientRequestDto
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
code, callbackURL, err := oc.oidcService.Authorize(
|
||||
c.Request.Context(),
|
||||
input,
|
||||
c.GetString("userID"),
|
||||
c.GetString("authenticationMethod"),
|
||||
c.ClientIP(),
|
||||
c.Request.UserAgent(),
|
||||
)
|
||||
code, callbackURL, err := oc.oidcService.Authorize(c.Request.Context(), input, c.GetString("userID"), c.ClientIP(), c.Request.UserAgent())
|
||||
if err != nil {
|
||||
// Check if this is a prompt-related error that should be returned as a redirect error
|
||||
if isOidcPromptError(err) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"error": err.Error(),
|
||||
"requiresRedirect": true,
|
||||
"callbackURL": callbackURL,
|
||||
})
|
||||
return
|
||||
}
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -129,49 +109,6 @@ func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// authorizeCallbackURLHandler godoc
|
||||
// @Summary Resolve a validated callback URL for an OIDC authorization request
|
||||
// @Description Resolves the redirect URI against the client's configured callback URLs without consuming PAR records.
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.AuthorizeOidcClientCallbackRequestDto true "Authorization callback parameters"
|
||||
// @Success 200 {object} dto.AuthorizeOidcClientCallbackResponseDto "Resolved callback URL"
|
||||
// @Router /api/oidc/authorize/callback-url [post]
|
||||
func (oc *OidcController) authorizeCallbackURLHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientCallbackRequestDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
callbackURL, err := oc.oidcService.ResolveAuthorizeCallbackURL(
|
||||
c.Request.Context(),
|
||||
input.ClientID,
|
||||
input.CallbackURL,
|
||||
input.RequestURI,
|
||||
)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.AuthorizeOidcClientCallbackResponseDto{CallbackURL: callbackURL})
|
||||
}
|
||||
|
||||
// isOidcPromptError checks if an error is a prompt-related OIDC error that should trigger a redirect
|
||||
func isOidcPromptError(err error) bool {
|
||||
var loginReq *common.OidcLoginRequiredError
|
||||
var consentReq *common.OidcConsentRequiredError
|
||||
var interactionReq *common.OidcInteractionRequiredError
|
||||
var accountSelectionReq *common.OidcAccountSelectionRequiredError
|
||||
|
||||
return errors.As(err, &loginReq) ||
|
||||
errors.As(err, &consentReq) ||
|
||||
errors.As(err, &interactionReq) ||
|
||||
errors.As(err, &accountSelectionReq)
|
||||
}
|
||||
|
||||
// authorizationConfirmationRequiredHandler godoc
|
||||
// @Summary Check if authorization confirmation is required
|
||||
// @Description Check if the user needs to confirm authorization for the client
|
||||
@@ -188,50 +125,13 @@ func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Contex
|
||||
return
|
||||
}
|
||||
|
||||
authorizationRequired, scope, err := oc.oidcService.AuthorizationRequired(c.Request.Context(), input.ClientID, c.GetString("userID"), input.Scope, input.RequestURI)
|
||||
hasAuthorizedClient, err := oc.oidcService.HasAuthorizedClient(c.Request.Context(), input.ClientID, c.GetString("userID"), input.Scope)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"authorizationRequired": authorizationRequired, "scope": scope})
|
||||
}
|
||||
|
||||
// parRequestInfoHandler godoc
|
||||
// @Summary Resolve stored authorization request parameters
|
||||
// @Description Resolve the parameters of a stored request_uri request so the consent page can render the requested scope and build the final redirect. Does not consume the request.
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @Param client_id query string true "Client ID"
|
||||
// @Param request_uri query string true "Request URI returned from the PAR endpoint"
|
||||
// @Success 200 {object} dto.OidcAuthorizeRequestInfoDto "Resolved authorization request parameters"
|
||||
// @Router /api/oidc/par-request-info [get]
|
||||
func (oc *OidcController) parRequestInfoHandler(c *gin.Context) {
|
||||
clientID := c.Query("client_id")
|
||||
requestURI := c.Query("request_uri")
|
||||
if clientID == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "client_id is required"})
|
||||
return
|
||||
}
|
||||
if requestURI == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "request_uri is required"})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := oc.oidcService.GetPushedAuthorizationRequest(c.Request.Context(), clientID, requestURI)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var infoDto dto.OidcAuthorizeRequestInfoDto
|
||||
err = dto.MapStruct(info.Parameters, &infoDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, infoDto)
|
||||
c.JSON(http.StatusOK, gin.H{"authorizationRequired": !hasAuthorizedClient})
|
||||
}
|
||||
|
||||
// createTokensHandler godoc
|
||||
@@ -273,8 +173,10 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the client ID / secret are passed in the Authorization header (RFC 6749)
|
||||
parseBasicAuth(c.Request, &input.ClientSecret, &input.ClientID)
|
||||
// Client id and secret can also be passed over the Authorization header
|
||||
if input.ClientID == "" && input.ClientSecret == "" {
|
||||
input.ClientID, input.ClientSecret, _ = utils.OAuthClientBasicAuth(c.Request)
|
||||
}
|
||||
|
||||
tokens, err := oc.createTokens(c.Request.Context(), input)
|
||||
|
||||
@@ -303,49 +205,6 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// pushedAuthorizationRequestHandler godoc
|
||||
// @Summary Pushed Authorization Request (PAR)
|
||||
// @Description RFC 9126: Push authorization request parameters and receive a request_uri. Only confidential clients may use this endpoint.
|
||||
// @Tags OIDC
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Success 201 {object} dto.OidcPARResponseDto
|
||||
// @Router /api/oidc/par [post]
|
||||
func (oc *OidcController) pushedAuthorizationRequestHandler(c *gin.Context) {
|
||||
// Per RFC 9126, parameters MUST be passed in the request body
|
||||
c.Request.URL.RawQuery = ""
|
||||
|
||||
var input dto.OidcPARRequestDto
|
||||
if err := c.ShouldBind(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Client id and secret can also be passed over the Authorization header
|
||||
if input.ClientID == "" && input.ClientSecret == "" {
|
||||
input.ClientID, input.ClientSecret, _ = utils.OAuthClientBasicAuth(c.Request)
|
||||
}
|
||||
|
||||
creds := service.ClientAuthCredentials{
|
||||
ClientID: input.ClientID,
|
||||
ClientSecret: input.ClientSecret,
|
||||
ClientAssertion: input.ClientAssertion,
|
||||
ClientAssertionType: input.ClientAssertionType,
|
||||
}
|
||||
|
||||
requestURI, expiresIn, err := oc.oidcService.CreatePushedAuthorizationRequest(c.Request.Context(), creds, input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 9126 §2.2 requires HTTP 201 Created for successful PAR responses
|
||||
c.JSON(http.StatusCreated, dto.OidcPARResponseDto{
|
||||
RequestURI: requestURI,
|
||||
ExpiresIn: expiresIn,
|
||||
})
|
||||
}
|
||||
|
||||
// userInfoHandler godoc
|
||||
// @Summary Get user information
|
||||
// @Description Get user information based on the access token
|
||||
@@ -425,12 +284,6 @@ func (oc *OidcController) EndSessionHandler(c *gin.Context) {
|
||||
// The validation was successful, so we can log out and redirect the user to the callback URL without confirmation
|
||||
cookie.AddAccessTokenCookie(c, 0, "")
|
||||
|
||||
// Callback URL can be empty if none is configured
|
||||
if callbackURL == "" {
|
||||
c.Redirect(http.StatusFound, common.EnvConfig.AppURL+"/logout")
|
||||
return
|
||||
}
|
||||
|
||||
logoutCallbackURL, _ := url.Parse(callbackURL)
|
||||
if input.State != "" {
|
||||
q := logoutCallbackURL.Query()
|
||||
@@ -810,22 +663,6 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, oidcClientDto)
|
||||
}
|
||||
|
||||
// This method can modify the value of clientSecret and clientID
|
||||
func parseBasicAuth(r *http.Request, clientSecret *string, clientID *string) {
|
||||
// Client id and secret can also be passed via the Authorization header (RFC 6749, section 2.3.1 "client_secret_basic")
|
||||
// When PKCE is used, some libraries send client_id in the body for the code_verifier binding while keeping the secret only in the Authorization header
|
||||
// We therefore fall back to Basic auth whenever the secret is missing, not only when both fields are empty
|
||||
if *clientSecret == "" {
|
||||
basicID, basicSecret, ok := utils.OAuthClientBasicAuth(r)
|
||||
if ok {
|
||||
if *clientID == "" {
|
||||
*clientID = basicID
|
||||
}
|
||||
*clientSecret = basicSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
|
||||
// Per RFC 8628 (OAuth 2.0 Device Authorization Grant), parameters for the device authorization request MUST be sent in the body of the POST request
|
||||
// Gin's "ShouldBind" by default reads from the query string too, so we need to reset all query string args before invoking ShouldBind
|
||||
@@ -838,8 +675,10 @@ func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the client ID / secret are passed in the Authorization header (RFC 6749)
|
||||
parseBasicAuth(c.Request, &input.ClientSecret, &input.ClientID)
|
||||
// Client id and secret can also be passed over the Authorization header
|
||||
if input.ClientID == "" && input.ClientSecret == "" {
|
||||
input.ClientID, input.ClientSecret, _ = utils.OAuthClientBasicAuth(c.Request)
|
||||
}
|
||||
|
||||
response, err := oc.oidcService.CreateDeviceAuthorization(c.Request.Context(), input)
|
||||
if err != nil {
|
||||
@@ -962,14 +801,7 @@ func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) {
|
||||
ipAddress := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
err := oc.oidcService.VerifyDeviceCode(
|
||||
c.Request.Context(),
|
||||
userCode,
|
||||
c.GetString("userID"),
|
||||
c.GetString("authenticationMethod"),
|
||||
ipAddress,
|
||||
userAgent)
|
||||
|
||||
err := oc.oidcService.VerifyDeviceCode(c.Request.Context(), userCode, c.GetString("userID"), ipAddress, userAgent)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
@@ -1025,13 +857,7 @@ func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
preview, err := oc.oidcService.GetClientPreview(
|
||||
c.Request.Context(),
|
||||
clientID,
|
||||
userID,
|
||||
strings.Split(scopes, " "),
|
||||
c.GetString("authenticationMethod"))
|
||||
|
||||
preview, err := oc.oidcService.GetClientPreview(c.Request.Context(), clientID, userID, strings.Split(scopes, " "))
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -146,43 +146,6 @@ func TestCreateTokensHandler(t *testing.T) {
|
||||
assert.Equal(t, 120, response.ExpiresIn)
|
||||
})
|
||||
|
||||
t.Run("Uses Basic Auth Secret When Body Has Only Client ID (PKCE)", func(t *testing.T) {
|
||||
// Some OIDC libraries (e.g. jumbojett/openid-connect-php) combine client_secret_basic with PKCE by sending client_id in the body alongside code_verifier while keeping the secret only in the Authorization header
|
||||
// RFC 6749 §2.3.1 permits this; the body client_id must not block the Basic auth fallback for the secret.
|
||||
var capturedInput dto.OidcCreateTokensDto
|
||||
oc := &OidcController{
|
||||
createTokens: func(_ context.Context, input dto.OidcCreateTokensDto) (service.CreatedTokens, error) {
|
||||
capturedInput = input
|
||||
return service.CreatedTokens{
|
||||
AccessToken: "access-token",
|
||||
IdToken: "id-token",
|
||||
RefreshToken: "refresh-token",
|
||||
ExpiresIn: 2 * time.Minute,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("client-id:client-secret"))
|
||||
c, recorder := createTestContext(
|
||||
t,
|
||||
"http://example.com/oidc/token",
|
||||
url.Values{
|
||||
"grant_type": {service.GrantTypeRefreshToken},
|
||||
"refresh_token": {"input-refresh-token"},
|
||||
"client_id": {"client-id"},
|
||||
},
|
||||
basicAuth,
|
||||
false,
|
||||
)
|
||||
|
||||
oc.createTokensHandler(c)
|
||||
|
||||
require.Empty(t, c.Errors)
|
||||
assert.Equal(t, "client-id", capturedInput.ClientID)
|
||||
assert.Equal(t, "client-secret", capturedInput.ClientSecret)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
})
|
||||
|
||||
t.Run("Maps Authorization Pending Error", func(t *testing.T) {
|
||||
oc := &OidcController{
|
||||
createTokens: func(context.Context, dto.OidcCreateTokensDto) (service.CreatedTokens, error) {
|
||||
|
||||
@@ -21,11 +21,10 @@ const defaultOneTimeAccessTokenDuration = 15 * time.Minute
|
||||
// @Summary User management controller
|
||||
// @Description Initializes all user-related API endpoints
|
||||
// @Tags Users
|
||||
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, webAuthnService *service.WebAuthnService, appConfigService *service.AppConfigService) {
|
||||
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, appConfigService *service.AppConfigService) {
|
||||
uc := UserController{
|
||||
userService: userService,
|
||||
oneTimeAccessService: oneTimeAccessService,
|
||||
webAuthnService: webAuthnService,
|
||||
appConfigService: appConfigService,
|
||||
}
|
||||
|
||||
@@ -35,10 +34,8 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
group.POST("/users", authMiddleware.Add(), uc.createUserHandler)
|
||||
group.PUT("/users/:id", authMiddleware.Add(), uc.updateUserHandler)
|
||||
group.GET("/users/:id/groups", authMiddleware.Add(), uc.getUserGroupsHandler)
|
||||
group.GET("/users/:id/webauthn-credentials", authMiddleware.Add(), uc.listUserWebauthnCredentialsHandler)
|
||||
group.PUT("/users/me", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserHandler)
|
||||
group.DELETE("/users/:id", authMiddleware.Add(), uc.deleteUserHandler)
|
||||
group.DELETE("/users/:id/webauthn-credentials/:credentialId", authMiddleware.Add(), uc.deleteUserWebauthnCredentialHandler)
|
||||
|
||||
group.PUT("/users/:id/user-groups", authMiddleware.Add(), uc.updateUserGroups)
|
||||
|
||||
@@ -63,7 +60,6 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
type UserController struct {
|
||||
userService *service.UserService
|
||||
oneTimeAccessService *service.OneTimeAccessService
|
||||
webAuthnService *service.WebAuthnService
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
@@ -91,36 +87,6 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, groupsDto)
|
||||
}
|
||||
|
||||
// listUserWebauthnCredentialsHandler godoc
|
||||
// @Summary List user passkeys
|
||||
// @Description Retrieve all WebAuthn credentials for a specific user
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {array} dto.WebauthnCredentialDto
|
||||
// @Router /api/users/{id}/webauthn-credentials [get]
|
||||
func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
if _, err := uc.userService.GetUser(c.Request.Context(), userID); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
credentials, err := uc.webAuthnService.ListCredentials(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var credentialDtos []dto.WebauthnCredentialDto
|
||||
if err := dto.MapStructList(credentials, &credentialDtos); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, credentialDtos)
|
||||
}
|
||||
|
||||
// listUsersHandler godoc
|
||||
// @Summary List users
|
||||
// @Description Get a paginated list of users with optional search and sorting
|
||||
@@ -215,31 +181,6 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// deleteUserWebauthnCredentialHandler godoc
|
||||
// @Summary Delete user passkey
|
||||
// @Description Delete a specific WebAuthn credential for a user
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Param credentialId path string true "Credential ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/users/{id}/webauthn-credentials/{credentialId} [delete]
|
||||
func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) {
|
||||
err := uc.webAuthnService.DeleteCredential(
|
||||
c.Request.Context(),
|
||||
c.Param("id"),
|
||||
c.Param("credentialId"),
|
||||
c.ClientIP(),
|
||||
c.Request.UserAgent(),
|
||||
c.GetString("userID"),
|
||||
)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// createUserHandler godoc
|
||||
// @Summary Create user
|
||||
// @Description Create a new user
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
@@ -31,7 +30,6 @@ func NewUserSignupController(group *gin.RouterGroup, authMiddleware *middleware.
|
||||
group.GET("/signup-tokens", authMiddleware.Add(), usc.listSignupTokensHandler)
|
||||
group.DELETE("/signup-tokens/:id", authMiddleware.Add(), usc.deleteSignupTokenHandler)
|
||||
group.POST("/signup", rateLimitMiddleware.Add(rate.Every(1*time.Minute), 10), usc.signupHandler)
|
||||
group.GET("/signup/setup", usc.checkInitialAdminSetupAvailable)
|
||||
group.POST("/signup/setup", usc.signUpInitialAdmin)
|
||||
|
||||
}
|
||||
@@ -41,21 +39,6 @@ type UserSignupController struct {
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
func (usc *UserSignupController) checkInitialAdminSetupAvailable(c *gin.Context) {
|
||||
setupCompleted, err := usc.userSignUpService.IsInitialAdminSetupCompleted(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if setupCompleted {
|
||||
_ = c.Error(&common.SetupNotAvailableError{})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// signUpInitialAdmin godoc
|
||||
// @Summary Sign up initial admin user
|
||||
// @Description Sign up and generate setup access token for initial admin user
|
||||
|
||||
@@ -137,7 +137,7 @@ func (wc *WebauthnController) deleteCredentialHandler(c *gin.Context) {
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
err := wc.webAuthnService.DeleteCredential(c.Request.Context(), userID, credentialID, clientIP, userAgent, userID)
|
||||
err := wc.webAuthnService.DeleteCredential(c.Request.Context(), userID, credentialID, clientIP, userAgent)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -91,10 +91,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
||||
"id_token_signing_alg_values_supported": []string{alg.String()},
|
||||
"authorization_response_iss_parameter_supported": true,
|
||||
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||
"prompt_values_supported": []string{"none", "login", "consent", "select_account"},
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "private_key_jwt", "none"},
|
||||
"pushed_authorization_request_endpoint": internalAppUrl + "/api/oidc/par",
|
||||
"require_pushed_authorization_requests": false,
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "none"},
|
||||
}
|
||||
return json.Marshal(config)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ type AuditLogDto struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||
|
||||
Event string `json:"event"`
|
||||
IpAddress string `json:"ipAddress"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Device string `json:"device"`
|
||||
UserID string `json:"userID"`
|
||||
Username string `json:"username"`
|
||||
ActorUsername string `json:"actorUsername"`
|
||||
Data map[string]string `json:"data"`
|
||||
Event string `json:"event"`
|
||||
IpAddress string `json:"ipAddress"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Device string `json:"device"`
|
||||
UserID string `json:"userID"`
|
||||
Username string `json:"username"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
@@ -13,13 +13,12 @@ type OidcClientMetaDataDto struct {
|
||||
|
||||
type OidcClientDto struct {
|
||||
OidcClientMetaDataDto
|
||||
CallbackURLs []string `json:"callbackURLs"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresPushedAuthorizationRequests bool `json:"requiresPushedAuthorizationRequests"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||
CallbackURLs []string `json:"callbackURLs"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||
}
|
||||
|
||||
type OidcClientWithAllowedUserGroupsDto struct {
|
||||
@@ -33,20 +32,19 @@ type OidcClientWithAllowedGroupsCountDto struct {
|
||||
}
|
||||
|
||||
type OidcClientUpdateDto struct {
|
||||
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
|
||||
CallbackURLs []string `json:"callbackURLs" binding:"omitempty,dive,callback_url_pattern"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs" binding:"omitempty,dive,callback_url_pattern"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
RequiresPushedAuthorizationRequests bool `json:"requiresPushedAuthorizationRequests"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||
LogoURL *string `json:"logoUrl"`
|
||||
DarkLogoURL *string `json:"darkLogoUrl"`
|
||||
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||
Name string `json:"name" binding:"required,max=50" unorm:"nfc"`
|
||||
CallbackURLs []string `json:"callbackURLs" binding:"omitempty,dive,callback_url"`
|
||||
LogoutCallbackURLs []string `json:"logoutCallbackURLs" binding:"omitempty,dive,callback_url"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||
LogoURL *string `json:"logoUrl"`
|
||||
DarkLogoURL *string `json:"darkLogoUrl"`
|
||||
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||
}
|
||||
|
||||
type OidcClientCreateDto struct {
|
||||
@@ -67,36 +65,13 @@ type OidcClientFederatedIdentityDto struct {
|
||||
|
||||
type AuthorizeOidcClientRequestDto struct {
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope" binding:"required_without=RequestURI"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
Nonce string `json:"nonce"`
|
||||
CodeChallenge string `json:"codeChallenge"`
|
||||
CodeChallengeMethod string `json:"codeChallengeMethod"`
|
||||
ReauthenticationToken string `json:"reauthenticationToken"`
|
||||
Prompt string `json:"prompt"`
|
||||
ResponseMode string `json:"responseMode" binding:"omitempty,response_mode"`
|
||||
RequestURI string `json:"requestURI"`
|
||||
}
|
||||
|
||||
type OidcPARRequestDto struct {
|
||||
ClientID string `form:"client_id"`
|
||||
ClientSecret string `form:"client_secret"`
|
||||
ClientAssertion string `form:"client_assertion"`
|
||||
ClientAssertionType string `form:"client_assertion_type"`
|
||||
ResponseType string `form:"response_type" binding:"required"`
|
||||
Scope string `form:"scope" binding:"required"`
|
||||
RedirectURI string `form:"redirect_uri"`
|
||||
State string `form:"state"`
|
||||
Nonce string `form:"nonce"`
|
||||
CodeChallenge string `form:"code_challenge"`
|
||||
CodeChallengeMethod string `form:"code_challenge_method"`
|
||||
Prompt string `form:"prompt"`
|
||||
ResponseMode string `form:"response_mode" binding:"omitempty,response_mode"`
|
||||
}
|
||||
|
||||
type OidcPARResponseDto struct {
|
||||
RequestURI string `json:"request_uri"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientResponseDto struct {
|
||||
@@ -105,29 +80,9 @@ type AuthorizeOidcClientResponseDto struct {
|
||||
Issuer string `json:"issuer"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientCallbackRequestDto struct {
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
RequestURI string `json:"requestURI"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientCallbackResponseDto struct {
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
}
|
||||
|
||||
type AuthorizationRequiredDto struct {
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope"`
|
||||
RequestURI string `json:"requestURI"`
|
||||
}
|
||||
|
||||
type OidcAuthorizeRequestInfoDto struct {
|
||||
Scope string `json:"scope"`
|
||||
RedirectURI string `json:"redirectURI"`
|
||||
State string `json:"state,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
ResponseMode string `json:"responseMode,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
}
|
||||
|
||||
type OidcCreateTokensDto struct {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
@@ -21,41 +19,44 @@ var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9_.@-]*[a-
|
||||
var validateClientIDRegex = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
|
||||
|
||||
func init() {
|
||||
engine := binding.Validator.Engine().(*validator.Validate)
|
||||
v := binding.Validator.Engine().(*validator.Validate)
|
||||
|
||||
// Maximum allowed value for TTLs
|
||||
const maxTTL = 31 * 24 * time.Hour
|
||||
|
||||
validators := map[string]validator.Func{
|
||||
"username": func(fl validator.FieldLevel) bool {
|
||||
return ValidateUsername(fl.Field().String())
|
||||
},
|
||||
"client_id": func(fl validator.FieldLevel) bool {
|
||||
return ValidateClientID(fl.Field().String())
|
||||
},
|
||||
"ttl": func(fl validator.FieldLevel) bool {
|
||||
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Allow zero, which means the field wasn't set
|
||||
return ttl.Duration == 0 || (ttl.Duration > time.Second && ttl.Duration <= maxTTL)
|
||||
},
|
||||
"callback_url": func(fl validator.FieldLevel) bool {
|
||||
return ValidateCallbackURL(fl.Field().String())
|
||||
},
|
||||
"callback_url_pattern": func(fl validator.FieldLevel) bool {
|
||||
return ValidateCallbackURLPattern(fl.Field().String())
|
||||
},
|
||||
"response_mode": func(fl validator.FieldLevel) bool {
|
||||
return ValidateResponseMode(fl.Field().String())
|
||||
},
|
||||
if err := v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
|
||||
return ValidateUsername(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for username: " + err.Error())
|
||||
}
|
||||
for k, v := range validators {
|
||||
err := engine.RegisterValidation(k, v)
|
||||
if err != nil {
|
||||
panic("Failed to register custom validation for " + k + ": " + err.Error())
|
||||
|
||||
if err := v.RegisterValidation("client_id", func(fl validator.FieldLevel) bool {
|
||||
return ValidateClientID(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for client_id: " + err.Error())
|
||||
}
|
||||
|
||||
if err := v.RegisterValidation("ttl", func(fl validator.FieldLevel) bool {
|
||||
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Allow zero, which means the field wasn't set
|
||||
return ttl.Duration == 0 || (ttl.Duration > time.Second && ttl.Duration <= maxTTL)
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for ttl: " + err.Error())
|
||||
}
|
||||
|
||||
if err := v.RegisterValidation("callback_url", func(fl validator.FieldLevel) bool {
|
||||
return ValidateCallbackURL(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for callback_url: " + err.Error())
|
||||
}
|
||||
|
||||
if err := v.RegisterValidation("response_mode", func(fl validator.FieldLevel) bool {
|
||||
return ValidateResponseMode(fl.Field().String())
|
||||
}); err != nil {
|
||||
panic("Failed to register custom validation for response_mode: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,34 +70,18 @@ func ValidateClientID(clientID string) bool {
|
||||
return validateClientIDRegex.MatchString(clientID)
|
||||
}
|
||||
|
||||
// ValidateCallbackURL validates the input callback URL
|
||||
func ValidateCallbackURL(str string) bool {
|
||||
// Ensure the URL is a valid one and that the protocol is not "javascript:" or "data:"
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "javascript", "data":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCallbackURLPattern validates callback URL patterns, with support for wildcards
|
||||
func ValidateCallbackURLPattern(raw string) bool {
|
||||
// ValidateCallbackURL validates callback URLs with support for wildcards
|
||||
func ValidateCallbackURL(raw string) bool {
|
||||
err := utils.ValidateCallbackURLPattern(raw)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ValidateResponseMode validates response_mode parameter
|
||||
// If responseMode is present, it must be "form_post", "query", or "fragment"
|
||||
// If responseMode is present, it must be "form_post" or "query"
|
||||
// Empty responseMode is allowed (field not provided, use default)
|
||||
func ValidateResponseMode(responseMode string) bool {
|
||||
switch responseMode {
|
||||
case "form_post", "query", "fragment":
|
||||
case "form_post", "query":
|
||||
return true
|
||||
case "":
|
||||
return true
|
||||
|
||||
@@ -66,9 +66,8 @@ func TestValidateResponseMode(t *testing.T) {
|
||||
}{
|
||||
{"valid form_post", "form_post", true},
|
||||
{"valid query", "query", true},
|
||||
{"valid fragment", "fragment", true},
|
||||
{"valid empty", "", true},
|
||||
{"invalid unknown", "unknown", false},
|
||||
{"invalid fragment", "fragment", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -77,28 +76,3 @@ func TestValidateResponseMode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCallbackURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"valid https URL", "https://example.com/callback", true},
|
||||
{"valid loopback URL", "http://127.0.0.1:49813/callback", true},
|
||||
{"empty scheme", "//127.0.0.1:49813/callback", true},
|
||||
{"valid custom scheme", "pocketid://callback", true},
|
||||
{"invalid malformed URL", "http://[::1", false},
|
||||
{"invalid missing scheme separator", "://example.com/callback", false},
|
||||
{"rejects javascript scheme", "javascript:alert(1)", false},
|
||||
{"rejects mixed case javascript scheme", "JavaScript:alert(1)", false},
|
||||
{"rejects data scheme", "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
||||
{"rejects mixed case data scheme", "DaTa:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, ValidateCallbackURL(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type WebauthnCredentialDto struct {
|
||||
Name string `json:"name"`
|
||||
CredentialID string `json:"credentialID"`
|
||||
AttestationType string `json:"attestationType"`
|
||||
Transport []protocol.AuthenticatorTransport `json:"transport" swaggertype:"array,string"`
|
||||
Transport []protocol.AuthenticatorTransport `json:"transport"`
|
||||
|
||||
BackupEligible bool `json:"backupEligible"`
|
||||
BackupState bool `json:"backupState"`
|
||||
|
||||
@@ -38,7 +38,6 @@ func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) erro
|
||||
s.RegisterJob(ctx, "ClearOidcRefreshTokens", jobDefWithJitter(24*time.Hour), jobs.clearOidcRefreshTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
|
||||
s.RegisterJob(ctx, "ClearReauthenticationTokens", jobDefWithJitter(24*time.Hour), jobs.clearReauthenticationTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
|
||||
s.RegisterJob(ctx, "ClearAuditLogs", jobDefWithJitter(24*time.Hour), jobs.clearAuditLogs, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
|
||||
s.RegisterJob(ctx, "ClearOidcPushedAuthorizationRequests", jobDefWithJitter(24*time.Hour), jobs.clearOidcPushedAuthorizationRequests, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,20 +146,6 @@ func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearOidcPushedAuthorizationRequests deletes PAR records that have expired without being consumed
|
||||
func (j *DbCleanupJobs) clearOidcPushedAuthorizationRequests(ctx context.Context) error {
|
||||
st := j.db.
|
||||
WithContext(ctx).
|
||||
Delete(&model.OidcPushedAuthorizationRequest{}, "expires_at < ?", datatype.DateTime(time.Now()))
|
||||
if st.Error != nil {
|
||||
return fmt.Errorf("failed to clean expired pushed authorization requests: %w", st.Error)
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "Cleaned expired pushed authorization requests", slog.Int64("count", st.RowsAffected))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearEmailVerificationTokens deletes email verification tokens that have expired
|
||||
func (j *DbCleanupJobs) clearEmailVerificationTokens(ctx context.Context) error {
|
||||
st := j.db.
|
||||
|
||||
@@ -74,11 +74,10 @@ func (m *AuthMiddleware) WithApiKeyAuthDisabled() *AuthMiddleware {
|
||||
|
||||
func (m *AuthMiddleware) Add() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, isAdmin, authenticationMethod, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
|
||||
userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
|
||||
if err == nil {
|
||||
c.Set("userID", userID)
|
||||
c.Set("userIsAdmin", isAdmin)
|
||||
c.Set("authenticationMethod", authenticationMethod)
|
||||
if c.IsAborted() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
authMiddleware := NewAuthMiddleware(apiKeyService, userService, jwtService)
|
||||
|
||||
user := createUserForAuthMiddlewareTest(t, db)
|
||||
jwtToken, err := jwtService.GenerateAccessToken(user, "")
|
||||
jwtToken, err := jwtService.GenerateAccessToken(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, apiKeyToken, err := apiKeyService.CreateApiKey(t.Context(), user.ID, dto.ApiKeyCreateDto{
|
||||
@@ -60,7 +60,7 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("rejects API key auth when API key auth is disabled", func(t *testing.T) {
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/protected", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/protected", nil)
|
||||
req.Header.Set("X-API-Key", apiKeyToken)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("allows JWT auth when API key auth is disabled", func(t *testing.T) {
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/protected", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+jwtToken)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -88,9 +88,10 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
func createUserForAuthMiddlewareTest(t *testing.T, db *gorm.DB) model.User {
|
||||
t.Helper()
|
||||
|
||||
email := "auth@example.com"
|
||||
user := model.User{
|
||||
Username: "auth-user",
|
||||
Email: new("auth@example.com"),
|
||||
Email: &email,
|
||||
FirstName: "Auth",
|
||||
LastName: "User",
|
||||
DisplayName: "Auth User",
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestCacheControlMiddlewareSetsDefault(t *testing.T) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", http.NoBody)
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
@@ -36,7 +36,7 @@ func TestCacheControlMiddlewarePreservesExistingHeader(t *testing.T) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/custom", http.NoBody)
|
||||
req := httptest.NewRequest(http.MethodGet, "/custom", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@@ -3,7 +3,6 @@ package middleware
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -26,7 +25,6 @@ func GetCSPNonce(c *gin.Context) string {
|
||||
|
||||
func (m *CspMiddleware) Add() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Generate a random base64 nonce for this request
|
||||
nonce := generateNonce()
|
||||
c.Set("csp_nonce", nonce)
|
||||
c.Writer.Header().Set("Content-Security-Policy", BuildCSP(nonce))
|
||||
@@ -37,18 +35,10 @@ func (m *CspMiddleware) Add() gin.HandlerFunc {
|
||||
|
||||
func BuildCSP(nonce string, formActionExtra ...string) string {
|
||||
formAction := "'self'"
|
||||
|
||||
if len(formActionExtra) > 0 {
|
||||
b := strings.Builder{}
|
||||
|
||||
for _, extra := range formActionExtra {
|
||||
if extra != "" {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(extra)
|
||||
}
|
||||
for _, extra := range formActionExtra {
|
||||
if extra != "" {
|
||||
formAction += " " + extra
|
||||
}
|
||||
|
||||
formAction += b.String()
|
||||
}
|
||||
|
||||
return "default-src 'self'; " +
|
||||
|
||||
@@ -23,6 +23,7 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
for _, err := range c.Errors {
|
||||
|
||||
// Check for record not found errors
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
errorResponse(c, http.StatusNotFound, "Record not found")
|
||||
@@ -38,56 +39,30 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// Check for slice validation errors
|
||||
svErr, ok := errors.AsType[binding.SliceValidationError](err)
|
||||
if ok {
|
||||
if errors.As(svErr[0], &validationErrors) {
|
||||
var sliceValidationErrors binding.SliceValidationError
|
||||
if errors.As(err, &sliceValidationErrors) {
|
||||
if errors.As(sliceValidationErrors[0], &validationErrors) {
|
||||
message := handleValidationError(validationErrors)
|
||||
errorResponse(c, http.StatusBadRequest, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// AppError with description
|
||||
appDescErr, ok := errors.AsType[common.AppErrorDescription](err)
|
||||
if ok {
|
||||
errorResponseWithDescription(c, appDescErr.HttpStatusCode(), appDescErr.Error(), appDescErr.Description())
|
||||
return
|
||||
}
|
||||
|
||||
// AppError (without description)
|
||||
appErr, ok := errors.AsType[common.AppError](err)
|
||||
if ok {
|
||||
var appErr common.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
errorResponse(c, appErr.HttpStatusCode(), appErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, errorResponseBody{
|
||||
Error: "Something went wrong",
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type errorResponseBody struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
func errorResponse(c *gin.Context, statusCode int, message string) {
|
||||
// Capitalize the first letter of the message
|
||||
message = strings.ToUpper(message[:1]) + message[1:]
|
||||
c.JSON(statusCode, errorResponseBody{
|
||||
Error: message,
|
||||
})
|
||||
}
|
||||
|
||||
func errorResponseWithDescription(c *gin.Context, statusCode int, message string, description string) {
|
||||
// Capitalize the first letter of the message
|
||||
message = strings.ToUpper(message[:1]) + message[1:]
|
||||
c.JSON(statusCode, errorResponseBody{
|
||||
Error: message,
|
||||
ErrorDescription: description,
|
||||
})
|
||||
c.JSON(statusCode, gin.H{"error": message})
|
||||
}
|
||||
|
||||
func handleValidationError(validationErrors validator.ValidationErrors) string {
|
||||
|
||||
@@ -16,7 +16,7 @@ type headWriter struct {
|
||||
|
||||
func (w *headWriter) Write(b []byte) (int, error) {
|
||||
w.size += len(b)
|
||||
return len(b), nil
|
||||
return w.size, nil
|
||||
}
|
||||
|
||||
func HeadMiddleware() gin.HandlerFunc {
|
||||
|
||||
@@ -20,7 +20,7 @@ func NewJwtAuthMiddleware(jwtService *service.JwtService, userService *service.U
|
||||
|
||||
func (m *JwtAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, isAdmin, authenticationMethod, err := m.Verify(c, adminRequired)
|
||||
userID, isAdmin, err := m.Verify(c, adminRequired)
|
||||
if err != nil {
|
||||
c.Abort()
|
||||
_ = c.Error(err)
|
||||
@@ -29,12 +29,11 @@ func (m *JwtAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc {
|
||||
|
||||
c.Set("userID", userID)
|
||||
c.Set("userIsAdmin", isAdmin)
|
||||
c.Set("authenticationMethod", authenticationMethod)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject string, isAdmin bool, authenticationMethod string, err error) {
|
||||
func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject string, isAdmin bool, err error) {
|
||||
// Extract the token from the cookie
|
||||
accessToken, err := c.Cookie(cookie.AccessTokenCookieName)
|
||||
if err != nil {
|
||||
@@ -42,37 +41,33 @@ func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject
|
||||
var ok bool
|
||||
_, accessToken, ok = strings.Cut(c.GetHeader("Authorization"), " ")
|
||||
if !ok || accessToken == "" {
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
return "", false, &common.NotSignedInError{}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := m.jwtService.VerifyAccessToken(accessToken)
|
||||
if err != nil {
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
}
|
||||
authenticationMethod, err = service.GetAuthenticationMethod(token)
|
||||
if err != nil {
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
return "", false, &common.NotSignedInError{}
|
||||
}
|
||||
|
||||
subject, ok := token.Subject()
|
||||
if !ok {
|
||||
_ = c.Error(&common.TokenInvalidError{})
|
||||
return "", false, "", &common.TokenInvalidError{}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.userService.GetUser(c, subject)
|
||||
if err != nil {
|
||||
return "", false, "", &common.NotSignedInError{}
|
||||
return "", false, &common.NotSignedInError{}
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
return "", false, "", &common.UserDisabledError{}
|
||||
return "", false, &common.UserDisabledError{}
|
||||
}
|
||||
|
||||
if adminRequired && !user.IsAdmin {
|
||||
return "", false, "", &common.MissingPermissionError{}
|
||||
return "", false, &common.MissingPermissionError{}
|
||||
}
|
||||
|
||||
return subject, user.IsAdmin, authenticationMethod, nil
|
||||
return subject, isAdmin, nil
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ type OidcAuthorizationCode struct {
|
||||
|
||||
Code string
|
||||
Scope string
|
||||
AuthenticationMethod string
|
||||
Nonce string
|
||||
CodeChallenge *string
|
||||
CodeChallengeMethodSha256 *bool
|
||||
@@ -48,19 +47,18 @@ type OidcAuthorizationCode struct {
|
||||
type OidcClient struct {
|
||||
Base
|
||||
|
||||
Name string `sortable:"true"`
|
||||
Secret string
|
||||
CallbackURLs UrlList
|
||||
LogoutCallbackURLs UrlList
|
||||
ImageType *string
|
||||
DarkImageType *string
|
||||
IsPublic bool
|
||||
PkceEnabled bool `sortable:"true" filterable:"true"`
|
||||
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
||||
RequiresPushedAuthorizationRequests bool `sortable:"true" filterable:"true"`
|
||||
Credentials OidcClientCredentials
|
||||
LaunchURL *string
|
||||
IsGroupRestricted bool `sortable:"true" filterable:"true"`
|
||||
Name string `sortable:"true"`
|
||||
Secret string
|
||||
CallbackURLs UrlList
|
||||
LogoutCallbackURLs UrlList
|
||||
ImageType *string
|
||||
DarkImageType *string
|
||||
IsPublic bool
|
||||
PkceEnabled bool `sortable:"true" filterable:"true"`
|
||||
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
||||
Credentials OidcClientCredentials
|
||||
LaunchURL *string
|
||||
IsGroupRestricted bool `sortable:"true" filterable:"true"`
|
||||
|
||||
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||
CreatedByID *string
|
||||
@@ -79,11 +77,9 @@ func (c OidcClient) HasDarkLogo() bool {
|
||||
type OidcRefreshToken struct {
|
||||
Base
|
||||
|
||||
Token string
|
||||
IdTokenJti *string
|
||||
ExpiresAt datatype.DateTime
|
||||
Scope string
|
||||
AuthenticationMethod string
|
||||
Token string
|
||||
ExpiresAt datatype.DateTime
|
||||
Scope string
|
||||
|
||||
UserID string
|
||||
User User
|
||||
@@ -145,45 +141,15 @@ func (cu UrlList) Value() (driver.Value, error) {
|
||||
|
||||
type OidcDeviceCode struct {
|
||||
Base
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scope string
|
||||
AuthenticationMethod string
|
||||
Nonce string
|
||||
ExpiresAt datatype.DateTime
|
||||
IsAuthorized bool
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scope string
|
||||
Nonce string
|
||||
ExpiresAt datatype.DateTime
|
||||
IsAuthorized bool
|
||||
|
||||
UserID *string
|
||||
User User
|
||||
ClientID string
|
||||
Client OidcClient
|
||||
}
|
||||
|
||||
type OidcPushedAuthorizationRequest struct {
|
||||
Base
|
||||
|
||||
RequestURI string
|
||||
ClientID string
|
||||
Parameters OidcAuthorizationRequestParameters
|
||||
ExpiresAt datatype.DateTime
|
||||
}
|
||||
|
||||
type OidcAuthorizationRequestParameters struct { //nolint:recvcheck
|
||||
Scope string `json:"scope,omitempty"`
|
||||
RedirectURI string `json:"redirect_uri,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
CodeChallenge string `json:"code_challenge,omitempty"`
|
||||
CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
|
||||
ResponseType string `json:"response_type,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
ResponseMode string `json:"response_mode,omitempty"`
|
||||
}
|
||||
|
||||
func (p *OidcAuthorizationRequestParameters) Scan(value any) error {
|
||||
return utils.UnmarshalJSONFromDatabase(p, value)
|
||||
}
|
||||
|
||||
func (p OidcAuthorizationRequestParameters) Value() (driver.Value, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
@@ -265,9 +265,9 @@ func (s *AppConfigService) UpdateAppConfigValues(ctx context.Context, keysAndVal
|
||||
// We update the in-memory data (in the cfg struct) and collect values to update in the database
|
||||
// (Note the += 2, as we are iterating through key-value pairs)
|
||||
dbUpdate := make([]model.AppConfigVariable, 0, len(keysAndValues)/2)
|
||||
for i := 1; i < len(keysAndValues); i += 2 {
|
||||
key := keysAndValues[i-1]
|
||||
value := keysAndValues[i]
|
||||
for i := 0; i < len(keysAndValues); i += 2 {
|
||||
key := keysAndValues[i]
|
||||
value := keysAndValues[i+1]
|
||||
|
||||
// Ensure that the field is valid
|
||||
// We do this by grabbing the default value
|
||||
@@ -408,7 +408,6 @@ func (s *AppConfigService) loadDbConfigFromEnv(ctx context.Context, tx *gorm.DB)
|
||||
if attrs == "sensitive" {
|
||||
fileName := os.Getenv(envVarName + "_FILE")
|
||||
if fileName != "" {
|
||||
// #nosec G703 - Value is provided by admin
|
||||
b, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret '%s' from file '%s': %w", envVarName, fileName, err)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
imageutil "github.com/pocket-id/pocket-id/backend/internal/utils/image"
|
||||
)
|
||||
|
||||
type AppImagesService struct {
|
||||
@@ -69,12 +68,7 @@ func (s *AppImagesService) UpdateImage(ctx context.Context, file *multipart.File
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
strippedReader, err := imageutil.StripMetadata(fileReader, fileType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.storage.Save(ctx, imagePath, strippedReader); err != nil {
|
||||
if err := s.storage.Save(ctx, imagePath, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
@@ -57,29 +56,6 @@ func TestAppImagesService_UpdateImage(t *testing.T) {
|
||||
require.ErrorIs(t, err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
func TestAppImagesService_UpdateImageStripsMetadata(t *testing.T) {
|
||||
store, err := storage.NewFilesystemStorage(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
|
||||
service := NewAppImagesService(map[string]string{}, store)
|
||||
|
||||
fileHeader := newFileHeader(t, "logo.webp", webpFile(
|
||||
webpChunk("VP8 ", []byte{1, 2, 3, 4}),
|
||||
webpChunk("EXIF", []byte("secret")),
|
||||
))
|
||||
|
||||
require.NoError(t, service.UpdateImage(context.Background(), fileHeader, "logoLight"))
|
||||
|
||||
reader, _, err := store.Open(context.Background(), path.Join("application-images", "logoLight.webp"))
|
||||
require.NoError(t, err)
|
||||
defer reader.Close()
|
||||
|
||||
payload, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(payload), "secret")
|
||||
assert.Contains(t, string(payload), "VP8 ")
|
||||
}
|
||||
|
||||
func TestAppImagesService_ErrorsAndFlags(t *testing.T) {
|
||||
store, err := storage.NewFilesystemStorage(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
@@ -128,7 +104,7 @@ func newFileHeader(t *testing.T, filename string, content []byte) *multipart.Fil
|
||||
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", body)
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
_, fileHeader, err := req.FormFile("file")
|
||||
@@ -136,26 +112,3 @@ func newFileHeader(t *testing.T, filename string, content []byte) *multipart.Fil
|
||||
|
||||
return fileHeader
|
||||
}
|
||||
|
||||
func webpFile(chunks ...[]byte) []byte {
|
||||
var out bytes.Buffer
|
||||
out.WriteString("RIFF")
|
||||
out.Write([]byte{0, 0, 0, 0})
|
||||
out.WriteString("WEBP")
|
||||
for _, chunk := range chunks {
|
||||
out.Write(chunk)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(out.Bytes()[4:8], uint32(out.Len()-8)) //nolint:gosec
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
func webpChunk(chunkType string, data []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
out.WriteString(chunkType)
|
||||
_ = binary.Write(&out, binary.LittleEndian, uint32(len(data))) //nolint:gosec
|
||||
out.Write(data)
|
||||
if len(data)%2 == 1 {
|
||||
out.WriteByte(0)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
@@ -154,7 +154,8 @@ func TestAppLockServiceAcquire(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
service := newTestAppLockService(t, db)
|
||||
|
||||
err := db.Create(&model.KV{Key: lockKey, Value: new("this-is-not-json")}).Error
|
||||
raw := "this-is-not-json"
|
||||
err := db.Create(&model.KV{Key: lockKey, Value: &raw}).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Acquire(context.Background(), false)
|
||||
|
||||
@@ -92,7 +92,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
|
||||
// If the user hasn't logged in from the same device before and email notifications are enabled, send an email
|
||||
if s.appConfigService.GetDbConfig().EmailLoginNotificationEnabled.IsTrue() && count <= 1 {
|
||||
// #nosec G118 - We use a background context here as this is running in a goroutine
|
||||
// We use a background context here as this is running in a goroutine
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
|
||||
@@ -24,7 +24,6 @@ func isReservedClaim(key string) bool {
|
||||
"family_name",
|
||||
"name",
|
||||
"email",
|
||||
"email_verified",
|
||||
"preferred_username",
|
||||
"display_name",
|
||||
"groups",
|
||||
|
||||
@@ -236,15 +236,6 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
userGroups[1],
|
||||
},
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "a1b2c3d4-e5f6-7890-abcd-ef0000000001",
|
||||
},
|
||||
Name: "PAR Test Client",
|
||||
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
|
||||
CallbackURLs: model.UrlList{"http://par-client/auth/callback"},
|
||||
CreatedByID: new(users[0].ID),
|
||||
},
|
||||
}
|
||||
for _, client := range oidcClients {
|
||||
if err := tx.Create(&client).Error; err != nil {
|
||||
@@ -254,22 +245,20 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
|
||||
authCodes := []model.OidcAuthorizationCode{
|
||||
{
|
||||
Code: "auth-code",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
AuthenticationMethod: AuthenticationMethodPhishingResistant,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
Code: "auth-code",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
},
|
||||
{
|
||||
Code: "federated",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
AuthenticationMethod: AuthenticationMethodPhishingResistant,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[1].ID,
|
||||
ClientID: oidcClients[3].ID,
|
||||
Code: "federated",
|
||||
Scope: "openid profile",
|
||||
Nonce: "nonce",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
UserID: users[1].ID,
|
||||
ClientID: oidcClients[3].ID,
|
||||
},
|
||||
}
|
||||
for _, authCode := range authCodes {
|
||||
@@ -279,13 +268,11 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
}
|
||||
|
||||
refreshToken := model.OidcRefreshToken{
|
||||
Token: utils.CreateSha256Hash("ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo"),
|
||||
IdTokenJti: new("dd75f6f6-ce0a-44b7-a645-7de390ccd2fa"),
|
||||
AuthenticationMethod: AuthenticationMethodPhishingResistant,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
Token: utils.CreateSha256Hash("ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo"),
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
}
|
||||
if err := tx.Create(&refreshToken).Error; err != nil {
|
||||
return err
|
||||
@@ -319,12 +306,6 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
ClientID: oidcClients[3].ID,
|
||||
LastUsedAt: datatype.DateTime(time.Date(2025, 8, 12, 12, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
{
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[5].ID,
|
||||
LastUsedAt: datatype.DateTime(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}
|
||||
for _, userAuthorizedClient := range userAuthorizedClients {
|
||||
if err := tx.Create(&userAuthorizedClient).Error; err != nil {
|
||||
|
||||
@@ -32,15 +32,6 @@ const (
|
||||
// RefreshTokenClaim is the claim used for the refresh token's value
|
||||
RefreshTokenClaim = "rt"
|
||||
|
||||
// AuthenticationMethodsClaim is the claim used to identify how the user authenticated
|
||||
AuthenticationMethodsClaim = "amr"
|
||||
|
||||
// AuthenticationMethodPhishingResistant identifies phishing-resistant authentication, such as passkeys
|
||||
AuthenticationMethodPhishingResistant = "phr"
|
||||
|
||||
// AuthenticationMethodOneTimePassword identifies one-time password/code authentication
|
||||
AuthenticationMethodOneTimePassword = "otp"
|
||||
|
||||
// OAuthAccessTokenJWTType identifies a JWT as an OAuth access token
|
||||
OAuthAccessTokenJWTType = "oauth-access-token" //nolint:gosec
|
||||
|
||||
@@ -196,8 +187,7 @@ func (s *JwtService) SetKey(privateKey jwk.Key) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JwtService) GenerateAccessToken(user model.User, authenticationMethod string) (string, error) {
|
||||
|
||||
func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
||||
now := time.Now()
|
||||
token, err := jwt.NewBuilder().
|
||||
Subject(user.ID).
|
||||
@@ -225,11 +215,6 @@ func (s *JwtService) GenerateAccessToken(user model.User, authenticationMethod s
|
||||
return "", fmt.Errorf("failed to set 'isAdmin' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAuthenticationMethods(token, authenticationMethod)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to set '%s' claim in token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
alg, _ := s.privateKey.Algorithm()
|
||||
signed, err := jwt.Sign(token, jwt.WithKey(alg, s.privateKey))
|
||||
if err != nil {
|
||||
@@ -258,65 +243,59 @@ func (s *JwtService) VerifyAccessToken(tokenString string) (jwt.Token, error) {
|
||||
}
|
||||
|
||||
// BuildIDToken creates an ID token with all claims
|
||||
func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, nonce string, authenticationMethod string) (jwt.Token, string, error) {
|
||||
func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, nonce string) (jwt.Token, error) {
|
||||
now := time.Now()
|
||||
jti := uuid.New().String()
|
||||
token, err := jwt.NewBuilder().
|
||||
Expiration(now.Add(1 * time.Hour)).
|
||||
IssuedAt(now).
|
||||
Issuer(s.envConfig.AppURL).
|
||||
JwtID(jti).
|
||||
JwtID(uuid.New().String()).
|
||||
Build()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to build token: %w", err)
|
||||
return nil, fmt.Errorf("failed to build token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAudienceString(token, clientID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to set 'aud' claim in token: %w", err)
|
||||
return nil, fmt.Errorf("failed to set 'aud' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetTokenType(token, IDTokenJWTType)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to set 'type' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAuthenticationMethods(token, authenticationMethod)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to set '%s' claim in token: %w", AuthenticationMethodsClaim, err)
|
||||
return nil, fmt.Errorf("failed to set 'type' claim in token: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range userClaims {
|
||||
err = token.Set(k, v)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to set claim '%s': %w", k, err)
|
||||
return nil, fmt.Errorf("failed to set claim '%s': %w", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
err = token.Set("nonce", nonce)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to set claim 'nonce': %w", err)
|
||||
return nil, fmt.Errorf("failed to set claim 'nonce': %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return token, jti, nil
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GenerateIDToken creates and signs an ID token
|
||||
func (s *JwtService) GenerateIDToken(userClaims map[string]any, clientID string, nonce string, authenticationMethod string) (signedToken, jti string, err error) {
|
||||
token, jti, err := s.BuildIDToken(userClaims, clientID, nonce, authenticationMethod)
|
||||
func (s *JwtService) GenerateIDToken(userClaims map[string]any, clientID string, nonce string) (string, error) {
|
||||
token, err := s.BuildIDToken(userClaims, clientID, nonce)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
|
||||
alg, _ := s.privateKey.Algorithm()
|
||||
signed, err := jwt.Sign(token, jwt.WithKey(alg, s.privateKey))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to sign token: %w", err)
|
||||
return "", fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return string(signed), jti, nil
|
||||
return string(signed), nil
|
||||
}
|
||||
|
||||
func (s *JwtService) VerifyIdToken(tokenString string, acceptExpiredTokens bool) (jwt.Token, error) {
|
||||
@@ -353,7 +332,7 @@ func (s *JwtService) VerifyIdToken(tokenString string, acceptExpiredTokens bool)
|
||||
}
|
||||
|
||||
// BuildOAuthAccessToken creates an OAuth access token with all claims
|
||||
func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string, authenticationMethod string) (jwt.Token, error) {
|
||||
func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string) (jwt.Token, error) {
|
||||
now := time.Now()
|
||||
token, err := jwt.NewBuilder().
|
||||
Subject(user.ID).
|
||||
@@ -376,17 +355,12 @@ func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string, aut
|
||||
return nil, fmt.Errorf("failed to set 'type' claim in token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAuthenticationMethods(token, authenticationMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set '%s' claim in token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GenerateOAuthAccessToken creates and signs an OAuth access token
|
||||
func (s *JwtService) GenerateOAuthAccessToken(user model.User, clientID string, authenticationMethod string) (string, error) {
|
||||
token, err := s.BuildOAuthAccessToken(user, clientID, authenticationMethod)
|
||||
func (s *JwtService) GenerateOAuthAccessToken(user model.User, clientID string) (string, error) {
|
||||
token, err := s.BuildOAuthAccessToken(user, clientID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -560,27 +534,6 @@ func GetIsAdmin(token jwt.Token) (bool, error) {
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
// GetAuthenticationMethod returns the first authentication method in the "amr" claim in the token
|
||||
func GetAuthenticationMethod(token jwt.Token) (string, error) {
|
||||
if !token.Has(AuthenticationMethodsClaim) {
|
||||
return "", nil
|
||||
}
|
||||
var rawAuthenticationMethods []any
|
||||
err := token.Get(AuthenticationMethodsClaim, &rawAuthenticationMethods)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get '%s' claim from token: %w", AuthenticationMethodsClaim, err)
|
||||
}
|
||||
|
||||
if len(rawAuthenticationMethods) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
authenticationMethod, ok := rawAuthenticationMethods[0].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid '%s' claim in token: expected array of strings", AuthenticationMethodsClaim)
|
||||
}
|
||||
return authenticationMethod, nil
|
||||
}
|
||||
|
||||
// SetTokenType sets the "type" claim in the token
|
||||
func SetTokenType(token jwt.Token, tokenType string) error {
|
||||
if tokenType == "" {
|
||||
@@ -598,14 +551,6 @@ func SetIsAdmin(token jwt.Token, isAdmin bool) error {
|
||||
return token.Set(IsAdminClaim, isAdmin)
|
||||
}
|
||||
|
||||
// SetAuthenticationMethods sets the authentication method references claim in the token
|
||||
func SetAuthenticationMethods(token jwt.Token, authenticationMethod string) error {
|
||||
if authenticationMethod == "" {
|
||||
return nil
|
||||
}
|
||||
return token.Set(AuthenticationMethodsClaim, []string{authenticationMethod})
|
||||
}
|
||||
|
||||
// SetAudienceString sets the "aud" claim with a value that is a string, and not an array
|
||||
// This is permitted by RFC 7519, and it's done here for backwards-compatibility
|
||||
func SetAudienceString(token jwt.Token, audience string) error {
|
||||
|
||||
@@ -174,7 +174,6 @@ func TestJwtService_Init(t *testing.T) {
|
||||
_ = assert.True(t, ok) &&
|
||||
assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestJwtService_GetPublicJWK(t *testing.T) {
|
||||
@@ -309,7 +308,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -322,9 +321,6 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
isAdmin, err := GetIsAdmin(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get isAdmin claim") &&
|
||||
assert.False(t, isAdmin, "isAdmin should be false")
|
||||
authenticationMethod, err := GetAuthenticationMethod(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get amr claim") &&
|
||||
assert.Empty(t, authenticationMethod, "amr should be empty when not specified")
|
||||
audience, ok := claims.Audience()
|
||||
_ = assert.True(t, ok, "Audience not found in token") &&
|
||||
assert.Equal(t, []string{service.envConfig.AppURL}, audience, "Audience should contain the app URL")
|
||||
@@ -348,7 +344,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(adminUser, "")
|
||||
tokenString, err := service.GenerateAccessToken(adminUser)
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
|
||||
claims, err := service.VerifyAccessToken(tokenString)
|
||||
@@ -362,24 +358,6 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
assert.Equal(t, adminUser.ID, subject, "Token subject should match user ID")
|
||||
})
|
||||
|
||||
t.Run("sets authentication method references claim when provided", func(t *testing.T) {
|
||||
service, _, _ := setupJwtService(t, mockConfig)
|
||||
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "user-with-auth-method"},
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, AuthenticationMethodPhishingResistant)
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
|
||||
claims, err := service.VerifyAccessToken(tokenString)
|
||||
require.NoError(t, err, "Failed to verify generated token")
|
||||
|
||||
authenticationMethod, err := GetAuthenticationMethod(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get amr claim") &&
|
||||
assert.Equal(t, AuthenticationMethodPhishingResistant, authenticationMethod, "amr should match")
|
||||
})
|
||||
|
||||
t.Run("uses session duration from config", func(t *testing.T) {
|
||||
customMockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "30"}, // 30 minutes
|
||||
@@ -390,7 +368,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
Base: model.Base{ID: "user456"},
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
require.NoError(t, err, "Failed to generate access token")
|
||||
|
||||
claims, err := service.VerifyAccessToken(tokenString)
|
||||
@@ -418,7 +396,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
require.NoError(t, err, "Failed to generate access token with Ed25519 key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -455,7 +433,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
require.NoError(t, err, "Failed to generate access token with ECDSA key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -492,7 +470,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
tokenString, err := service.GenerateAccessToken(user, "")
|
||||
tokenString, err := service.GenerateAccessToken(user)
|
||||
require.NoError(t, err, "Failed to generate access token with RSA key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -530,10 +508,9 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "test-client-123"
|
||||
|
||||
tokenString, jti, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate ID token")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
assert.Regexp(t, uuidRegexPattern, jti, "Returned JWT ID is not a UUID")
|
||||
|
||||
claims, err := service.VerifyIdToken(tokenString, false)
|
||||
require.NoError(t, err, "Failed to verify generated ID token")
|
||||
@@ -550,7 +527,6 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
jwtID, ok := claims.JwtID()
|
||||
_ = assert.True(t, ok, "JWT ID not found in token") &&
|
||||
assert.Regexp(t, uuidRegexPattern, jwtID, "JWT ID is not a UUID")
|
||||
assert.Equal(t, jti, jwtID, "Returned JWT ID should match token claim")
|
||||
|
||||
expectedExp := time.Now().Add(1 * time.Hour)
|
||||
expiration, ok := claims.Expiration()
|
||||
@@ -617,7 +593,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
const clientID = "test-client-456"
|
||||
nonce := "random-nonce-value"
|
||||
|
||||
tokenString, _, err := service.GenerateIDToken(userClaims, clientID, nonce, "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, nonce)
|
||||
require.NoError(t, err, "Failed to generate ID token with nonce")
|
||||
|
||||
publicKey, err := service.GetPublicJWK()
|
||||
@@ -638,7 +614,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
userClaims := map[string]any{
|
||||
"sub": "user789",
|
||||
}
|
||||
tokenString, _, err := service.GenerateIDToken(userClaims, "client-789", "", "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, "client-789", "")
|
||||
require.NoError(t, err, "Failed to generate ID token")
|
||||
|
||||
service.envConfig.AppURL = "https://wrong-issuer.com"
|
||||
@@ -664,7 +640,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "eddsa-client-123"
|
||||
|
||||
tokenString, _, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate ID token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -701,7 +677,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "ecdsa-client-123"
|
||||
|
||||
tokenString, _, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate ID token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -739,7 +715,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "rsa-client-123"
|
||||
|
||||
tokenString, _, err := service.GenerateIDToken(userClaims, clientID, "", "")
|
||||
tokenString, err := service.GenerateIDToken(userClaims, clientID, "")
|
||||
require.NoError(t, err, "Failed to generate ID token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -769,7 +745,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "test-client-123"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -796,25 +772,6 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
assert.InDelta(t, 0, timeDiff, 1.0, "Token should expire in approximately 1 hour")
|
||||
})
|
||||
|
||||
t.Run("sets authentication method references claim when provided", func(t *testing.T) {
|
||||
service, _, _ := setupJwtService(t, mockConfig)
|
||||
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "oauth-amr-user"},
|
||||
}
|
||||
const clientID = "test-client-amr"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, AuthenticationMethodPhishingResistant)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token")
|
||||
|
||||
claims, err := service.VerifyOAuthAccessToken(tokenString)
|
||||
require.NoError(t, err, "Failed to verify generated OAuth access token")
|
||||
|
||||
authenticationMethod, err := GetAuthenticationMethod(claims)
|
||||
_ = assert.NoError(t, err, "Failed to get amr claim") &&
|
||||
assert.Equal(t, AuthenticationMethodPhishingResistant, authenticationMethod, "amr should match")
|
||||
})
|
||||
|
||||
t.Run("fails verification for expired token", func(t *testing.T) {
|
||||
service, _, _ := setupJwtService(t, mockConfig)
|
||||
|
||||
@@ -848,7 +805,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
user := model.User{Base: model.Base{ID: "user789"}}
|
||||
const clientID = "test-client-789"
|
||||
|
||||
tokenString, err := service1.GenerateOAuthAccessToken(user, clientID, "")
|
||||
tokenString, err := service1.GenerateOAuthAccessToken(user, clientID)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token")
|
||||
|
||||
_, err = service2.VerifyOAuthAccessToken(tokenString)
|
||||
@@ -871,7 +828,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "eddsa-oauth-client"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -908,7 +865,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "ecdsa-oauth-client"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
@@ -945,7 +902,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
}
|
||||
const clientID = "rsa-oauth-client"
|
||||
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID, "")
|
||||
tokenString, err := service.GenerateOAuthAccessToken(user, clientID)
|
||||
require.NoError(t, err, "Failed to generate OAuth access token with key")
|
||||
assert.NotEmpty(t, tokenString, "Token should not be empty")
|
||||
|
||||
|
||||
@@ -391,12 +391,6 @@ func (s *LdapService) resolveGroupMemberUsername(ctx context.Context, client lda
|
||||
return norm.NFC.String(username)
|
||||
}
|
||||
|
||||
// posixGroup (and similar) stores bare usernames in memberUid, not DNs. Treat any value
|
||||
// that is not a valid DN as the username directly — see https://github.com/pocket-id/pocket-id/issues/1408
|
||||
if _, err := ldap.ParseDN(member); err != nil {
|
||||
return norm.NFC.String(member)
|
||||
}
|
||||
|
||||
// As a fallback, query LDAP for the referenced entry
|
||||
userSearchReq := ldap.NewSearchRequest(
|
||||
member,
|
||||
@@ -583,7 +577,8 @@ func (s *LdapService) reconcileUsers(ctx context.Context, tx *gorm.DB, desiredUs
|
||||
|
||||
err = s.userService.deleteUserInternal(ctx, tx, user.ID, true)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*common.LdapUserUpdateError](err); ok {
|
||||
target := &common.LdapUserUpdateError{}
|
||||
if errors.As(err, &target) {
|
||||
return nil, nil, fmt.Errorf("failed to delete user %s: LDAP user must be disabled before deletion", user.Username)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to delete user %s: %w", user.Username, err)
|
||||
|
||||
@@ -141,48 +141,6 @@ func TestLdapServiceSyncAllReconcilesUsersAndGroups(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{"alice", "bob"}, usernames(team.Users))
|
||||
}
|
||||
|
||||
// Regression: posixGroup uses memberUid (bare uid values), not member DNs — issue #1408.
|
||||
func TestLdapServiceSyncAllMapsPosixGroupMemberUid(t *testing.T) {
|
||||
appCfg := defaultTestLDAPAppConfig()
|
||||
appCfg.LdapUserGroupSearchFilter = model.AppConfigVariable{Value: "(objectClass=posixGroup)"}
|
||||
appCfg.LdapAttributeGroupMember = model.AppConfigVariable{Value: "memberUid"}
|
||||
|
||||
service, db := newTestLdapServiceWithAppConfig(t, appCfg, newFakeLDAPClient(
|
||||
ldapSearchResult(
|
||||
ldapEntry("uid=alice,ou=users,dc=example,dc=com", map[string][]string{
|
||||
"entryUUID": {"u-alice"},
|
||||
"uid": {"alice"},
|
||||
"mail": {"alice@example.com"},
|
||||
"givenName": {"Alice"},
|
||||
"sn": {"Jones"},
|
||||
"displayName": {""},
|
||||
}),
|
||||
ldapEntry("uid=bob,ou=users,dc=example,dc=com", map[string][]string{
|
||||
"entryUUID": {"u-bob"},
|
||||
"uid": {"bob"},
|
||||
"mail": {"bob@example.com"},
|
||||
"givenName": {"Bob"},
|
||||
"sn": {"Brown"},
|
||||
"displayName": {""},
|
||||
}),
|
||||
),
|
||||
ldapSearchResult(
|
||||
ldapEntry("cn=users,ou=groups,dc=example,dc=com", map[string][]string{
|
||||
"entryUUID": {"g-users"},
|
||||
"cn": {"users"},
|
||||
"memberUid": {"alice", "bob", "unknown"},
|
||||
}),
|
||||
),
|
||||
))
|
||||
|
||||
require.NoError(t, service.SyncAll(t.Context()))
|
||||
|
||||
var group model.UserGroup
|
||||
require.NoError(t, db.Preload("Users").First(&group, "ldap_id = ?", "g-users").Error)
|
||||
assert.Equal(t, "users", group.Name)
|
||||
assert.ElementsMatch(t, []string{"alice", "bob"}, usernames(group.Users))
|
||||
}
|
||||
|
||||
func TestLdapServiceSyncAllHandlesDuplicateLDAPIDsInSingleRun(t *testing.T) {
|
||||
service, db := newTestLdapService(t, newFakeLDAPClient(
|
||||
ldapSearchResult(
|
||||
@@ -367,7 +325,7 @@ func newFakeLDAPClient(userResult, groupResult *ldap.SearchResult) ldapClient {
|
||||
switch searchRequest.Filter {
|
||||
case "(objectClass=person)":
|
||||
return userResult, nil
|
||||
case "(objectClass=groupOfNames)", "(objectClass=posixGroup)":
|
||||
case "(objectClass=groupOfNames)":
|
||||
return groupResult, nil
|
||||
default:
|
||||
return &ldap.SearchResult{}, nil
|
||||
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
imageutil "github.com/pocket-id/pocket-id/backend/internal/utils/image"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,9 +47,6 @@ const (
|
||||
AccessTokenDuration = time.Hour
|
||||
RefreshTokenDuration = 30 * 24 * time.Hour // 30 days
|
||||
DeviceCodeDuration = 15 * time.Minute
|
||||
PARDuration = 90 * time.Second
|
||||
|
||||
parRequestURIPrefix = "urn:ietf:params:oauth:request_uri:"
|
||||
)
|
||||
|
||||
type OidcService struct {
|
||||
@@ -127,9 +123,11 @@ func (s *OidcService) getJWKCache(ctx context.Context) (*jwk.Cache, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClientRequestDto, userID string, authenticationMethod string, ipAddress, userAgent string) (string, string, error) {
|
||||
func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClientRequestDto, userID, ipAddress, userAgent string) (string, string, error) {
|
||||
tx := s.db.Begin()
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var client model.OidcClient
|
||||
err := tx.
|
||||
@@ -141,14 +139,12 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// If the client requires PAR, a request_uri must be provided
|
||||
if client.RequiresPushedAuthorizationRequests && input.RequestURI == "" {
|
||||
return "", "", &common.OidcPARRequiredError{}
|
||||
}
|
||||
|
||||
// If a request_uri is provided, consume the stored PAR and overwrite input fields
|
||||
if input.RequestURI != "" {
|
||||
if err := s.applyPushedAuthorizationRequest(ctx, tx, &input); err != nil {
|
||||
if client.RequiresReauthentication {
|
||||
if input.ReauthenticationToken == "" {
|
||||
return "", "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
err = s.webAuthnService.ConsumeReauthenticationToken(ctx, tx, input.ReauthenticationToken, userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
@@ -158,42 +154,12 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", &common.OidcMissingCodeChallengeError{}
|
||||
}
|
||||
|
||||
// Validate the callback URL before any prompt checks, so that prompt-related
|
||||
// error responses never contain an unvalidated redirect target
|
||||
// Get the callback URL of the client. Return an error if the provided callback URL is not allowed
|
||||
callbackURL, err := s.getCallbackURL(&client, input.CallbackURL, tx, ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Parse prompt parameter (space-delimited list per OIDC spec)
|
||||
promptValues := parsePromptParameter(input.Prompt)
|
||||
hasPromptNone := slices.Contains(promptValues, "none")
|
||||
hasPromptLogin := slices.Contains(promptValues, "login")
|
||||
hasPromptConsent := slices.Contains(promptValues, "consent")
|
||||
hasPromptSelectAccount := slices.Contains(promptValues, "select_account")
|
||||
|
||||
// Validate prompt parameter conflicts early.
|
||||
// Per OIDC Core §3.1.2.6, prompt=none must not be combined with any
|
||||
// value that requires user interaction.
|
||||
if hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount) {
|
||||
return "", "", common.NewOidcInvalidRequestError("prompt type 'none' cannot be combined with others")
|
||||
}
|
||||
|
||||
// prompt=select_account is handled entirely in the UI
|
||||
// Pocket ID holds one session per browser, so the frontend renders the current user as the sole selectable account and then calls Authorize normally.
|
||||
|
||||
// If prompt=login is specified or the client requires reauthentication, check the reauthentication token
|
||||
if hasPromptLogin || client.RequiresReauthentication {
|
||||
if input.ReauthenticationToken == "" {
|
||||
return "", "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
|
||||
err = s.webAuthnService.ConsumeReauthenticationToken(ctx, tx, input.ReauthenticationToken, userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user group is allowed to authorize the client
|
||||
var user model.User
|
||||
err = tx.
|
||||
@@ -209,95 +175,37 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
return "", "", &common.OidcAccessDeniedError{}
|
||||
}
|
||||
|
||||
// Handle prompt=none - if consent would be required, we can't show UI
|
||||
if hasPromptNone {
|
||||
hasAlreadyAuthorized, err := s.hasAuthorizedClientInternal(ctx, input.ClientID, userID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !hasAlreadyAuthorized {
|
||||
return "", callbackURL, &common.OidcConsentRequiredError{}
|
||||
}
|
||||
}
|
||||
|
||||
hasAlreadyAuthorizedClient, err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Create the authorization code
|
||||
code, err := s.createAuthorizationCode(ctx, input.ClientID, userID, input.Scope, authenticationMethod, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod, tx)
|
||||
code, err := s.createAuthorizationCode(ctx, input.ClientID, userID, input.Scope, input.Nonce, input.CodeChallenge, input.CodeChallengeMethod, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Log the authorization event
|
||||
if hasAlreadyAuthorizedClient {
|
||||
s.auditLogService.Create(
|
||||
ctx, model.AuditLogEventClientAuthorization,
|
||||
ipAddress, userAgent, userID,
|
||||
model.AuditLogData{"clientName": client.Name},
|
||||
tx,
|
||||
)
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx)
|
||||
} else {
|
||||
s.auditLogService.Create(
|
||||
ctx, model.AuditLogEventNewClientAuthorization,
|
||||
ipAddress, userAgent, userID,
|
||||
model.AuditLogData{"clientName": client.Name},
|
||||
tx,
|
||||
)
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventNewClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx)
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to commit transaction: %w", err)
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return code, callbackURL, nil
|
||||
}
|
||||
|
||||
// applyPushedAuthorizationRequest consumes the stored PAR for the given request_uri
|
||||
// and overwrites the corresponding fields on input.
|
||||
func (s *OidcService) applyPushedAuthorizationRequest(ctx context.Context, tx *gorm.DB, input *dto.AuthorizeOidcClientRequestDto) error {
|
||||
parMeta, err := s.getAndConsumePushedAuthorizationRequest(ctx, tx, input.ClientID, input.RequestURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
par := parMeta.Parameters
|
||||
|
||||
input.Scope = par.Scope
|
||||
input.CallbackURL = par.RedirectURI
|
||||
input.Nonce = par.Nonce
|
||||
input.Prompt = par.Prompt
|
||||
input.CodeChallenge = par.CodeChallenge
|
||||
input.CodeChallengeMethod = par.CodeChallengeMethod
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasAuthorizedClient checks if the user has already authorized the client with the given scope
|
||||
func (s *OidcService) HasAuthorizedClient(ctx context.Context, clientID, userID, scope string) (bool, error) {
|
||||
return s.hasAuthorizedClientInternal(ctx, clientID, userID, scope, s.db)
|
||||
}
|
||||
|
||||
// AuthorizationRequired reports whether the user must confirm authorization for the client.
|
||||
func (s *OidcService) AuthorizationRequired(ctx context.Context, clientID, userID, scope, requestURI string) (required bool, resolvedScope string, err error) {
|
||||
if requestURI != "" {
|
||||
par, err := s.getPushedAuthorizationRequestInternal(ctx, s.db, clientID, requestURI)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
scope = par.Parameters.Scope
|
||||
}
|
||||
|
||||
hasAuthorized, err := s.hasAuthorizedClientInternal(ctx, clientID, userID, scope, s.db)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
return !hasAuthorized, scope, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) hasAuthorizedClientInternal(ctx context.Context, clientID, userID, scope string, tx *gorm.DB) (bool, error) {
|
||||
var userAuthorizedOidcClient model.UserAuthorizedOidcClient
|
||||
err := tx.
|
||||
@@ -406,17 +314,17 @@ func (s *OidcService) createTokenFromDeviceCode(ctx context.Context, input dto.O
|
||||
}
|
||||
|
||||
// Explicitly use the input clientID for the audience claim to ensure consistency
|
||||
idToken, idTokenJti, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, deviceAuth.Nonce, deviceAuth.AuthenticationMethod)
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, deviceAuth.Nonce)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, *deviceAuth.UserID, deviceAuth.Scope, deviceAuth.AuthenticationMethod, idTokenJti, tx)
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, *deviceAuth.UserID, deviceAuth.Scope, tx)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(deviceAuth.User, input.ClientID, deviceAuth.AuthenticationMethod)
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(deviceAuth.User, input.ClientID)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -457,7 +365,7 @@ func (s *OidcService) createTokenFromClientCredentials(ctx context.Context, inpu
|
||||
audClaim = input.Resource
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(dummyUser, audClaim, "")
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(dummyUser, audClaim)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -505,20 +413,18 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, inpu
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
authenticationMethod := authorizationCodeMetaData.AuthenticationMethod
|
||||
|
||||
idToken, idTokenJti, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, authorizationCodeMetaData.Nonce, authenticationMethod)
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, authorizationCodeMetaData.Nonce)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
// Generate a refresh token
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, authorizationCodeMetaData.UserID, authorizationCodeMetaData.Scope, authenticationMethod, idTokenJti, tx)
|
||||
refreshToken, err := s.createRefreshToken(ctx, input.ClientID, authorizationCodeMetaData.UserID, authorizationCodeMetaData.Scope, tx)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(authorizationCodeMetaData.User, input.ClientID, authenticationMethod)
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(authorizationCodeMetaData.User, input.ClientID)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -595,46 +501,8 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, input dto
|
||||
return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
|
||||
}
|
||||
|
||||
if storedRefreshToken.User.Disabled {
|
||||
return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
|
||||
}
|
||||
|
||||
var authorizedClient model.UserAuthorizedOidcClient
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ? AND client_id = ?", storedRefreshToken.UserID, input.ClientID).
|
||||
First(&authorizedClient).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = tx.WithContext(ctx).Delete(&storedRefreshToken).Error
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
|
||||
} else if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
if client.IsGroupRestricted {
|
||||
err = tx.WithContext(ctx).Model(client).Association("AllowedUserGroups").Find(&client.AllowedUserGroups)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if !IsUserGroupAllowedToAuthorize(storedRefreshToken.User, *client) {
|
||||
return CreatedTokens{}, &common.OidcAccessDeniedError{}
|
||||
}
|
||||
|
||||
// Generate a new access token
|
||||
authenticationMethods := storedRefreshToken.AuthenticationMethod
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(storedRefreshToken.User, input.ClientID, authenticationMethods)
|
||||
accessToken, err := s.jwtService.GenerateOAuthAccessToken(storedRefreshToken.User, input.ClientID)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -647,13 +515,13 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, input dto
|
||||
|
||||
// Generate a new ID token
|
||||
// There's no nonce here because we don't have one with the refresh token, but that's not required
|
||||
idToken, idTokenJti, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, "", authenticationMethods)
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, "")
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
|
||||
// Generate a new refresh token and invalidate the old one
|
||||
newRefreshToken, err := s.createRefreshToken(ctx, input.ClientID, storedRefreshToken.UserID, storedRefreshToken.Scope, authenticationMethods, idTokenJti, tx)
|
||||
newRefreshToken, err := s.createRefreshToken(ctx, input.ClientID, storedRefreshToken.UserID, storedRefreshToken.Scope, tx)
|
||||
if err != nil {
|
||||
return CreatedTokens{}, err
|
||||
}
|
||||
@@ -798,10 +666,11 @@ func (s *OidcService) introspectRefreshToken(ctx context.Context, clientID strin
|
||||
).
|
||||
First(&storedRefreshToken).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
introspectDto.Active = false
|
||||
return introspectDto, nil
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
introspectDto.Active = false
|
||||
return introspectDto, nil
|
||||
}
|
||||
return introspectDto, err
|
||||
}
|
||||
|
||||
@@ -820,41 +689,7 @@ func (s *OidcService) ResolveAllowedCallbackURL(ctx context.Context, clientID, i
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resolveConfiguredCallbackURL(&client, inputCallbackURL)
|
||||
}
|
||||
|
||||
// ResolveAuthorizeCallbackURL resolves the callback URL for a browser authorization
|
||||
// request without authorizing the user or consuming PAR state.
|
||||
func (s *OidcService) ResolveAuthorizeCallbackURL(ctx context.Context, clientID, inputCallbackURL, requestURI string) (string, error) {
|
||||
client, err := s.GetClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if client.RequiresPushedAuthorizationRequests && requestURI == "" {
|
||||
return "", &common.OidcPARRequiredError{}
|
||||
}
|
||||
|
||||
if requestURI != "" {
|
||||
par, err := s.GetPushedAuthorizationRequest(ctx, clientID, requestURI)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
inputCallbackURL = par.Parameters.RedirectURI
|
||||
}
|
||||
|
||||
return resolveConfiguredCallbackURL(&client, inputCallbackURL)
|
||||
}
|
||||
|
||||
func resolveConfiguredCallbackURL(client *model.OidcClient, inputCallbackURL string) (string, error) {
|
||||
if inputCallbackURL == "" {
|
||||
if len(client.CallbackURLs) > 0 {
|
||||
return client.CallbackURLs[0], nil
|
||||
}
|
||||
return "", &common.OidcMissingCallbackURLError{}
|
||||
}
|
||||
|
||||
if len(client.CallbackURLs) == 0 {
|
||||
if inputCallbackURL == "" || len(client.CallbackURLs) == 0 {
|
||||
return "", &common.OidcMissingCallbackURLError{}
|
||||
}
|
||||
|
||||
@@ -1011,8 +846,6 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
|
||||
// PKCE is required for public clients
|
||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||
client.RequiresReauthentication = input.RequiresReauthentication
|
||||
// PAR is not available for public clients, so ignore the flag if the client is public
|
||||
client.RequiresPushedAuthorizationRequests = !input.IsPublic && input.RequiresPushedAuthorizationRequests
|
||||
client.LaunchURL = input.LaunchURL
|
||||
client.IsGroupRestricted = input.IsGroupRestricted
|
||||
|
||||
@@ -1151,12 +984,7 @@ func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, fil
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
strippedReader, err := imageutil.StripMetadata(reader, fileType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.fileStorage.Save(ctx, imagePath, strippedReader)
|
||||
err = s.fileStorage.Save(ctx, imagePath, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1289,7 +1117,7 @@ func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, in
|
||||
}
|
||||
|
||||
// ValidateEndSession returns the logout callback URL for the client if all the validations pass
|
||||
func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogoutDto, userID string) (callbackURL string, err error) {
|
||||
func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogoutDto, userID string) (string, error) {
|
||||
// If no ID token hint is provided, return an error
|
||||
if input.IdTokenHint == "" {
|
||||
return "", &common.TokenInvalidError{}
|
||||
@@ -1311,22 +1139,9 @@ func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogo
|
||||
return "", &common.OidcClientIdNotMatchingError{}
|
||||
}
|
||||
|
||||
subject, ok := token.Subject()
|
||||
if !ok || subject != userID {
|
||||
return "", &common.TokenInvalidError{}
|
||||
}
|
||||
|
||||
idTokenJti, ok := token.JwtID()
|
||||
if !ok {
|
||||
return "", &common.TokenInvalidError{}
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
defer tx.Rollback()
|
||||
|
||||
// Check if the user has authorized the client before
|
||||
var userAuthorizedOIDCClient model.UserAuthorizedOidcClient
|
||||
err = tx.
|
||||
err = s.db.
|
||||
WithContext(ctx).
|
||||
Preload("Client").
|
||||
First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", clientID[0], userID).
|
||||
@@ -1335,40 +1150,26 @@ func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogo
|
||||
return "", &common.OidcMissingAuthorizationError{}
|
||||
}
|
||||
|
||||
// If the client has a callback URL, validate it
|
||||
if len(userAuthorizedOIDCClient.Client.LogoutCallbackURLs) > 0 {
|
||||
callbackURL, err = s.getLogoutCallbackURL(&userAuthorizedOIDCClient.Client, input.PostLogoutRedirectUri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// If the client has no logout callback URLs, return an error
|
||||
if len(userAuthorizedOIDCClient.Client.LogoutCallbackURLs) == 0 {
|
||||
return "", &common.OidcNoCallbackURLError{}
|
||||
}
|
||||
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ? AND client_id = ? AND id_token_jti = ?", userID, clientID[0], idTokenJti).
|
||||
Delete(&model.OidcRefreshToken{}).
|
||||
Error
|
||||
callbackURL, err := s.getLogoutCallbackURL(&userAuthorizedOIDCClient.Client, input.PostLogoutRedirectUri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return "", fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return callbackURL, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID string, userID string, scope string, authenticationMethod string, nonce string, codeChallenge string, codeChallengeMethod string, tx *gorm.DB) (string, error) {
|
||||
func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID string, userID string, scope string, nonce string, codeChallenge string, codeChallengeMethod string, tx *gorm.DB) (string, error) {
|
||||
randomString, err := utils.GenerateRandomAlphanumericString(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
codeChallengeMethodSha256, err := codeChallengeMethodIsSha256(codeChallengeMethod)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
codeChallengeMethodSha256 := strings.ToUpper(codeChallengeMethod) == "S256"
|
||||
|
||||
oidcAuthorizationCode := model.OidcAuthorizationCode{
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(15 * time.Minute)),
|
||||
@@ -1376,7 +1177,6 @@ func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID stri
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
AuthenticationMethod: authenticationMethod,
|
||||
Nonce: nonce,
|
||||
CodeChallenge: &codeChallenge,
|
||||
CodeChallengeMethodSha256: &codeChallengeMethodSha256,
|
||||
@@ -1393,129 +1193,6 @@ func (s *OidcService) createAuthorizationCode(ctx context.Context, clientID stri
|
||||
return randomString, nil
|
||||
}
|
||||
|
||||
func codeChallengeMethodIsSha256(codeChallengeMethod string) (bool, error) {
|
||||
switch strings.ToUpper(codeChallengeMethod) {
|
||||
case "":
|
||||
return false, nil
|
||||
case "PLAIN":
|
||||
return false, nil
|
||||
case "S256":
|
||||
return true, nil
|
||||
default:
|
||||
return false, common.NewOidcInvalidRequestError("code challenge method not supported")
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePushedAuthorizationRequest validates and stores authorization parameters for PAR (RFC 9126).
|
||||
// Only confidential clients (non-public) may use this endpoint.
|
||||
func (s *OidcService) CreatePushedAuthorizationRequest(ctx context.Context, creds ClientAuthCredentials, input dto.OidcPARRequestDto) (requestURI string, expiresIn int, err error) {
|
||||
// Public clients are not allowed, but we allow them in this step for better error messages
|
||||
client, err := s.verifyClientCredentialsInternal(ctx, s.db, creds, true)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// Reject public clients here
|
||||
if client.IsPublic {
|
||||
return "", 0, &common.OidcPARNotSupportedForPublicClientsError{}
|
||||
}
|
||||
|
||||
if input.ResponseType != "code" {
|
||||
return "", 0, common.NewOidcInvalidRequestError("unsupported response_type: only 'code' is supported")
|
||||
}
|
||||
|
||||
// Validate redirect_uri at push time (BCP requirement)
|
||||
if _, err = s.getCallbackURL(client, input.RedirectURI, s.db, ctx); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
randomSuffix, err := utils.GenerateRandomAlphanumericString(32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("failed to generate request URI: %w", err)
|
||||
}
|
||||
requestURI = parRequestURIPrefix + randomSuffix
|
||||
|
||||
par := model.OidcPushedAuthorizationRequest{
|
||||
RequestURI: requestURI,
|
||||
ClientID: client.ID,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(PARDuration)),
|
||||
Parameters: model.OidcAuthorizationRequestParameters{
|
||||
Scope: input.Scope,
|
||||
RedirectURI: input.RedirectURI,
|
||||
State: input.State,
|
||||
Nonce: input.Nonce,
|
||||
CodeChallenge: input.CodeChallenge,
|
||||
CodeChallengeMethod: input.CodeChallengeMethod,
|
||||
ResponseType: input.ResponseType,
|
||||
Prompt: input.Prompt,
|
||||
ResponseMode: input.ResponseMode,
|
||||
},
|
||||
}
|
||||
|
||||
if err = s.db.WithContext(ctx).Create(&par).Error; err != nil {
|
||||
return "", 0, fmt.Errorf("failed to store pushed authorization request: %w", err)
|
||||
}
|
||||
|
||||
return requestURI, int(PARDuration.Seconds()), nil
|
||||
}
|
||||
|
||||
// GetPushedAuthorizationRequest retrieves a PAR record without consuming it.
|
||||
func (s *OidcService) GetPushedAuthorizationRequest(ctx context.Context, clientID, requestURI string) (model.OidcPushedAuthorizationRequest, error) {
|
||||
return s.getPushedAuthorizationRequestInternal(ctx, s.db, clientID, requestURI)
|
||||
}
|
||||
|
||||
// getPushedAuthorizationRequestInternal retrieves a PAR record without consuming it.
|
||||
func (s *OidcService) getPushedAuthorizationRequestInternal(ctx context.Context, tx *gorm.DB, clientID, requestURI string) (model.OidcPushedAuthorizationRequest, error) {
|
||||
var par model.OidcPushedAuthorizationRequest
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Where(
|
||||
"request_uri = ? AND client_id = ? AND expires_at > ?",
|
||||
requestURI,
|
||||
clientID,
|
||||
datatype.DateTime(time.Now()),
|
||||
).
|
||||
First(&par).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return par, &common.OidcInvalidRequestURIError{}
|
||||
}
|
||||
return par, err
|
||||
}
|
||||
|
||||
return par, nil
|
||||
}
|
||||
|
||||
// getAndConsumePushedAuthorizationRequest atomically retrieves and deletes a PAR record.
|
||||
// Returns OidcInvalidRequestURIError if the record is not found, expired, or belongs to a different client.
|
||||
func (s *OidcService) getAndConsumePushedAuthorizationRequest(ctx context.Context, tx *gorm.DB, clientID, requestURI string) (model.OidcPushedAuthorizationRequest, error) {
|
||||
var par model.OidcPushedAuthorizationRequest
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Clauses(clause.Returning{}).
|
||||
Where(
|
||||
"request_uri = ? AND client_id = ? AND expires_at > ?",
|
||||
requestURI,
|
||||
clientID,
|
||||
datatype.DateTime(time.Now()),
|
||||
).
|
||||
Delete(&par).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return par, &common.OidcInvalidRequestURIError{}
|
||||
}
|
||||
return par, err
|
||||
}
|
||||
// After DELETE … RETURNING, check if a row was actually deleted
|
||||
if par.ID == "" {
|
||||
return par, &common.OidcInvalidRequestURIError{}
|
||||
}
|
||||
|
||||
return par, nil
|
||||
}
|
||||
|
||||
func validateCodeVerifier(codeVerifier, codeChallenge string, codeChallengeMethodSha256 bool) bool {
|
||||
if codeVerifier == "" || codeChallenge == "" {
|
||||
return false
|
||||
@@ -1643,7 +1320,7 @@ func (s *OidcService) CreateDeviceAuthorization(ctx context.Context, input dto.O
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, userID string, authenticationMethod string, ipAddress string, userAgent string) error {
|
||||
func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, userID string, ipAddress string, userAgent string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -1692,7 +1369,6 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
|
||||
}
|
||||
|
||||
deviceAuth.UserID = &userID
|
||||
deviceAuth.AuthenticationMethod = authenticationMethod
|
||||
deviceAuth.IsAuthorized = true
|
||||
|
||||
err = tx.
|
||||
@@ -1809,15 +1485,6 @@ func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string,
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Where("user_id = ? AND client_id = ?", userID, clientID).
|
||||
Delete(&model.OidcRefreshToken{}).
|
||||
Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1905,7 +1572,7 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
||||
return dtos, response, err
|
||||
}
|
||||
|
||||
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, authenticationMethod string, idTokenJti string, tx *gorm.DB) (string, error) {
|
||||
func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, userID string, scope string, tx *gorm.DB) (string, error) {
|
||||
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1916,13 +1583,11 @@ func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, u
|
||||
refreshTokenHash := utils.CreateSha256Hash(refreshToken)
|
||||
|
||||
m := model.OidcRefreshToken{
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(RefreshTokenDuration)),
|
||||
Token: refreshTokenHash,
|
||||
IdTokenJti: &idTokenJti,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
AuthenticationMethod: authenticationMethod,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(RefreshTokenDuration)),
|
||||
Token: refreshTokenHash,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
err = tx.
|
||||
@@ -2052,18 +1717,14 @@ func (s *OidcService) jwkSetForURL(ctx context.Context, url string) (set jwk.Set
|
||||
// We set a timeout because otherwise Register will keep trying in case of errors
|
||||
registerCtx, registerCancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer registerCancel()
|
||||
|
||||
registerOptions := []jwk.RegisterOption{
|
||||
jwk.WithMaxInterval(24 * time.Hour),
|
||||
jwk.WithMinInterval(15 * time.Minute),
|
||||
jwk.WithWaitReady(true),
|
||||
}
|
||||
if s.httpClient != nil {
|
||||
registerOptions = append(registerOptions, jwk.WithHTTPClient(s.httpClient))
|
||||
}
|
||||
|
||||
// We need to register the URL
|
||||
err = s.jwkCache.Register(registerCtx, url, registerOptions...)
|
||||
err = s.jwkCache.Register(
|
||||
registerCtx,
|
||||
url,
|
||||
jwk.WithMaxInterval(24*time.Hour),
|
||||
jwk.WithMinInterval(15*time.Minute),
|
||||
jwk.WithWaitReady(true),
|
||||
)
|
||||
// In case of race conditions (two goroutines calling jwkCache.Register at the same time), it's possible we can get a conflict anyways, so we ignore that error
|
||||
if err != nil && !errors.Is(err, httprc.ErrResourceAlreadyExists()) {
|
||||
return nil, fmt.Errorf("failed to register JWK set: %w", err)
|
||||
@@ -2142,7 +1803,7 @@ func (s *OidcService) verifyClientAssertionFromFederatedIdentities(ctx context.C
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string, authenticationMethod string) (*dto.OidcClientPreviewDto, error) {
|
||||
func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string) (*dto.OidcClientPreviewDto, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -2178,12 +1839,12 @@ func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, use
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idToken, _, err := s.jwtService.BuildIDToken(userClaims, clientID, "", authenticationMethod)
|
||||
idToken, err := s.jwtService.BuildIDToken(userClaims, clientID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.BuildOAuthAccessToken(user, clientID, authenticationMethod)
|
||||
accessToken, err := s.jwtService.BuildOAuthAccessToken(user, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2382,16 +2043,8 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, clie
|
||||
darkSuffix = "-dark"
|
||||
}
|
||||
|
||||
limitReader := utils.NewLimitReader(resp.Body, maxLogoSize+1)
|
||||
strippedReader, err := imageutil.StripMetadata(limitReader, ext)
|
||||
if errors.Is(err, utils.ErrSizeExceeded) {
|
||||
return errLogoTooLarge
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+ext)
|
||||
err = s.fileStorage.Save(ctx, imagePath, strippedReader)
|
||||
err = s.fileStorage.Save(ctx, imagePath, utils.NewLimitReader(resp.Body, maxLogoSize+1))
|
||||
if errors.Is(err, utils.ErrSizeExceeded) {
|
||||
return errLogoTooLarge
|
||||
} else if err != nil {
|
||||
@@ -2471,11 +2124,3 @@ func (s *OidcService) GetClientScimServiceProvider(ctx context.Context, clientID
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// parsePromptParameter parses the OIDC prompt parameter which is a space-delimited list of values
|
||||
func parsePromptParameter(prompt string) []string {
|
||||
if prompt == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Fields(prompt)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -21,12 +20,10 @@ import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
||||
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
||||
)
|
||||
@@ -565,179 +562,6 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOidcServiceRefreshTokenAuthorizationState(t *testing.T) {
|
||||
newFixture := func(t *testing.T, isGroupRestricted bool) (*OidcService, *gorm.DB, model.User, model.OidcClient, string, string, *model.UserGroup) {
|
||||
t.Helper()
|
||||
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
common.EnvConfig.EncryptionKey = []byte("0123456789abcdef0123456789abcdef")
|
||||
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
jwtService, err := NewJwtService(t.Context(), db, mockConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
service := &OidcService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
appConfigService: mockConfig,
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: "refresh-token-user",
|
||||
Email: new("refresh-token-user@example.com"),
|
||||
EmailVerified: true,
|
||||
FirstName: "Refresh",
|
||||
LastName: "User",
|
||||
}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
client, err := service.CreateClient(t.Context(), dto.OidcClientCreateDto{
|
||||
OidcClientUpdateDto: dto.OidcClientUpdateDto{
|
||||
Name: "Refresh Token Client",
|
||||
CallbackURLs: []string{"https://example.com/callback"},
|
||||
IsGroupRestricted: isGroupRestricted,
|
||||
},
|
||||
}, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
clientSecret, err := service.CreateClientSecret(t.Context(), client.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var userGroup *model.UserGroup
|
||||
if isGroupRestricted {
|
||||
group := model.UserGroup{
|
||||
FriendlyName: "Allowed Group",
|
||||
Name: "allowed-group",
|
||||
}
|
||||
require.NoError(t, db.Create(&group).Error)
|
||||
require.NoError(t, db.Model(&user).Association("UserGroups").Append(&group))
|
||||
require.NoError(t, db.Model(&client).Association("AllowedUserGroups").Append(&group))
|
||||
userGroup = &group
|
||||
}
|
||||
|
||||
scope := "openid profile email groups"
|
||||
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
||||
UserID: user.ID,
|
||||
ClientID: client.ID,
|
||||
Scope: scope,
|
||||
}).Error)
|
||||
|
||||
refreshToken, err := service.createRefreshToken(t.Context(), client.ID, user.ID, scope, AuthenticationMethodPhishingResistant, "03f94e54-53c4-42f8-afe5-918ffd97a30e", db)
|
||||
require.NoError(t, err)
|
||||
|
||||
return service, db, user, client, clientSecret, refreshToken, userGroup
|
||||
}
|
||||
|
||||
refreshInput := func(client model.OidcClient, clientSecret string, refreshToken string) dto.OidcCreateTokensDto {
|
||||
return dto.OidcCreateTokensDto{
|
||||
GrantType: GrantTypeRefreshToken,
|
||||
RefreshToken: refreshToken,
|
||||
ClientID: client.ID,
|
||||
ClientSecret: clientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("rejects refresh token after authorization revocation", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, _ := newFixture(t, false)
|
||||
|
||||
err := service.RevokeAuthorizedClient(t.Context(), user.ID, client.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var refreshTokenCount int64
|
||||
require.NoError(t, db.Model(&model.OidcRefreshToken{}).
|
||||
Where("user_id = ? AND client_id = ?", user.ID, client.ID).
|
||||
Count(&refreshTokenCount).Error)
|
||||
assert.Zero(t, refreshTokenCount)
|
||||
|
||||
_, err = service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcInvalidRefreshTokenError{})
|
||||
})
|
||||
|
||||
t.Run("rejects and deletes stale refresh token without authorization record", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, _ := newFixture(t, false)
|
||||
|
||||
require.NoError(t, db.
|
||||
Where("user_id = ? AND client_id = ?", user.ID, client.ID).
|
||||
Delete(&model.UserAuthorizedOidcClient{}).Error)
|
||||
|
||||
_, err := service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcInvalidRefreshTokenError{})
|
||||
|
||||
var refreshTokenCount int64
|
||||
require.NoError(t, db.Model(&model.OidcRefreshToken{}).
|
||||
Where("user_id = ? AND client_id = ?", user.ID, client.ID).
|
||||
Count(&refreshTokenCount).Error)
|
||||
assert.Zero(t, refreshTokenCount)
|
||||
})
|
||||
|
||||
t.Run("rejects refresh token for disabled user", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, _ := newFixture(t, false)
|
||||
|
||||
require.NoError(t, db.Model(&model.User{}).
|
||||
Where("id = ?", user.ID).
|
||||
Update("disabled", true).Error)
|
||||
|
||||
_, err := service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcInvalidRefreshTokenError{})
|
||||
})
|
||||
|
||||
t.Run("rejects refresh token after user leaves allowed group", func(t *testing.T) {
|
||||
service, db, user, client, clientSecret, refreshToken, userGroup := newFixture(t, true)
|
||||
require.NotNil(t, userGroup)
|
||||
|
||||
require.NoError(t, db.Model(&user).Association("UserGroups").Delete(userGroup))
|
||||
|
||||
_, err := service.createTokenFromRefreshToken(t.Context(), refreshInput(client, clientSecret, refreshToken))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, &common.OidcAccessDeniedError{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOidcServiceAuthenticationMethodsPersistence(t *testing.T) {
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
jwtService, db, _ := setupJwtService(t, mockConfig)
|
||||
service := &OidcService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
authenticationMethod := AuthenticationMethodPhishingResistant
|
||||
|
||||
t.Run("stores authentication method on authorization codes", func(t *testing.T) {
|
||||
code, err := service.createAuthorizationCode(
|
||||
t.Context(),
|
||||
"amr-client",
|
||||
"amr-user",
|
||||
"openid profile",
|
||||
authenticationMethod,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
db,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var authorizationCode model.OidcAuthorizationCode
|
||||
require.NoError(t, db.First(&authorizationCode, "code = ?", code).Error)
|
||||
assert.Equal(t, authenticationMethod, authorizationCode.AuthenticationMethod)
|
||||
})
|
||||
|
||||
t.Run("stores authentication methods on refresh tokens", func(t *testing.T) {
|
||||
_, err := service.createRefreshToken(t.Context(), "amr-client", "amr-user", "openid profile", authenticationMethod, "03f94e54-53c4-42f8-afe5-918ffd97a30e", db)
|
||||
require.NoError(t, err)
|
||||
|
||||
var refreshToken model.OidcRefreshToken
|
||||
require.NoError(t, db.First(&refreshToken, "client_id = ? AND user_id = ?", "amr-client", "amr-user").Error)
|
||||
assert.Equal(t, authenticationMethod, refreshToken.AuthenticationMethod)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateCodeVerifier_Plain(t *testing.T) {
|
||||
require.False(t, validateCodeVerifier("", "", false))
|
||||
require.False(t, validateCodeVerifier("", "", true))
|
||||
@@ -763,61 +587,6 @@ func TestValidateCodeVerifier_Plain(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCodeChallengeMethodIsSha256(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
wantSha256 bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "omitted defaults to plain",
|
||||
method: "",
|
||||
wantSha256: false,
|
||||
},
|
||||
{
|
||||
name: "plain",
|
||||
method: "plain",
|
||||
wantSha256: false,
|
||||
},
|
||||
{
|
||||
name: "plain case insensitive",
|
||||
method: "PLAIN",
|
||||
wantSha256: false,
|
||||
},
|
||||
{
|
||||
name: "s256",
|
||||
method: "S256",
|
||||
wantSha256: true,
|
||||
},
|
||||
{
|
||||
name: "s256 case insensitive",
|
||||
method: "s256",
|
||||
wantSha256: true,
|
||||
},
|
||||
{
|
||||
name: "unknown method",
|
||||
method: "S384",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := codeChallengeMethodIsSha256(tt.method)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
var invalidRequest *common.OidcInvalidRequestError
|
||||
require.ErrorAs(t, err, &invalidRequest)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantSha256, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOidcService_updateClientLogoType(t *testing.T) {
|
||||
// Create a test database
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
@@ -951,8 +720,6 @@ func TestOidcService_updateClientLogoType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
const publicLogoHost = "https://8.8.8.8"
|
||||
|
||||
// Create a test database
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
@@ -998,7 +765,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
// Create a mock HTTP client with responses
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/logo.png": pngResponse,
|
||||
"https://example.com/logo.png": pngResponse,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1014,7 +781,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
}
|
||||
|
||||
// Download and save the logo
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/logo.png", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/logo.png", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the file was saved
|
||||
@@ -1043,7 +810,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/dark-logo.webp": webpResponse,
|
||||
"https://example.com/dark-logo.webp": webpResponse,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1058,7 +825,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
}
|
||||
|
||||
// Download and save the dark logo
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/dark-logo.webp", false)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/dark-logo.webp", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the dark logo file was saved
|
||||
@@ -1082,7 +849,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/icon.svg": testutils.NewMockResponse(http.StatusOK, string(svgContent)),
|
||||
"https://example.com/icon.svg": testutils.NewMockResponse(http.StatusOK, string(svgContent)),
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1096,7 +863,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/icon.svg", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/icon.svg", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify SVG file was saved
|
||||
@@ -1113,7 +880,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/logo": jpgResponse,
|
||||
"https://example.com/logo": jpgResponse,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1127,7 +894,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/logo", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/logo", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify JPG file was saved (jpeg extension is normalized to jpg)
|
||||
@@ -1149,7 +916,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
t.Run("Returns error for non-200 status code", func(t *testing.T) {
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/not-found.png": testutils.NewMockResponse(http.StatusNotFound, "Not Found"),
|
||||
"https://example.com/not-found.png": testutils.NewMockResponse(http.StatusNotFound, "Not Found"),
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1163,7 +930,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/not-found.png", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/not-found.png", true)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to fetch logo")
|
||||
})
|
||||
@@ -1179,7 +946,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/large.png": largeResponse,
|
||||
"https://example.com/large.png": largeResponse,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1193,7 +960,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/large.png", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/large.png", true)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errLogoTooLarge)
|
||||
})
|
||||
@@ -1205,7 +972,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/file.txt": textResponse,
|
||||
"https://example.com/file.txt": textResponse,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1219,7 +986,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, publicLogoHost+"/file.txt", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), client.ID, "https://example.com/file.txt", true)
|
||||
require.Error(t, err)
|
||||
var fileTypeErr *common.FileTypeNotSupportedError
|
||||
require.ErrorAs(t, err, &fileTypeErr)
|
||||
@@ -1232,7 +999,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
publicLogoHost + "/logo.png": pngResponse,
|
||||
"https://example.com/logo.png": pngResponse,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
@@ -1246,170 +1013,8 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), "non-existent-client-id", publicLogoHost+"/logo.png", true)
|
||||
err := s.downloadAndSaveLogoFromURL(t.Context(), "non-existent-client-id", "https://example.com/logo.png", true)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to look up client")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOidcService_ValidateEndSessionDeletesMatchingRefreshToken(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
common.EnvConfig.EncryptionKey = []byte("0123456789abcdef0123456789abcdef")
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
mockJwtService, err := NewJwtService(t.Context(), db, mockConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
oidcService := &OidcService{
|
||||
db: db,
|
||||
jwtService: mockJwtService,
|
||||
}
|
||||
|
||||
userID := "test-user-123"
|
||||
clientID := "test-client-456"
|
||||
otherClientID := "other-client-789"
|
||||
otherIDTokenJti := "ac653f42-4781-49f2-bc7c-cc44503c3a1a" //nolint:gosec
|
||||
userEmail := "test@example.com"
|
||||
|
||||
user := model.User{
|
||||
Base: model.Base{ID: userID},
|
||||
Email: &userEmail,
|
||||
}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
client := model.OidcClient{
|
||||
Base: model.Base{ID: clientID},
|
||||
Name: "Test Client",
|
||||
LogoutCallbackURLs: []string{"https://example.com/logout"},
|
||||
}
|
||||
require.NoError(t, db.Create(&client).Error)
|
||||
|
||||
otherClient := model.OidcClient{
|
||||
Base: model.Base{ID: otherClientID},
|
||||
Name: "Other Client",
|
||||
LogoutCallbackURLs: []string{"https://other.example.com/logout"},
|
||||
}
|
||||
require.NoError(t, db.Create(&otherClient).Error)
|
||||
|
||||
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
}).Error)
|
||||
|
||||
userClaims := map[string]any{
|
||||
"sub": userID,
|
||||
"name": "Test User",
|
||||
"email": userEmail,
|
||||
}
|
||||
idToken, idTokenJti, err := mockJwtService.GenerateIDToken(userClaims, clientID, "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
refreshTokens := []model.OidcRefreshToken{
|
||||
{
|
||||
Token: "matching-refresh-token",
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
IdTokenJti: &idTokenJti,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(time.Hour)),
|
||||
Scope: "openid profile",
|
||||
},
|
||||
{
|
||||
Token: "same-client-different-session",
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
IdTokenJti: &otherIDTokenJti,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(time.Hour)),
|
||||
Scope: "openid profile",
|
||||
},
|
||||
{
|
||||
Token: "other-client-same-jti",
|
||||
UserID: userID,
|
||||
ClientID: otherClientID,
|
||||
IdTokenJti: &idTokenJti,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(time.Hour)),
|
||||
Scope: "openid profile",
|
||||
},
|
||||
{
|
||||
Token: "legacy-refresh-token",
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(time.Hour)),
|
||||
Scope: "openid profile",
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&refreshTokens).Error)
|
||||
|
||||
callbackURL, err := oidcService.ValidateEndSession(t.Context(), dto.OidcLogoutDto{
|
||||
IdTokenHint: idToken,
|
||||
ClientId: clientID,
|
||||
PostLogoutRedirectUri: "https://example.com/logout",
|
||||
}, userID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://example.com/logout", callbackURL)
|
||||
|
||||
var remainingTokens []model.OidcRefreshToken
|
||||
require.NoError(t, db.Order("token").Find(&remainingTokens).Error)
|
||||
remainingTokenValues := make([]string, len(remainingTokens))
|
||||
for i, token := range remainingTokens {
|
||||
remainingTokenValues[i] = token.Token
|
||||
}
|
||||
assert.ElementsMatch(t, []string{
|
||||
"legacy-refresh-token",
|
||||
"other-client-same-jti",
|
||||
"same-client-different-session",
|
||||
}, remainingTokenValues)
|
||||
}
|
||||
|
||||
// Tests for prompt parameter parsing and handling
|
||||
func TestParsePromptParameter(t *testing.T) {
|
||||
t.Run("empty prompt returns empty slice", func(t *testing.T) {
|
||||
result := parsePromptParameter("")
|
||||
assert.Equal(t, []string{}, result)
|
||||
})
|
||||
|
||||
t.Run("single prompt value", func(t *testing.T) {
|
||||
result := parsePromptParameter("none")
|
||||
assert.Equal(t, []string{"none"}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple prompt values space-delimited", func(t *testing.T) {
|
||||
result := parsePromptParameter("login consent")
|
||||
assert.Equal(t, []string{"login", "consent"}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple prompt values with extra spaces", func(t *testing.T) {
|
||||
result := parsePromptParameter(" none login ")
|
||||
assert.Equal(t, []string{"none", "login"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPromptParameterConflicts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt string
|
||||
expectConflict bool
|
||||
}{
|
||||
{"none alone is valid", "none", false},
|
||||
{"login alone is valid", "login", false},
|
||||
{"consent alone is valid", "consent", false},
|
||||
{"login consent is valid", "login consent", false},
|
||||
{"none consent conflicts", "none consent", true},
|
||||
{"none login conflicts", "none login", true},
|
||||
{"none select_account conflicts", "none select_account", true},
|
||||
{"none consent login conflicts", "none consent login", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
values := parsePromptParameter(tt.prompt)
|
||||
hasNone := slices.Contains(values, "none")
|
||||
hasConsent := slices.Contains(values, "consent")
|
||||
hasLogin := slices.Contains(values, "login")
|
||||
hasSelectAccount := slices.Contains(values, "select_account")
|
||||
|
||||
conflict := hasNone && (hasConsent || hasLogin || hasSelectAccount)
|
||||
assert.Equal(t, tt.expectConflict, conflict)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (s *OneTimeAccessService) requestOneTimeAccessEmailInternal(ctx context.Con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// #nosec G118 - We use a background context here as this is running in a goroutine
|
||||
// We use a background context here as this is running in a goroutine
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
@@ -197,7 +197,7 @@ func (s *OneTimeAccessService) ExchangeOneTimeAccessToken(ctx context.Context, t
|
||||
return model.User{}, "", &common.DeviceCodeInvalid{}
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(oneTimeAccessToken.User, AuthenticationMethodOneTimePassword)
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(oneTimeAccessToken.User)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
@@ -136,8 +136,7 @@ func (s *UserService) GetProfilePicture(ctx context.Context, userID string) (io.
|
||||
|
||||
// Save the default picture for future use (in a goroutine to avoid blocking)
|
||||
defaultPictureBytes := defaultPicture.Bytes()
|
||||
//#nosec G118 - We use a background context as this is running in background
|
||||
// nolint:contextcheck
|
||||
//nolint:contextcheck
|
||||
go func() {
|
||||
// Use bytes.NewReader because we need an io.ReadSeeker
|
||||
rErr := s.fileStorage.Save(context.Background(), defaultPicturePath, bytes.NewReader(defaultPictureBytes))
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *UserSignUpService) SignUp(ctx context.Context, signupData dto.SignUpDto
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(user, "")
|
||||
accessToken, err := s.jwtService.GenerateAccessToken(user)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -124,12 +124,14 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
setupCompleted, err := s.isInitialAdminSetupCompleted(ctx, tx)
|
||||
if err != nil {
|
||||
var userCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.User{}).
|
||||
Where("id != ?", staticApiKeyUserID).
|
||||
Count(&userCount).Error; err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
if setupCompleted {
|
||||
return model.User{}, "", &common.SetupNotAvailableError{}
|
||||
if userCount != 0 {
|
||||
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
||||
}
|
||||
|
||||
userToCreate := dto.UserCreateDto{
|
||||
@@ -146,7 +148,7 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
token, err := s.jwtService.GenerateAccessToken(user, AuthenticationMethodOneTimePassword)
|
||||
token, err := s.jwtService.GenerateAccessToken(user)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -159,21 +161,6 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *UserSignUpService) IsInitialAdminSetupCompleted(ctx context.Context) (bool, error) {
|
||||
return s.isInitialAdminSetupCompleted(ctx, s.db)
|
||||
}
|
||||
|
||||
func (s *UserSignUpService) isInitialAdminSetupCompleted(ctx context.Context, db *gorm.DB) (bool, error) {
|
||||
var userCount int64
|
||||
if err := db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("id != ?", staticApiKeyUserID).
|
||||
Count(&userCount).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return userCount != 0, nil
|
||||
}
|
||||
|
||||
func (s *UserSignUpService) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.SignupToken, utils.PaginationResponse, error) {
|
||||
var tokens []model.SignupToken
|
||||
query := s.db.WithContext(ctx).Preload("UserGroups").Model(&model.SignupToken{})
|
||||
|
||||
@@ -69,7 +69,8 @@ func (s *VersionService) GetLatestVersion(ctx context.Context) (string, error) {
|
||||
return strings.TrimPrefix(payload.TagName, "v"), nil
|
||||
})
|
||||
|
||||
if staleErr, ok := errors.AsType[*utils.ErrStale](err); ok {
|
||||
var staleErr *utils.ErrStale
|
||||
if errors.As(err, &staleErr) {
|
||||
slog.Warn("Failed to fetch latest version, returning stale cache", "error", staleErr.Err)
|
||||
return version, nil
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
|
||||
return model.User{}, "", &common.UserDisabledError{}
|
||||
}
|
||||
|
||||
token, err := s.jwtService.GenerateAccessToken(*user, AuthenticationMethodPhishingResistant)
|
||||
token, err := s.jwtService.GenerateAccessToken(*user)
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -293,40 +293,26 @@ func (s *WebAuthnService) ListCredentials(ctx context.Context, userID string) ([
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID string, credentialID string, ipAddress string, userAgent string, actorUserID string) error {
|
||||
func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID string, credentialID string, ipAddress string, userAgent string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
credential := &model.WebauthnCredential{}
|
||||
result := tx.
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(credential, "id = ? AND user_id = ?", credentialID, userID)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to delete record: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
Delete(credential, "id = ? AND user_id = ?", credentialID, userID).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete record: %w", err)
|
||||
}
|
||||
|
||||
auditLogData := model.AuditLogData{"credentialID": hex.EncodeToString(credential.CredentialID), "passkeyName": credential.Name}
|
||||
if actorUserID != "" && actorUserID != userID {
|
||||
var actor model.User
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
First(&actor, "id = ?", actorUserID).
|
||||
Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load actor user: %w", err)
|
||||
}
|
||||
auditLogData["actorUserID"] = actorUserID
|
||||
auditLogData["actorUsername"] = actor.Username
|
||||
}
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventPasskeyRemoved, ipAddress, userAgent, userID, auditLogData, tx)
|
||||
|
||||
err := tx.Commit().Error
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
@@ -389,14 +375,6 @@ func (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(ctx context
|
||||
return "", errors.New("access token does not contain user ID")
|
||||
}
|
||||
|
||||
authenticationMethod, err := GetAuthenticationMethod(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if authenticationMethod != AuthenticationMethodPhishingResistant {
|
||||
return "", &common.ReauthenticationRequiredError{}
|
||||
}
|
||||
|
||||
// Check if token is issued less than a minute ago
|
||||
tokenExpiration, ok := token.IssuedAt()
|
||||
if !ok || time.Since(tokenExpiration) > time.Minute {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestCreateReauthenticationTokenWithAccessToken(t *testing.T) {
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
SessionDuration: model.AppConfigVariable{Value: "60"},
|
||||
})
|
||||
|
||||
setupService := func(t *testing.T) (*WebAuthnService, model.User) {
|
||||
t.Helper()
|
||||
|
||||
jwtService, db, _ := setupJwtService(t, mockConfig)
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "reauth-user"},
|
||||
Username: "reauth-user",
|
||||
}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
return &WebAuthnService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
}, user
|
||||
}
|
||||
|
||||
t.Run("accepts a fresh access token from WebAuthn login", func(t *testing.T) {
|
||||
service, user := setupService(t)
|
||||
accessToken, err := service.jwtService.GenerateAccessToken(user, AuthenticationMethodPhishingResistant)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, reauthenticationToken)
|
||||
})
|
||||
|
||||
t.Run("rejects a fresh access token from one-time access login", func(t *testing.T) {
|
||||
service, user := setupService(t)
|
||||
accessToken, err := service.jwtService.GenerateAccessToken(user, AuthenticationMethodOneTimePassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
assert.Empty(t, reauthenticationToken)
|
||||
require.Error(t, err)
|
||||
assert.ErrorAs(t, err, new(*common.ReauthenticationRequiredError))
|
||||
})
|
||||
|
||||
t.Run("rejects a fresh access token without an authentication method", func(t *testing.T) {
|
||||
service, user := setupService(t)
|
||||
accessToken, err := service.jwtService.GenerateAccessToken(user, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
assert.Empty(t, reauthenticationToken)
|
||||
require.Error(t, err)
|
||||
assert.ErrorAs(t, err, new(*common.ReauthenticationRequiredError))
|
||||
})
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error)
|
||||
continue
|
||||
}
|
||||
objects = append(objects, ObjectInfo{
|
||||
Path: s.pathFromKey(aws.ToString(obj.Key)),
|
||||
Path: aws.ToString(obj.Key),
|
||||
Size: aws.ToInt64(obj.Size),
|
||||
ModTime: aws.ToTime(obj.LastModified),
|
||||
})
|
||||
@@ -147,13 +147,6 @@ func (s *s3Storage) List(ctx context.Context, path string) ([]ObjectInfo, error)
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func (s *s3Storage) pathFromKey(key string) string {
|
||||
if s.prefix == "" {
|
||||
return key
|
||||
}
|
||||
return strings.TrimPrefix(key, s.prefix+"/")
|
||||
}
|
||||
|
||||
func (s *s3Storage) Walk(ctx context.Context, root string, fn func(ObjectInfo) error) error {
|
||||
objects, err := s.List(ctx, root)
|
||||
if err != nil {
|
||||
@@ -186,7 +179,8 @@ func (s *s3Storage) buildObjectKey(p string) string {
|
||||
}
|
||||
|
||||
func isS3NotFound(err error) bool {
|
||||
if apiErr, ok := errors.AsType[smithy.APIError](err); ok {
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
if apiErr.ErrorCode() == "NotFound" || apiErr.ErrorCode() == "NoSuchKey" {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -35,50 +35,6 @@ func TestS3Helpers(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pathFromKey strips prefix to honor relative-path contract", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{name: "no prefix returns key unchanged", prefix: "", key: "images/logo.png", expected: "images/logo.png"},
|
||||
{name: "no prefix empty key", prefix: "", key: "", expected: ""},
|
||||
{name: "prefix matches and is stripped", prefix: "data/uploads", key: "data/uploads/application-images/logo.svg", expected: "application-images/logo.svg"},
|
||||
{name: "single-segment prefix stripped", prefix: "root", key: "root/foo/bar.txt", expected: "foo/bar.txt"},
|
||||
{name: "prefix equal to key without trailing slash is unchanged", prefix: "root", key: "root", expected: "root"},
|
||||
{name: "key without expected prefix returned unchanged", prefix: "data/uploads", key: "other/path.txt", expected: "other/path.txt"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := &s3Storage{
|
||||
bucket: "bucket",
|
||||
prefix: tc.prefix,
|
||||
}
|
||||
assert.Equal(t, tc.expected, s.pathFromKey(tc.key))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pathFromKey is the inverse of buildObjectKey for clean paths", func(t *testing.T) {
|
||||
paths := []string{
|
||||
"images/logo.png",
|
||||
"application-images/logo.svg",
|
||||
"oidc-client-images/abc.png",
|
||||
"deeply/nested/file.bin",
|
||||
}
|
||||
prefixes := []string{"", "root", "data/uploads"}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
for _, p := range paths {
|
||||
s := &s3Storage{bucket: "bucket", prefix: prefix}
|
||||
assert.Equal(t, p, s.pathFromKey(s.buildObjectKey(p)),
|
||||
"round-trip failed for prefix=%q path=%q", prefix, p)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("isS3NotFound detects expected errors", func(t *testing.T) {
|
||||
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NoSuchKey"}))
|
||||
assert.True(t, isS3NotFound(&smithy.GenericAPIError{Code: "NotFound"}))
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestValidateCallbackURLPattern(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "wildcard userinfo",
|
||||
pattern: "https://user:*@example.com/callback", // #nosec G101 - Test credential
|
||||
pattern: "https://user:*@example.com/callback",
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -64,7 +64,6 @@ func TestBearerAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// #nosec G101 - Test credentials
|
||||
func TestOAuthClientBasicAuth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package profilepicture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/exifremove/pkg/exifremove"
|
||||
)
|
||||
|
||||
const maxRIFFSize = ^uint32(0)
|
||||
|
||||
// StripMetadata removes EXIF/XMP metadata from JPG, PNG and WEBP images
|
||||
// Returns a *bytes.Reader so that storage backends receive a seekable reader with a known content length,
|
||||
// which is required for correct checksum calculation on S3-compatible services
|
||||
func StripMetadata(file io.Reader, ext string) (*bytes.Reader, error) {
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(ext) {
|
||||
case "jpg", "jpeg":
|
||||
stripped, err := exifremove.Remove(data)
|
||||
if err == nil {
|
||||
return bytes.NewReader(stripped), nil
|
||||
}
|
||||
return bytes.NewReader(data), nil
|
||||
case "png":
|
||||
stripped, err := exifremove.Remove(data)
|
||||
if err == nil {
|
||||
return bytes.NewReader(stripped), nil
|
||||
}
|
||||
return bytes.NewReader(data), nil
|
||||
case "webp":
|
||||
return bytes.NewReader(stripWEBPMetadata(data)), nil
|
||||
default:
|
||||
return bytes.NewReader(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
func stripWEBPMetadata(data []byte) []byte {
|
||||
// Check if the file contains the RIFF...WEBP header
|
||||
if len(data) < 12 || string(data[:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
|
||||
return data
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
// Build the WEBP header
|
||||
out.WriteString("RIFF")
|
||||
out.Write([]byte{0, 0, 0, 0}) // Size will be filled at the end
|
||||
out.WriteString("WEBP")
|
||||
|
||||
for pos := 12; pos < len(data); {
|
||||
// Each RIFF chunk needs an 8 byte header
|
||||
if pos+8 > len(data) {
|
||||
return data
|
||||
}
|
||||
|
||||
// Read the chunk type and payload size from the header
|
||||
chunkType := string(data[pos : pos+4])
|
||||
chunkSize := int(binary.LittleEndian.Uint32(data[pos+4 : pos+8]))
|
||||
chunkEnd := pos + 8 + chunkSize
|
||||
// Chunks with odd payload sizes include one additional byte at the end
|
||||
if chunkSize%2 == 1 {
|
||||
chunkEnd++
|
||||
}
|
||||
|
||||
// End of chunk can't be more than the actual image data length
|
||||
if chunkEnd > len(data) {
|
||||
return data
|
||||
}
|
||||
|
||||
// Remove chunks with the EXIF or XMP data type
|
||||
if chunkType == "EXIF" || chunkType == "XMP " {
|
||||
pos = chunkEnd
|
||||
continue
|
||||
}
|
||||
|
||||
// In the VP8X chunk there is a feature flag if the file contains any EXIF or XMP data
|
||||
if chunkType == "VP8X" && chunkSize >= 10 {
|
||||
// Copy the chunk because we don't want to modify the original one
|
||||
chunk := make([]byte, chunkEnd-pos)
|
||||
copy(chunk, data[pos:chunkEnd])
|
||||
|
||||
// Clear the Exif and XMP feature flags
|
||||
chunk[8] &^= 0x0c
|
||||
out.Write(chunk)
|
||||
} else {
|
||||
out.Write(data[pos:chunkEnd])
|
||||
}
|
||||
|
||||
pos = chunkEnd
|
||||
}
|
||||
|
||||
// WEBP image can max be 4GB in size
|
||||
riffSize := out.Len() - 8
|
||||
if riffSize < 0 || uint64(riffSize) > uint64(maxRIFFSize) {
|
||||
return data
|
||||
}
|
||||
|
||||
// Set the size in the header (byte 4-7)
|
||||
binary.LittleEndian.PutUint32(out.Bytes()[4:8], uint32(riffSize))
|
||||
return out.Bytes()
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package profilepicture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStripMetadata(t *testing.T) {
|
||||
t.Run("removes WEBP EXIF and XMP chunks", func(t *testing.T) {
|
||||
input := webpFile(
|
||||
webpChunk("VP8X", []byte{0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
|
||||
webpChunk("VP8 ", []byte{1, 2, 3, 4}),
|
||||
webpChunk("EXIF", []byte("gps")),
|
||||
webpChunk("XMP ", []byte("xmp")),
|
||||
webpChunk("ICCP", []byte("profile")),
|
||||
)
|
||||
|
||||
output := stripReader(t, input, "webp")
|
||||
|
||||
assert.Contains(t, string(output), "VP8 ")
|
||||
assert.NotContains(t, string(output), "gps")
|
||||
assert.NotContains(t, string(output), "xmp")
|
||||
assert.Contains(t, string(output), "profile")
|
||||
assert.Equal(t, byte(0), output[20]&0x0c)
|
||||
})
|
||||
|
||||
t.Run("leaves non photo metadata formats unchanged", func(t *testing.T) {
|
||||
input := []byte(`<svg><metadata>secret</metadata><path d="M0 0"/></svg>`)
|
||||
|
||||
output := stripReader(t, input, "svg")
|
||||
|
||||
assert.Equal(t, input, output)
|
||||
})
|
||||
|
||||
t.Run("leaves malformed photo data unchanged", func(t *testing.T) {
|
||||
input := []byte("fake-png-content")
|
||||
|
||||
output := stripReader(t, input, "png")
|
||||
|
||||
assert.Equal(t, input, output)
|
||||
})
|
||||
}
|
||||
|
||||
func stripReader(t *testing.T, input []byte, ext string) []byte {
|
||||
t.Helper()
|
||||
|
||||
reader, err := StripMetadata(bytes.NewReader(input), ext)
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
return output
|
||||
}
|
||||
|
||||
func webpFile(chunks ...[]byte) []byte {
|
||||
var out bytes.Buffer
|
||||
out.WriteString("RIFF")
|
||||
out.Write([]byte{0, 0, 0, 0})
|
||||
out.WriteString("WEBP")
|
||||
for _, chunk := range chunks {
|
||||
out.Write(chunk)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(out.Bytes()[4:8], uint32(out.Len()-8)) //nolint:gosec
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
func webpChunk(chunkType string, data []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
out.WriteString(chunkType)
|
||||
_ = binary.Write(&out, binary.LittleEndian, uint32(len(data))) //nolint:gosec
|
||||
out.Write(data)
|
||||
if len(data)%2 == 1 {
|
||||
out.WriteByte(0)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) {
|
||||
img := imaging.New(profilePictureSize, profilePictureSize, color.RGBA{R: 255, G: 255, B: 255, A: 255})
|
||||
|
||||
// Load the font
|
||||
fontBytes, err := resources.FS.ReadFile("fonts/Gloock.ttf")
|
||||
fontBytes, err := resources.FS.ReadFile("fonts/PlayfairDisplay-Bold.ttf")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read font file: %w", err)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) {
|
||||
|
||||
// Center the initials
|
||||
x := (profilePictureSize - font.MeasureString(face, initials).Ceil()) / 2
|
||||
y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil()
|
||||
y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil() - 10
|
||||
drawer.Dot = fixed.P(x, y)
|
||||
|
||||
// Draw the initials
|
||||
|
||||
@@ -86,10 +86,12 @@ func (f *KeyProviderDatabase) SaveKey(ctx context.Context, key jwk.Key) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt key: %w", err)
|
||||
}
|
||||
encB64 := base64.StdEncoding.EncodeToString(enc)
|
||||
|
||||
// Save to database
|
||||
row := model.KV{
|
||||
Key: PrivateKeyDBKey,
|
||||
Value: new(base64.StdEncoding.EncodeToString(enc)),
|
||||
Value: &encB64,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
|
||||
@@ -106,9 +106,10 @@ func TestKeyProviderDatabase_LoadKey(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert invalid base64 data
|
||||
invalidBase64 := "not-valid-base64"
|
||||
err = db.Create(&model.KV{
|
||||
Key: PrivateKeyDBKey,
|
||||
Value: new("not-valid-base64"),
|
||||
Value: &invalidBase64,
|
||||
}).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -131,9 +132,10 @@ func TestKeyProviderDatabase_LoadKey(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert valid base64 but invalid encrypted data
|
||||
invalidData := base64.StdEncoding.EncodeToString([]byte("not-valid-encrypted-data"))
|
||||
err = db.Create(&model.KV{
|
||||
Key: PrivateKeyDBKey,
|
||||
Value: new(base64.StdEncoding.EncodeToString([]byte("not-valid-encrypted-data"))),
|
||||
Value: &invalidData,
|
||||
}).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -193,10 +195,13 @@ func TestKeyProviderDatabase_LoadKey(t *testing.T) {
|
||||
encryptedData, err := cryptoutils.Encrypt(kek, invalidKeyData, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Base64 encode the encrypted data
|
||||
encodedData := base64.StdEncoding.EncodeToString(encryptedData)
|
||||
|
||||
// Save to database
|
||||
err = db.Create(&model.KV{
|
||||
Key: PrivateKeyDBKey,
|
||||
Value: new(base64.StdEncoding.EncodeToString(encryptedData)),
|
||||
Value: &encodedData,
|
||||
}).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
@@ -16,62 +14,25 @@ import (
|
||||
type MockRoundTripper struct {
|
||||
Err error
|
||||
Responses map[string]*http.Response
|
||||
|
||||
mu sync.Mutex
|
||||
responseBodies map[string][]byte
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface
|
||||
func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
|
||||
// Check if we have a specific response for this URL
|
||||
for url, resp := range m.Responses {
|
||||
if req.URL.String() == url {
|
||||
return m.cloneResponse(url, resp)
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
return NewMockResponse(http.StatusNotFound, ""), nil
|
||||
}
|
||||
|
||||
func (m *MockRoundTripper) cloneResponse(url string, resp *http.Response) (*http.Response, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.responseBodies == nil {
|
||||
m.responseBodies = make(map[string][]byte, len(m.Responses))
|
||||
}
|
||||
|
||||
body, ok := m.responseBodies[url]
|
||||
if !ok {
|
||||
var err error
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.responseBodies[url] = body
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
|
||||
cloned := new(http.Response)
|
||||
*cloned = *resp
|
||||
cloned.Header = resp.Header.Clone()
|
||||
cloned.Body = io.NopCloser(bytes.NewReader(body))
|
||||
cloned.ContentLength = int64(len(body))
|
||||
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
// NewMockResponse creates an http.Response with the given status code and body
|
||||
func NewMockResponse(statusCode int, body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
Header: make(http.Header),
|
||||
ContentLength: int64(len(body)),
|
||||
StatusCode: statusCode,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
backend/resources/fonts/PlayfairDisplay-Bold.ttf
Normal file
BIN
backend/resources/fonts/PlayfairDisplay-Bold.ttf
Normal file
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE oidc_authorization_codes DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_refresh_tokens DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_device_codes DROP COLUMN authentication_method;
|
||||
@@ -1,6 +0,0 @@
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_device_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
@@ -1,4 +0,0 @@
|
||||
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_id_token_jti;
|
||||
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
DROP COLUMN id_token_jti;
|
||||
@@ -1,5 +0,0 @@
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD COLUMN id_token_jti UUID;
|
||||
|
||||
CREATE INDEX idx_oidc_refresh_tokens_id_token_jti
|
||||
ON oidc_refresh_tokens(user_id, client_id, id_token_jti);
|
||||
@@ -1,3 +0,0 @@
|
||||
DROP TABLE IF EXISTS oidc_pushed_authorization_requests;
|
||||
|
||||
ALTER TABLE oidc_clients DROP COLUMN requires_pushed_authorization_requests;
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE oidc_pushed_authorization_requests (
|
||||
id UUID NOT NULL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
request_uri TEXT NOT NULL UNIQUE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE,
|
||||
parameters JSONB NOT NULL DEFAULT '{}',
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oidc_par_expires_at ON oidc_pushed_authorization_requests (expires_at);
|
||||
|
||||
ALTER TABLE oidc_clients ADD COLUMN requires_pushed_authorization_requests BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -1,10 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE oidc_authorization_codes DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_refresh_tokens DROP COLUMN authentication_method;
|
||||
ALTER TABLE oidc_device_codes DROP COLUMN authentication_method;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE oidc_authorization_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE oidc_device_codes
|
||||
ADD COLUMN authentication_method TEXT NOT NULL DEFAULT '';
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
@@ -1,10 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_id_token_jti;
|
||||
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
DROP COLUMN id_token_jti;
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
@@ -1,11 +0,0 @@
|
||||
PRAGMA foreign_keys= OFF;
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE oidc_refresh_tokens
|
||||
ADD COLUMN id_token_jti TEXT;
|
||||
|
||||
CREATE INDEX idx_oidc_refresh_tokens_id_token_jti
|
||||
ON oidc_refresh_tokens(user_id, client_id, id_token_jti);
|
||||
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys= ON;
|
||||
@@ -1,3 +0,0 @@
|
||||
DROP TABLE IF EXISTS oidc_pushed_authorization_requests;
|
||||
|
||||
ALTER TABLE oidc_clients DROP COLUMN requires_pushed_authorization_requests;
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE oidc_pushed_authorization_requests (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at INTEGER NOT NULL,
|
||||
request_uri TEXT NOT NULL UNIQUE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE,
|
||||
parameters TEXT NOT NULL DEFAULT '{}',
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oidc_par_expires_at ON oidc_pushed_authorization_requests (expires_at);
|
||||
|
||||
ALTER TABLE oidc_clients ADD COLUMN requires_pushed_authorization_requests BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -1 +0,0 @@
|
||||
{ "id": "c36t29j6bz" }
|
||||
@@ -1,5 +1,3 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# This file uses multi-stage builds to build the application from source, including the front-end
|
||||
|
||||
# Tags passed to "go build"
|
||||
@@ -11,33 +9,27 @@ RUN corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY frontend/package.json ./frontend/
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
RUN pnpm --filter pocket-id-frontend install --frozen-lockfile
|
||||
|
||||
COPY ./frontend ./frontend/
|
||||
|
||||
RUN --mount=type=cache,target=/build/frontend/node_modules/.vite \
|
||||
--mount=type=cache,target=/build/frontend/.svelte-kit \
|
||||
BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
|
||||
RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
|
||||
|
||||
# Stage 2: Build Backend
|
||||
FROM golang:1.26.4-alpine AS backend-builder
|
||||
FROM golang:1.26-alpine AS backend-builder
|
||||
ARG BUILD_TAGS
|
||||
WORKDIR /build
|
||||
COPY ./backend/go.mod ./backend/go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
RUN go mod download
|
||||
|
||||
COPY ./backend ./
|
||||
COPY --from=frontend-builder /build/frontend/dist ./frontend/dist
|
||||
COPY .version .version
|
||||
|
||||
WORKDIR /build/cmd
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
VERSION=$(cat /build/.version) && \
|
||||
RUN VERSION=$(cat /build/.version) \
|
||||
CGO_ENABLED=0 \
|
||||
GOOS=linux \
|
||||
go build \
|
||||
@@ -48,7 +40,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
.
|
||||
|
||||
# Stage 3: Production Image
|
||||
FROM alpine:3.24.1
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl su-exec
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This Dockerfile embeds a pre-built binary for the given Linux architecture
|
||||
# Binaries must be built using "./scripts/development/build-binaries.sh --docker-only"
|
||||
|
||||
FROM alpine:3.24.1
|
||||
FROM alpine
|
||||
|
||||
# TARGETARCH can be "amd64" or "arm64"
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -9,17 +9,17 @@
|
||||
"export": "email export"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "1.0.12",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"tailwind-merge": "^3.6.0"
|
||||
"@react-email/components": "1.0.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-email/preview-server": "5.2.10",
|
||||
"@types/node": "^25.9.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@react-email/preview-server": "5.0.7",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-email": "6.5.0",
|
||||
"tsx": "^4.22.2"
|
||||
"react-email": "5.0.7",
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user