mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-08 14:57:58 +00:00
Compare commits
19 Commits
v5.0.5-rc.
...
feat/prese
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd5d0d0359 | ||
|
|
646e24d98c | ||
|
|
0d286d1864 | ||
|
|
165197e472 | ||
|
|
431cd33053 | ||
|
|
3373dcc412 | ||
|
|
9d666dc108 | ||
|
|
7ad13935a5 | ||
|
|
98e788b1e8 | ||
|
|
a074efb3a3 | ||
|
|
48ef8c0e4c | ||
|
|
1313e8767a | ||
|
|
892097dc4d | ||
|
|
6542e1b121 | ||
|
|
e1d7fa2eb3 | ||
|
|
41244080c0 | ||
|
|
34f9724866 | ||
|
|
19a317dd7c | ||
|
|
8a9ffcf185 |
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -24,3 +24,8 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -10,13 +10,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
|
||||
@@ -56,6 +56,6 @@ jobs:
|
||||
run: go test -coverprofile=coverage.txt -v ./...
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
82
.github/workflows/nightly.yml
vendored
82
.github/workflows/nightly.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Delete old release
|
||||
run: gh release delete --cleanup-tag --yes nightly || echo release not found
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tinyauth-amd64
|
||||
path: tinyauth-amd64
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tinyauth-arm64
|
||||
path: tinyauth-arm64
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -176,22 +176,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-linux-amd64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -228,7 +228,7 @@ jobs:
|
||||
- image-build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -243,22 +243,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-distroless-linux-amd64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -310,22 +310,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-linux-arm64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -362,7 +362,7 @@ jobs:
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
@@ -377,22 +377,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -415,7 +415,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-distroless-linux-arm64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -429,25 +429,25 @@ jobs:
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -468,25 +468,25 @@ jobs:
|
||||
- image-build-arm-distroless
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-distroless-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -506,7 +506,7 @@ jobs:
|
||||
- binary-build
|
||||
- binary-build-arm
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: tinyauth-*
|
||||
path: binaries
|
||||
|
||||
80
.github/workflows/release.yml
vendored
80
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Generate metadata
|
||||
id: metadata
|
||||
@@ -29,13 +29,13 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tinyauth-amd64
|
||||
path: tinyauth-amd64
|
||||
@@ -81,13 +81,13 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.26.0"
|
||||
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tinyauth-arm64
|
||||
path: tinyauth-arm64
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
@@ -146,22 +146,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-linux-amd64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
- image-build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
@@ -210,22 +210,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-distroless-linux-amd64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
- generate-metadata
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
@@ -274,22 +274,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-linux-arm64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
@@ -338,22 +338,22 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -376,7 +376,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-distroless-linux-arm64
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -390,25 +390,25 @@ jobs:
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -431,25 +431,25 @@ jobs:
|
||||
- image-build-arm-distroless
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-distroless-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||
flavor: |
|
||||
@@ -473,7 +473,7 @@ jobs:
|
||||
- binary-build
|
||||
- binary-build-arm
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: tinyauth-*
|
||||
path: binaries
|
||||
|
||||
2
.github/workflows/sponsors.yml
vendored
2
.github/workflows/sponsors.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Generate Sponsors
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 30
|
||||
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
|
||||
|
||||
@@ -58,7 +58,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
A big thank you to the following people for providing me with more coffee:
|
||||
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <!-- sponsors -->
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <!-- sponsors -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ export type OIDCValues = {
|
||||
redirect_uri: string;
|
||||
state: string;
|
||||
nonce: string;
|
||||
code_challenge: string;
|
||||
code_challenge_method: string;
|
||||
};
|
||||
|
||||
interface IuseOIDCParams {
|
||||
@@ -14,7 +16,12 @@ interface IuseOIDCParams {
|
||||
missingParams: string[];
|
||||
}
|
||||
|
||||
const optionalParams: string[] = ["state", "nonce"];
|
||||
const optionalParams: string[] = [
|
||||
"state",
|
||||
"nonce",
|
||||
"code_challenge",
|
||||
"code_challenge_method",
|
||||
];
|
||||
|
||||
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
|
||||
let compiled: string = "";
|
||||
@@ -28,6 +35,8 @@ export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
|
||||
redirect_uri: params.get("redirect_uri") ?? "",
|
||||
state: params.get("state") ?? "",
|
||||
nonce: params.get("nonce") ?? "",
|
||||
code_challenge: params.get("code_challenge") ?? "",
|
||||
code_challenge_method: params.get("code_challenge_method") ?? "",
|
||||
};
|
||||
|
||||
for (const key of Object.keys(values)) {
|
||||
|
||||
@@ -76,10 +76,14 @@ export const LoginPage = () => {
|
||||
isPending: oauthIsPending,
|
||||
variables: oauthVariables,
|
||||
} = useMutation({
|
||||
mutationFn: (provider: string) =>
|
||||
axios.get(
|
||||
`/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||
),
|
||||
mutationFn: (provider: string) => {
|
||||
const params = isOidc
|
||||
? `?${compiledOIDCParams}`
|
||||
: props.redirect_uri
|
||||
? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}`
|
||||
: "";
|
||||
return axios.get(`/api/oauth/url/${provider}${params}`);
|
||||
},
|
||||
mutationKey: ["oauth"],
|
||||
onSuccess: (data) => {
|
||||
toast.info(t("loginOauthSuccessTitle"), {
|
||||
|
||||
1
internal/assets/migrations/000007_oidc_pkce.down.sql
Normal file
1
internal/assets/migrations/000007_oidc_pkce.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "oidc_codes" DROP COLUMN "code_challenge";
|
||||
1
internal/assets/migrations/000007_oidc_pkce.up.sql
Normal file
1
internal/assets/migrations/000007_oidc_pkce.up.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "oidc_codes" ADD COLUMN "code_challenge" TEXT DEFAULT "";
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContextController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
controllerConfig := controller.ContextControllerConfig{
|
||||
Providers: []controller.Provider{
|
||||
{
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHealthController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tests := []struct {
|
||||
description string
|
||||
path string
|
||||
|
||||
@@ -62,7 +62,29 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionId, session, err := controller.auth.NewOAuthSession(req.Provider)
|
||||
var reqParams service.OAuthURLParams
|
||||
|
||||
err = c.BindQuery(&reqParams)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Error().Err(err).Msg("Failed to bind query parameters")
|
||||
c.JSON(400, gin.H{
|
||||
"status": 400,
|
||||
"message": "Bad Request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.isOidcRequest(reqParams) {
|
||||
isRedirectSafe := utils.IsRedirectSafe(reqParams.RedirectURI, controller.config.CookieDomain)
|
||||
|
||||
if !isRedirectSafe {
|
||||
tlog.App.Warn().Str("redirect_uri", reqParams.RedirectURI).Msg("Unsafe redirect URI detected, ignoring")
|
||||
reqParams.RedirectURI = ""
|
||||
}
|
||||
}
|
||||
|
||||
sessionId, _, err := controller.auth.NewOAuthSession(req.Provider, reqParams)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Error().Err(err).Msg("Failed to create OAuth session")
|
||||
@@ -85,20 +107,6 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
c.SetCookie(controller.config.CSRFCookieName, session.State, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
|
||||
|
||||
if !isRedirectSafe {
|
||||
tlog.App.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
|
||||
redirectURI = ""
|
||||
}
|
||||
|
||||
if redirectURI != "" && isRedirectSafe {
|
||||
tlog.App.Debug().Msg("Setting redirect URI cookie")
|
||||
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
@@ -129,19 +137,23 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
defer controller.auth.EndOAuthSession(sessionIdCookie)
|
||||
|
||||
state := c.Query("state")
|
||||
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
||||
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
||||
|
||||
if err != nil || state != csrfCookie {
|
||||
tlog.App.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
|
||||
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
if err != nil {
|
||||
tlog.App.Error().Err(err).Msg("Failed to get OAuth pending session")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
defer controller.auth.EndOAuthSession(sessionIdCookie)
|
||||
|
||||
state := c.Query("state")
|
||||
if state != oauthPendingSession.State {
|
||||
tlog.App.Warn().Err(err).Msg("CSRF token mismatch")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
code := c.Query("code")
|
||||
_, err = controller.auth.GetOAuthToken(sessionIdCookie, code)
|
||||
@@ -198,7 +210,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
username = strings.Replace(user.Email, "@", "_", 1)
|
||||
}
|
||||
|
||||
service, err := controller.auth.GetOAuthService(sessionIdCookie)
|
||||
svc, err := controller.auth.GetOAuthService(sessionIdCookie)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
||||
@@ -206,8 +218,8 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if service.ID() != req.Provider {
|
||||
tlog.App.Error().Msgf("OAuth service ID mismatch: expected %s, got %s", service.ID(), req.Provider)
|
||||
if svc.ID() != req.Provider {
|
||||
tlog.App.Error().Msgf("OAuth service ID mismatch: expected %s, got %s", svc.ID(), req.Provider)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
@@ -216,9 +228,9 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
Username: username,
|
||||
Name: name,
|
||||
Email: user.Email,
|
||||
Provider: service.ID(),
|
||||
Provider: svc.ID(),
|
||||
OAuthGroups: utils.CoalesceToString(user.Groups),
|
||||
OAuthName: service.Name(),
|
||||
OAuthName: svc.Name(),
|
||||
OAuthSub: user.Sub,
|
||||
}
|
||||
|
||||
@@ -234,16 +246,21 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
||||
|
||||
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
||||
|
||||
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
||||
tlog.App.Debug().Msg("No redirect URI cookie found, redirecting to app root")
|
||||
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
||||
if controller.isOidcRequest(oauthPendingSession.CallbackParams) {
|
||||
tlog.App.Debug().Msg("OIDC request, redirecting to authorize page")
|
||||
queries, err := query.Values(oauthPendingSession.CallbackParams)
|
||||
if err != nil {
|
||||
tlog.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/authorize?%s", controller.config.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
if oauthPendingSession.CallbackParams.RedirectURI != "" {
|
||||
queries, err := query.Values(config.RedirectQuery{
|
||||
RedirectURI: redirectURI,
|
||||
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -252,6 +269,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode()))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
||||
}
|
||||
|
||||
func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams) bool {
|
||||
return params.Scope != "" &&
|
||||
params.ResponseType != "" &&
|
||||
params.ClientID != "" &&
|
||||
params.RedirectURI != ""
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
@@ -34,6 +35,7 @@ type TokenRequest struct {
|
||||
RefreshToken string `form:"refresh_token" url:"refresh_token"`
|
||||
ClientSecret string `form:"client_secret" url:"client_secret"`
|
||||
ClientID string `form:"client_id" url:"client_id"`
|
||||
CodeVerifier string `form:"code_verifier" url:"code_verifier"`
|
||||
}
|
||||
|
||||
type CallbackError struct {
|
||||
@@ -69,6 +71,7 @@ func (controller *OIDCController) SetupRoutes() {
|
||||
oidcGroup.POST("/authorize", controller.Authorize)
|
||||
oidcGroup.POST("/token", controller.Token)
|
||||
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||
oidcGroup.POST("/userinfo", controller.Userinfo)
|
||||
}
|
||||
|
||||
func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||
@@ -308,6 +311,16 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ok := controller.oidc.ValidatePKCE(entry.CodeChallenge, req.CodeVerifier)
|
||||
|
||||
if !ok {
|
||||
tlog.App.Warn().Msg("PKCE validation failed")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
|
||||
|
||||
if err != nil {
|
||||
@@ -364,14 +377,15 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var token string
|
||||
|
||||
authorization := c.GetHeader("Authorization")
|
||||
|
||||
tokenType, token, ok := strings.Cut(authorization, " ")
|
||||
|
||||
if authorization != "" {
|
||||
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
||||
if !ok {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed with malformed authorization header")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_grant",
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -379,7 +393,32 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
if strings.ToLower(tokenType) != "bearer" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_grant",
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token = bearerToken
|
||||
} else if c.Request.Method == http.MethodPost {
|
||||
if c.ContentType() != "application/x-www-form-urlencoded" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
token = c.PostForm("access_token")
|
||||
if token == "" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo POST accessed without access_token in body")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -15,11 +17,13 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOIDCController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oidcServiceCfg := service.OIDCServiceConfig{
|
||||
@@ -431,6 +435,349 @@ func TestOIDCController(t *testing.T) {
|
||||
assert.False(t, ok, "Did not expect email claim in userinfo response")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with no authorization header",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with malformed authorization header",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Bearer")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with invalid token type",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Basic some-token")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with empty bearer token",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Bearer ")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_grant", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo POST rejects missing access token in body",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(""))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo POST rejects wrong content type",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(`{"access_token":"some-token"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo accepts access token via POST body",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
tokenTest, found := getTestByDescription("Ensure we can get a token with a valid request")
|
||||
assert.True(t, found, "Token test not found")
|
||||
tokenRecorder := httptest.NewRecorder()
|
||||
tokenTest(t, router, tokenRecorder)
|
||||
|
||||
var tokenRes map[string]any
|
||||
err := json.Unmarshal(tokenRecorder.Body.Bytes(), &tokenRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
accessToken := tokenRes["access_token"].(string)
|
||||
assert.NotEmpty(t, accessToken)
|
||||
|
||||
body := url.Values{}
|
||||
body.Set("access_token", accessToken)
|
||||
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(body.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var userInfoRes map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &userInfoRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, ok := userInfoRes["sub"]
|
||||
assert.True(t, ok, "Expected sub claim in userinfo response")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure plain PKCE succeeds",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: "some-challenge",
|
||||
// Not setting a code challenge method should default to "plain"
|
||||
CodeChallengeMethod: "",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// Now exchange the code for a token
|
||||
recorder = httptest.NewRecorder()
|
||||
tokenReqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
CodeVerifier: "some-challenge",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(tokenReqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure S256 PKCE succeeds",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("some-challenge"))
|
||||
codeChallenge := hasher.Sum(nil)
|
||||
codeChallengeEncoded := base64.RawURLEncoding.EncodeToString(codeChallenge)
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: codeChallengeEncoded,
|
||||
CodeChallengeMethod: "S256",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// Now exchange the code for a token
|
||||
recorder = httptest.NewRecorder()
|
||||
tokenReqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
CodeVerifier: "some-challenge",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(tokenReqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure request with invalid PKCE fails",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("some-challenge"))
|
||||
codeChallenge := hasher.Sum(nil)
|
||||
codeChallengeEncoded := base64.RawURLEncoding.EncodeToString(codeChallenge)
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: codeChallengeEncoded,
|
||||
CodeChallengeMethod: "S256",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// Now exchange the code for a token
|
||||
recorder = httptest.NewRecorder()
|
||||
tokenReqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
CodeVerifier: "some-challenge-1",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(tokenReqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure request with invalid challenge method fails",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("some-challenge"))
|
||||
codeChallenge := hasher.Sum(nil)
|
||||
codeChallengeEncoded := base64.RawURLEncoding.EncodeToString(codeChallenge)
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: codeChallengeEncoded,
|
||||
CodeChallengeMethod: "foo",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
error := queryParams.Get("error")
|
||||
assert.NotEmpty(t, error)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
@@ -25,6 +25,15 @@ const (
|
||||
ForwardAuth
|
||||
)
|
||||
|
||||
type ProxyType int
|
||||
|
||||
const (
|
||||
Traefik ProxyType = iota
|
||||
Caddy
|
||||
Envoy
|
||||
Nginx
|
||||
)
|
||||
|
||||
var BrowserUserAgentRegex = regexp.MustCompile("Chrome|Gecko|AppleWebKit|Opera|Edge")
|
||||
|
||||
type Proxy struct {
|
||||
@@ -38,6 +47,7 @@ type ProxyContext struct {
|
||||
Method string
|
||||
Type AuthModuleType
|
||||
IsBrowser bool
|
||||
ProxyType ProxyType
|
||||
}
|
||||
|
||||
type ProxyControllerConfig struct {
|
||||
@@ -121,7 +131,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !controller.auth.CheckIP(acls.IP, clientIP) {
|
||||
if !controller.useFriendlyError(proxyCtx) {
|
||||
if !controller.useBrowserResponse(proxyCtx) {
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -165,7 +175,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
if !userAllowed {
|
||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
|
||||
|
||||
if !controller.useFriendlyError(proxyCtx) {
|
||||
if !controller.useBrowserResponse(proxyCtx) {
|
||||
c.JSON(403, gin.H{
|
||||
"status": 403,
|
||||
"message": "Forbidden",
|
||||
@@ -205,7 +215,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
if !groupOK {
|
||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
|
||||
|
||||
if !controller.useFriendlyError(proxyCtx) {
|
||||
if !controller.useBrowserResponse(proxyCtx) {
|
||||
c.JSON(403, gin.H{
|
||||
"status": 403,
|
||||
"message": "Forbidden",
|
||||
@@ -256,7 +266,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.useFriendlyError(proxyCtx) {
|
||||
if !controller.useBrowserResponse(proxyCtx) {
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -296,7 +306,7 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
|
||||
}
|
||||
|
||||
func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyContext) {
|
||||
if !controller.useFriendlyError(proxyCtx) {
|
||||
if !controller.useBrowserResponse(proxyCtx) {
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
@@ -312,8 +322,34 @@ func (controller *ProxyController) getHeader(c *gin.Context, header string) (str
|
||||
return val, strings.TrimSpace(val) != ""
|
||||
}
|
||||
|
||||
func (controller *ProxyController) useFriendlyError(proxyCtx ProxyContext) bool {
|
||||
return (proxyCtx.Type == ForwardAuth || proxyCtx.Type == ExtAuthz) && proxyCtx.IsBrowser
|
||||
func (controller *ProxyController) useBrowserResponse(proxyCtx ProxyContext) bool {
|
||||
// If it's nginx or envoy we need non-browser response
|
||||
if proxyCtx.ProxyType == Nginx || proxyCtx.ProxyType == Envoy {
|
||||
return false
|
||||
}
|
||||
|
||||
// For other proxies (traefik or caddy) we can check
|
||||
// the user agent to determine if it's a browser or not
|
||||
if proxyCtx.IsBrowser {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (controller *ProxyController) getProxyType(proxy string) (ProxyType, error) {
|
||||
switch proxy {
|
||||
case "traefik":
|
||||
return Traefik, nil
|
||||
case "caddy":
|
||||
return Caddy, nil
|
||||
case "envoy":
|
||||
return Envoy, nil
|
||||
case "nginx":
|
||||
return Nginx, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported proxy type: %v", proxy)
|
||||
}
|
||||
}
|
||||
|
||||
// Code below is inspired from https://github.com/authelia/authelia/blob/master/internal/handlers/handler_authz.go
|
||||
@@ -417,13 +453,13 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (controller *ProxyController) determineAuthModules(proxy string) []AuthModuleType {
|
||||
func (controller *ProxyController) determineAuthModules(proxy ProxyType) []AuthModuleType {
|
||||
switch proxy {
|
||||
case "traefik", "caddy":
|
||||
case Traefik, Caddy:
|
||||
return []AuthModuleType{ForwardAuth}
|
||||
case "envoy":
|
||||
case Envoy:
|
||||
return []AuthModuleType{ExtAuthz, ForwardAuth}
|
||||
case "nginx":
|
||||
case Nginx:
|
||||
return []AuthModuleType{AuthRequest, ForwardAuth}
|
||||
default:
|
||||
return []AuthModuleType{}
|
||||
@@ -462,9 +498,15 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
|
||||
return ProxyContext{}, err
|
||||
}
|
||||
|
||||
proxy, err := controller.getProxyType(req.Proxy)
|
||||
|
||||
if err != nil {
|
||||
return ProxyContext{}, err
|
||||
}
|
||||
|
||||
tlog.App.Debug().Msgf("Proxy: %v", req.Proxy)
|
||||
|
||||
authModules := controller.determineAuthModules(req.Proxy)
|
||||
authModules := controller.determineAuthModules(proxy)
|
||||
|
||||
if len(authModules) == 0 {
|
||||
return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy)
|
||||
@@ -497,5 +539,6 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
|
||||
}
|
||||
|
||||
ctx.IsBrowser = isBrowser
|
||||
ctx.ProxyType = proxy
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func TestProxyController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authServiceCfg := service.AuthServiceConfig{
|
||||
@@ -164,6 +165,79 @@ func TestProxyController(t *testing.T) {
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth fallback for nginx with browser user agent",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
req.Header.Set("user-agent", browserUserAgent)
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth fallback for envoy with browser user agent",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/hello")
|
||||
req.Header.Set("user-agent", browserUserAgent)
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth with is browser false returns json",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
||||
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth with caddy and browser user agent returns redirect",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
req.Header.Set("user-agent", browserUserAgent)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 307, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=")
|
||||
assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth with caddy and non browser user agent returns json",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
||||
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure normal authentication flow for forward auth",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
@@ -317,8 +391,6 @@ func TestProxyController(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResourcesController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
resourcesControllerCfg := controller.ResourcesControllerConfig{
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func TestUserController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authServiceCfg := service.AuthServiceConfig{
|
||||
@@ -274,8 +275,6 @@ func TestUserController(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
@@ -13,11 +13,13 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWellKnownController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oidcServiceCfg := service.OIDCServiceConfig{
|
||||
|
||||
@@ -24,6 +24,7 @@ var (
|
||||
"GET /api/oidc/clients",
|
||||
"POST /api/oidc/token",
|
||||
"GET /api/oidc/userinfo",
|
||||
"POST /api/oidc/userinfo",
|
||||
"GET /resources",
|
||||
"POST /api/user/login",
|
||||
"GET /.well-known/openid-configuration",
|
||||
|
||||
@@ -12,6 +12,7 @@ type OidcCode struct {
|
||||
ClientID string
|
||||
ExpiresAt int64
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
}
|
||||
|
||||
type OidcToken struct {
|
||||
|
||||
@@ -17,11 +17,12 @@ INSERT INTO "oidc_codes" (
|
||||
"redirect_uri",
|
||||
"client_id",
|
||||
"expires_at",
|
||||
"nonce"
|
||||
"nonce",
|
||||
"code_challenge"
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
|
||||
`
|
||||
|
||||
type CreateOidcCodeParams struct {
|
||||
@@ -32,6 +33,7 @@ type CreateOidcCodeParams struct {
|
||||
ClientID string
|
||||
ExpiresAt int64
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
|
||||
@@ -43,6 +45,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
|
||||
arg.ClientID,
|
||||
arg.ExpiresAt,
|
||||
arg.Nonce,
|
||||
arg.CodeChallenge,
|
||||
)
|
||||
var i OidcCode
|
||||
err := row.Scan(
|
||||
@@ -53,6 +56,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -156,7 +160,7 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
|
||||
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
|
||||
DELETE FROM "oidc_codes"
|
||||
WHERE "expires_at" < ?
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
|
||||
@@ -176,6 +180,7 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -286,7 +291,7 @@ func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
|
||||
const getOidcCode = `-- name: GetOidcCode :one
|
||||
DELETE FROM "oidc_codes"
|
||||
WHERE "code_hash" = ?
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
|
||||
`
|
||||
|
||||
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
|
||||
@@ -300,6 +305,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -307,7 +313,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
|
||||
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
|
||||
DELETE FROM "oidc_codes"
|
||||
WHERE "sub" = ?
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
|
||||
`
|
||||
|
||||
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
|
||||
@@ -321,12 +327,13 @@ func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, e
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
|
||||
WHERE "sub" = ?
|
||||
`
|
||||
|
||||
@@ -341,12 +348,13 @@ func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcC
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
|
||||
WHERE "code_hash" = ?
|
||||
`
|
||||
|
||||
@@ -361,6 +369,7 @@ func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcC
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -28,12 +28,26 @@ const MaxOAuthPendingSessions = 256
|
||||
const OAuthCleanupCount = 16
|
||||
const MaxLoginAttemptRecords = 256
|
||||
|
||||
// slightly modified version of the AuthorizeRequest from the OIDC service to basically accept all
|
||||
// parameters and pass them to the authorize page if needed
|
||||
type OAuthURLParams struct {
|
||||
Scope string `form:"scope" url:"scope"`
|
||||
ResponseType string `form:"response_type" url:"response_type"`
|
||||
ClientID string `form:"client_id" url:"client_id"`
|
||||
RedirectURI string `form:"redirect_uri" url:"redirect_uri"`
|
||||
State string `form:"state" url:"state"`
|
||||
Nonce string `form:"nonce" url:"nonce"`
|
||||
CodeChallenge string `form:"code_challenge" url:"code_challenge"`
|
||||
CodeChallengeMethod string `form:"code_challenge_method" url:"code_challenge_method"`
|
||||
}
|
||||
|
||||
type OAuthPendingSession struct {
|
||||
State string
|
||||
Verifier string
|
||||
Token *oauth2.Token
|
||||
Service *OAuthServiceImpl
|
||||
ExpiresAt time.Time
|
||||
CallbackParams OAuthURLParams
|
||||
}
|
||||
|
||||
type LdapGroupsCache struct {
|
||||
@@ -598,7 +612,7 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) NewOAuthSession(serviceName string) (string, OAuthPendingSession, error) {
|
||||
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
||||
auth.ensureOAuthSessionLimit()
|
||||
|
||||
service, ok := auth.oauthBroker.GetService(serviceName)
|
||||
@@ -621,6 +635,7 @@ func (auth *AuthService) NewOAuthSession(serviceName string) (string, OAuthPendi
|
||||
Verifier: verifier,
|
||||
Service: &service,
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||
CallbackParams: params,
|
||||
}
|
||||
|
||||
auth.oauthMutex.Lock()
|
||||
@@ -631,7 +646,7 @@ func (auth *AuthService) NewOAuthSession(serviceName string) (string, OAuthPendi
|
||||
}
|
||||
|
||||
func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
|
||||
session, err := auth.getOAuthPendingSession(sessionId)
|
||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -641,7 +656,7 @@ func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
|
||||
}
|
||||
|
||||
func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) {
|
||||
session, err := auth.getOAuthPendingSession(sessionId)
|
||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -661,7 +676,7 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
|
||||
}
|
||||
|
||||
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, error) {
|
||||
session, err := auth.getOAuthPendingSession(sessionId)
|
||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||
|
||||
if err != nil {
|
||||
return config.Claims{}, err
|
||||
@@ -681,7 +696,7 @@ func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, erro
|
||||
}
|
||||
|
||||
func (auth *AuthService) GetOAuthService(sessionId string) (OAuthServiceImpl, error) {
|
||||
session, err := auth.getOAuthPendingSession(sessionId)
|
||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -715,7 +730,7 @@ func (auth *AuthService) CleanupOAuthSessionsRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthService) getOAuthPendingSession(sessionId string) (*OAuthPendingSession, error) {
|
||||
func (auth *AuthService) GetOAuthPendingSession(sessionId string) (*OAuthPendingSession, error) {
|
||||
auth.ensureOAuthSessionLimit()
|
||||
|
||||
auth.oauthMutex.RLock()
|
||||
|
||||
@@ -81,6 +81,8 @@ type AuthorizeRequest struct {
|
||||
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
CodeChallengeMethod string `json:"code_challenge_method"`
|
||||
}
|
||||
|
||||
type OIDCServiceConfig struct {
|
||||
@@ -293,6 +295,13 @@ func (service *OIDCService) ValidateAuthorizeParams(req AuthorizeRequest) error
|
||||
return errors.New("invalid_request_uri")
|
||||
}
|
||||
|
||||
// PKCE code challenge method if set
|
||||
if req.CodeChallenge != "" && req.CodeChallengeMethod != "" {
|
||||
if req.CodeChallengeMethod != "S256" && req.CodeChallengeMethod != "plain" {
|
||||
return errors.New("invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -306,8 +315,7 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
|
||||
// Fixed 10 minutes
|
||||
expiresAt := time.Now().Add(time.Minute * time.Duration(10)).Unix()
|
||||
|
||||
// Insert the code into the database
|
||||
_, err := service.queries.CreateOidcCode(c, repository.CreateOidcCodeParams{
|
||||
entry := repository.CreateOidcCodeParams{
|
||||
Sub: sub,
|
||||
CodeHash: service.Hash(code),
|
||||
// Here it's safe to split and trust the output since, we validated the scopes before
|
||||
@@ -316,7 +324,19 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
|
||||
ClientID: req.ClientID,
|
||||
ExpiresAt: expiresAt,
|
||||
Nonce: req.Nonce,
|
||||
})
|
||||
}
|
||||
|
||||
if req.CodeChallenge != "" {
|
||||
if req.CodeChallengeMethod == "S256" {
|
||||
entry.CodeChallenge = req.CodeChallenge
|
||||
} else {
|
||||
entry.CodeChallenge = service.hashAndEncodePKCE(req.CodeChallenge)
|
||||
tlog.App.Warn().Msg("Received plain PKCE code challenge, it's recommended to use S256 for better security")
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the code into the database
|
||||
_, err := service.queries.CreateOidcCode(c, entry)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -728,3 +748,16 @@ func (service *OIDCService) GetJWK() ([]byte, error) {
|
||||
|
||||
return jwk.Public().MarshalJSON()
|
||||
}
|
||||
|
||||
func (service *OIDCService) ValidatePKCE(codeChallenge string, codeVerifier string) bool {
|
||||
if codeChallenge == "" {
|
||||
return true
|
||||
}
|
||||
return codeChallenge == service.hashAndEncodePKCE(codeVerifier)
|
||||
}
|
||||
|
||||
func (service *OIDCService) hashAndEncodePKCE(codeVerifier string) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(codeVerifier))
|
||||
return base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -55,6 +55,17 @@ func NewSimpleLogger() *Logger {
|
||||
})
|
||||
}
|
||||
|
||||
func NewTestLogger() *Logger {
|
||||
return NewLogger(config.LogConfig{
|
||||
Level: "trace",
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{Enabled: true},
|
||||
App: config.LogStreamConfig{Enabled: true},
|
||||
Audit: config.LogStreamConfig{Enabled: true},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Logger) Init() {
|
||||
Audit = l.Audit
|
||||
HTTP = l.HTTP
|
||||
|
||||
@@ -6,9 +6,10 @@ INSERT INTO "oidc_codes" (
|
||||
"redirect_uri",
|
||||
"client_id",
|
||||
"expires_at",
|
||||
"nonce"
|
||||
"nonce",
|
||||
"code_challenge"
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ CREATE TABLE IF NOT EXISTS "oidc_codes" (
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"expires_at" INTEGER NOT NULL,
|
||||
"nonce" TEXT DEFAULT ""
|
||||
"nonce" TEXT DEFAULT "",
|
||||
"code_challenge" TEXT DEFAULT ""
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
||||
|
||||
Reference in New Issue
Block a user