Compare commits

..

19 Commits

Author SHA1 Message Date
Stavros 8426db306d chore: rabbit comments 2026-06-14 18:20:17 +03:00
Stavros 4b323d07ab tests: use new inputs in tests 2026-06-14 18:01:12 +03:00
Stavros 568809a9dc fix: fix gh codeql review 2026-06-14 01:01:45 +03:00
Stavros e4dc3ca2e4 fix: don't use pointers in interfaces 2026-06-14 00:35:18 +03:00
Stavros f8b85e3bc7 feat: use dig for controllers 2026-06-14 00:20:06 +03:00
Stavros 7cd3719734 feat: use dig for all services 2026-06-13 23:20:46 +03:00
Stavros c51ec3c7f6 feat: use dig for di in services 2026-06-13 20:25:18 +03:00
Stavros a0e74cd5f2 refactor: move oidc handling to backend and add support for oidc post (#923)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-13 16:45:12 +03:00
Ryc O'Chet 49105ce5ff feat: add ldap bind password file (#929) 2026-06-11 13:25:22 +03:00
Stavros 57c573502d chore: bump go to 1.26.4 2026-06-09 11:44:03 +03:00
Stavros 426eac2d0b refactor: rework oidc session storage (#913) 2026-06-06 16:26:08 +03:00
dependabot[bot] da17be400e chore(deps): bump the minor-patch group across 1 directory with 4 updates (#920)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:53:46 +03:00
dependabot[bot] 514fcb8fcc chore(deps): bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:53:24 +03:00
dependabot[bot] 831180c7fa chore(deps): bump docker/metadata-action from 6.0.0 to 6.1.0 (#900)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:53:10 +03:00
dependabot[bot] e0ab7c75bc chore(deps): bump node from 26.2-alpine3.23 to 26.3-alpine3.23 (#914)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:52:53 +03:00
dependabot[bot] 66546439fa chore(deps): bump docker/login-action from 4.1.0 to 4.2.0 (#902)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:52:38 +03:00
dependabot[bot] df742abb8d chore(deps): bump github.com/quic-go/quic-go from 0.59.0 to 0.59.1 (#917)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:52:18 +03:00
dependabot[bot] 57e1f963df chore(deps): bump github/codeql-action from 4.35.5 to 4.36.1 (#918)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:51:55 +03:00
dependabot[bot] d7c255948c chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#919)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:51:33 +03:00
70 changed files with 2511 additions and 1710 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
@@ -23,7 +23,7 @@ jobs:
- name: Setup go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "^1.26.0"
go-version: "^1.26.4"
- name: Go dependencies
run: go mod download
+28 -28
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Delete old release
run: gh release delete --cleanup-tag --yes nightly || echo release not found
@@ -37,7 +37,7 @@ jobs:
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
@@ -55,7 +55,7 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
@@ -67,7 +67,7 @@ jobs:
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "^1.26.0"
go-version: "^1.26.4"
- name: Install frontend dependencies
working-directory: ./frontend
@@ -100,7 +100,7 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
@@ -112,7 +112,7 @@ jobs:
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "^1.26.0"
go-version: "^1.26.4"
- name: Install frontend dependencies
working-directory: ./frontend
@@ -145,25 +145,25 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -203,25 +203,25 @@ jobs:
- image-build
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -261,25 +261,25 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -319,25 +319,25 @@ jobs:
- image-build-arm
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: nightly
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -384,18 +384,18 @@ jobs:
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
@@ -423,18 +423,18 @@ jobs:
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
+27 -27
View File
@@ -18,7 +18,7 @@ jobs:
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate metadata
id: metadata
@@ -33,7 +33,7 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
@@ -43,7 +43,7 @@ jobs:
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "^1.26.0"
go-version: "^1.26.4"
- name: Install frontend dependencies
working-directory: ./frontend
@@ -75,7 +75,7 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
@@ -85,7 +85,7 @@ jobs:
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "^1.26.0"
go-version: "^1.26.4"
- name: Install frontend dependencies
working-directory: ./frontend
@@ -117,23 +117,23 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -173,23 +173,23 @@ jobs:
- image-build
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -229,23 +229,23 @@ jobs:
- generate-metadata
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -285,23 +285,23 @@ jobs:
- image-build-arm
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -349,18 +349,18 @@ jobs:
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
@@ -390,18 +390,18 @@ jobs:
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/tinyauth
flavor: |
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
persist-credentials: false
@@ -38,6 +38,6 @@ jobs:
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
with:
sarif_file: results.sarif
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
+1 -1
View File
@@ -8,7 +8,7 @@ Contributing to Tinyauth is straightforward. Follow the steps below to set up a
## Requirements
- pnpm
- Golang v1.24.0 or later
- Golang v1.26.4 or later
- Git
- Docker
- Make
+1 -1
View File
@@ -1,5 +1,5 @@
# Site builder
FROM node:26.2-alpine3.23 AS frontend-builder
FROM node:26.3-alpine3.23 AS frontend-builder
WORKDIR /frontend
+1 -1
View File
@@ -1,5 +1,5 @@
# Site builder
FROM node:26.2-alpine3.23 AS frontend-builder
FROM node:26.3-alpine3.23 AS frontend-builder
WORKDIR /frontend
@@ -1,36 +0,0 @@
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { useState } from "react";
import i18n from "@/lib/i18n/i18n";
export const LanguageSelector = () => {
const [language, setLanguage] = useState<SupportedLanguage>(
i18n.language as SupportedLanguage,
);
const handleSelect = (option: string) => {
setLanguage(option as SupportedLanguage);
i18n.changeLanguage(option as SupportedLanguage);
};
return (
<Select onValueChange={handleSelect} value={language}>
<SelectTrigger aria-label="Select language">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
{Object.entries(languages).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
+3 -5
View File
@@ -1,9 +1,8 @@
import { useAppContext } from "@/context/app-context";
import { LanguageSelector } from "../language/language";
import { Outlet } from "react-router";
import { useCallback, useEffect, useState } from "react";
import { DomainWarning } from "../domain-warning/domain-warning";
import { ThemeToggle } from "../theme-toggle/theme-toggle";
import { QuickActions } from "../quick-actions/quick-actions";
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
const { ui } = useAppContext();
@@ -21,9 +20,8 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
backgroundPosition: "center",
}}
>
<div className="absolute top-4 right-4 flex flex-row gap-2">
<ThemeToggle />
<LanguageSelector />
<div className="absolute top-4 right-4">
<QuickActions />
</div>
<div className="max-w-sm md:min-w-sm min-w-xs">{children}</div>
</div>
@@ -0,0 +1,208 @@
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { useState } from "react";
import i18n from "@/lib/i18n/i18n";
import { useUserContext } from "@/context/user-context";
import { ScrollArea } from "../ui/scroll-area";
import { useTheme } from "../providers/theme-provider";
import {
Check,
DoorOpenIcon,
Languages,
Monitor,
Moon,
Palette,
Settings,
Sun,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import { useRef } from "react";
import {
useScreenParams,
recompileScreenParams,
} from "@/lib/hooks/screen-params";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
import { useEffect } from "react";
function Avatar({ initial }: { initial: string }) {
return (
<span className="group relative grid size-10 place-items-center rounded-full">
<span className="absolute inset-0 overflow-hidden rounded-full bg-linear-to-b from-neutral-50 to-neutral-100 dark:from-neutral-700 dark:to-neutral-950 shadow-lg"></span>
<span className="relative text-sm font-semibold text-primary">
{initial}
</span>
</span>
);
}
export const QuickActions = () => {
const { auth } = useUserContext();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const { search } = useLocation();
const [language, setLanguage] = useState<SupportedLanguage>(
i18n.language as SupportedLanguage,
);
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"),
mutationKey: ["logout"],
onSuccess: () => {
toast.success(t("logoutSuccessTitle"), {
description: t("logoutSuccessSubtitle"),
});
redirectTimer.current = window.setTimeout(() => {
window.location.replace(`/login${compiledParams}`);
}, 500);
},
onError: () => {
toast.error(t("logoutFailTitle"), {
description: t("logoutFailSubtitle"),
});
},
});
useEffect(() => {
return () => {
if (redirectTimer.current) {
clearTimeout(redirectTimer.current);
}
};
}, [redirectTimer]);
const initial = auth.authenticated
? (auth.name[0] || "U").toUpperCase()
: null;
const handleSelect = (option: string) => {
setLanguage(option as SupportedLanguage);
i18n.changeLanguage(option as SupportedLanguage);
};
const themes = [
{ key: "light", label: t("quickActionsThemeLight"), icon: Sun },
{ key: "dark", label: t("quickActionsThemeDark"), icon: Moon },
{ key: "system", label: t("quickActionsThemeSystem"), icon: Monitor },
] as const;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
aria-label={t("quickActionsTitle")}
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
>
{auth.authenticated ? (
<Avatar initial={initial!} />
) : (
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
<Settings className="size-4" />
</span>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={8}
className="rounded-xl p-1"
>
{auth.authenticated && (
<>
<DropdownMenuLabel className="flex items-center gap-3 p-2">
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium">
{initial}
</div>
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">
{auth.name}
</span>
<span className="text-muted-foreground truncate text-xs font-normal">
{auth.email}
</span>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Languages className="size-4" />
{t("quickActionsLanguage")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent sideOffset={8} className="rounded-xl p-1">
<ScrollArea className="h-80">
{Object.entries(languages).map(([key, value]) => (
<DropdownMenuItem
key={key}
onSelect={() => handleSelect(key)}
>
{value}
{language === key && <Check className="size-4" />}
</DropdownMenuItem>
))}
</ScrollArea>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="size-4" />
{t("quickActionsTheme")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="rounded-xl p-1" sideOffset={8}>
{themes.map(({ key, label, icon: Icon }) => (
<DropdownMenuItem key={key} onClick={() => setTheme(key)}>
<span className="flex items-center gap-2">
<Icon className="size-4" />
{label}
</span>
{theme === key && <Check className="size-4" />}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{auth.authenticated && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => logoutMutation.mutate()}
className="text-destructive"
>
<DoorOpenIcon className="size-4" />
{t("quickActionsLogout")}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,40 +0,0 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "@/components/providers/theme-provider";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="bg-card text-card-foreground hover:bg-card/90"
size="icon"
>
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,56 @@
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }
+17
View File
@@ -0,0 +1,17 @@
type UseLoginForProps = {
login_for?: "oidc" | "app";
compiledParams: string;
};
export const useLoginFor = (props: UseLoginForProps): string => {
const { login_for, compiledParams } = props;
switch (login_for) {
case "oidc":
return "/oidc/authorize" + compiledParams;
case "app":
return "/continue" + compiledParams;
default:
return "/logout";
}
};
-76
View File
@@ -1,76 +0,0 @@
import { z } from "zod";
export const oidcParamsSchema = z.object({
scope: z.string().min(1),
response_type: z.string().min(1),
client_id: z.string().min(1),
redirect_uri: z.string().min(1),
state: z.string().optional(),
nonce: z.string().optional(),
code_challenge: z.string().optional(),
code_challenge_method: z.string().optional(),
});
function b64urlDecode(s: string): string {
const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
return atob(base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="));
}
function decodeRequestObject(jwt: string): Record<string, string> {
try {
// Must have exactly 3 parts: header, payload, signature
const parts = jwt.split(".");
if (parts.length !== 3) return {};
// Header must specify "alg": "none" and signature must be empty string
const header = JSON.parse(b64urlDecode(parts[0]));
if (!header || typeof header !== "object" || header.alg !== "none" || parts[2] !== "") return {};
const payload = JSON.parse(b64urlDecode(parts[1]));
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
const result: Record<string, string> = {};
for (const [k, v] of Object.entries(payload)) {
if (typeof v === "string") result[k] = v;
}
return result;
} catch {
return {};
}
}
export const useOIDCParams = (
params: URLSearchParams,
): {
values: z.infer<typeof oidcParamsSchema>;
issues: string[];
isOidc: boolean;
compiled: string;
} => {
const obj = Object.fromEntries(params.entries());
// RFC 9101 / OIDC Core 6.1: if `request` param present, decode JWT payload
// and merge claims over top-level params (JWT claims take precedence)
const requestJwt = params.get("request");
if (requestJwt) {
const claims = decodeRequestObject(requestJwt);
Object.assign(obj, claims);
}
const parsed = oidcParamsSchema.safeParse(obj);
if (parsed.success) {
return {
values: parsed.data,
issues: [],
isOidc: true,
compiled: new URLSearchParams(parsed.data).toString(),
};
}
return {
issues: parsed.error.issues.map((issue) => issue.path.toString()),
values: {} as z.infer<typeof oidcParamsSchema>,
isOidc: false,
compiled: "",
};
};
+1 -1
View File
@@ -7,7 +7,7 @@ type IuseRedirectUri = {
};
export const useRedirectUri = (
redirect_uri: string | null,
redirect_uri: string | undefined,
cookieDomain: string,
): IuseRedirectUri => {
let isValid = false;
+40
View File
@@ -0,0 +1,40 @@
import { z } from "zod";
type ScreenParams = {
login_for?: "oidc" | "app";
redirect_uri?: string;
oidc_ticket?: string;
oidc_scope?: string;
oidc_name?: string;
};
const zodScreenParams = z.object({
login_for: z.enum(["oidc", "app"]).optional(),
redirect_uri: z.string().optional(),
oidc_ticket: z.string().optional(),
oidc_scope: z.string().optional(),
oidc_name: z.string().optional(),
});
export function useScreenParams(params: URLSearchParams): ScreenParams {
const paramsObj = Object.fromEntries(params.entries());
const parsed = zodScreenParams.safeParse(paramsObj);
if (!parsed.success) {
return {};
}
return parsed.data;
}
export function recompileScreenParams(params: ScreenParams): string {
const p = new URLSearchParams(
Object.fromEntries(
Object.entries(params).filter(([, v]) => v !== undefined),
) as Record<string, string>,
).toString();
if (p.length > 0) {
return "?" + p;
}
return "";
}
+101 -94
View File
@@ -1,96 +1,103 @@
{
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
"openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email",
"emailScopeDescription": "Allows the app to access your email address.",
"profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email",
"emailScopeDescription": "Allows the app to access your email address.",
"profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions"
}
+101 -94
View File
@@ -1,96 +1,103 @@
{
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
"openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email",
"emailScopeDescription": "Allows the app to access your email address.",
"profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.",
"openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email",
"emailScopeDescription": "Allows the app to access your email address.",
"profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout.",
"quickActionsLanguage": "Language",
"quickActionsTheme": "Theme",
"quickActionsThemeLight": "Light",
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions"
}
+4 -1
View File
@@ -35,7 +35,10 @@ createRoot(document.getElementById("root")!).render(
<Route element={<Layout />} errorElement={<ErrorPage />}>
<Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/authorize" element={<AuthorizePage />} />
<Route
path="/oidc/authorize"
element={<AuthorizePage />}
/>
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="/totp" element={<TotpPage />} />
+18 -50
View File
@@ -1,5 +1,5 @@
import { useUserContext } from "@/context/user-context";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { Navigate, useNavigate } from "react-router";
import { useLocation } from "react-router";
import {
@@ -10,11 +10,9 @@ import {
CardFooter,
CardContent,
} from "@/components/ui/card";
import { getOidcClientInfoSchema } from "@/schemas/oidc-schemas";
import { Button } from "@/components/ui/button";
import axios from "axios";
import { toast } from "sonner";
import { useOIDCParams } from "@/lib/hooks/oidc";
import { useTranslation } from "react-i18next";
import { TFunction } from "i18next";
import { Mail, MapPin, Phone, Shield, User, Users } from "lucide-react";
@@ -23,6 +21,10 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
recompileScreenParams,
useScreenParams,
} from "@/lib/hooks/screen-params";
type Scope = {
id: string;
@@ -84,27 +86,17 @@ export const AuthorizePage = () => {
const scopeMap = createScopeMap(t);
const searchParams = new URLSearchParams(search);
const oidcParams = useOIDCParams(searchParams);
const getClientInfo = useQuery({
queryKey: ["client", oidcParams.values.client_id],
queryFn: async () => {
const res = await fetch(
`/api/oidc/clients/${encodeURIComponent(oidcParams.values.client_id)}`,
);
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
return data;
},
enabled: oidcParams.isOidc,
});
const screenParams = useScreenParams(searchParams);
const isOidc = screenParams.login_for === "oidc";
const compiledParams = recompileScreenParams(screenParams);
const authorizeMutation = useMutation({
mutationFn: () => {
return axios.post("/api/oidc/authorize", {
...oidcParams.values,
return axios.post("/api/oidc/authorize-complete", {
ticket: screenParams.oidc_ticket,
});
},
mutationKey: ["authorize", oidcParams.values.client_id],
mutationKey: ["authorize", screenParams.oidc_ticket],
onSuccess: (data) => {
toast.info(t("authorizeSuccessTitle"), {
description: t("authorizeSuccessSubtitle"),
@@ -118,56 +110,32 @@ export const AuthorizePage = () => {
},
});
if (oidcParams.issues.length > 0) {
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
return (
<Navigate
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: oidcParams.issues.join(", ") }))}`}
to={`/error?error=${encodeURIComponent(t("authorizeErrorInvalidParams"))}`}
replace
/>
);
}
if (!auth.authenticated) {
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
}
if (getClientInfo.isLoading) {
return (
<Card className="gap-0">
<CardHeader>
<CardTitle className="text-xl">
{t("authorizeLoadingTitle")}
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
</CardContent>
</Card>
);
}
if (getClientInfo.isError) {
return (
<Navigate
to={`/error?error=${encodeURIComponent(t("authorizeErrorClientInfo"))}`}
replace
/>
);
return <Navigate to={`/login${compiledParams}`} replace />;
}
const scopes =
oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || [];
screenParams.oidc_scope.split(" ").filter((s) => s.trim() !== "") || [];
return (
<Card>
<CardHeader className="mb-2">
<div className="flex flex-col gap-3 items-center justify-center text-center">
<div className="bg-accent-foreground box-content text-muted text-xl font-bold font-sans rounded-lg size-8 p-2 flex items-center justify-center">
{getClientInfo.data?.name.slice(0, 1) || "U"}
{screenParams.oidc_name ? screenParams.oidc_name.slice(0, 1) : "U"}
</div>
<CardTitle className="text-xl">
{t("authorizeCardTitle", {
app: getClientInfo.data?.name || "Unknown",
app: screenParams.oidc_name || "Unknown",
})}
</CardTitle>
<CardDescription className="text-sm max-w-sm">
@@ -206,7 +174,7 @@ export const AuthorizePage = () => {
{t("authorizeTitle")}
</Button>
<Button
onClick={() => navigate("/")}
onClick={() => navigate(`/logout${compiledParams}`)}
disabled={authorizeMutation.isPending}
variant="outline"
>
+12 -9
View File
@@ -12,6 +12,10 @@ import { Trans, useTranslation } from "react-i18next";
import { Navigate, useLocation, useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
import {
recompileScreenParams,
useScreenParams,
} from "@/lib/hooks/screen-params";
export const ContinuePage = () => {
const { app, ui } = useAppContext();
@@ -25,7 +29,10 @@ export const ContinuePage = () => {
const hasRedirected = useRef(false);
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
const screenParams = useScreenParams(searchParams);
const redirectUri = screenParams.redirect_uri;
const isAppLogin = screenParams.login_for === "app";
const recompiledParams = recompileScreenParams(screenParams);
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
redirectUri,
@@ -43,7 +50,8 @@ export const ContinuePage = () => {
auth.authenticated &&
hasValidRedirect &&
!showUntrustedWarning &&
!showInsecureWarning;
!showInsecureWarning &&
isAppLogin;
const redirectToTarget = useCallback(() => {
if (!urlHref || hasRedirected.current) {
@@ -79,15 +87,10 @@ export const ContinuePage = () => {
}, [shouldAutoRedirect, redirectToTarget]);
if (!auth.authenticated) {
return (
<Navigate
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
replace
/>
);
return <Navigate to={`/login${recompiledParams}`} replace />;
}
if (!hasValidRedirect) {
if (!hasValidRedirect || !isAppLogin) {
return <Navigate to="/logout" replace />;
}
+1 -1
View File
@@ -11,7 +11,7 @@ export const ErrorPage = () => {
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const error = searchParams.get("error") ?? "";
const error = searchParams.get("error") || "";
return (
<Card>
+7 -4
View File
@@ -11,12 +11,18 @@ import { useAppContext } from "@/context/app-context";
import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import { useLocation } from "react-router";
import {
recompileScreenParams,
useScreenParams,
} from "@/lib/hooks/screen-params";
export const ForgotPasswordPage = () => {
const { ui } = useAppContext();
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
return (
<Card>
@@ -37,10 +43,7 @@ export const ForgotPasswordPage = () => {
className="w-full"
variant="outline"
onClick={() => {
const eparams = searchParams.toString();
window.location.replace(
`/login${eparams.length > 0 ? `?${eparams}` : ""}`,
);
window.location.replace(`/login${compiledParams}`);
}}
>
{t("backToLoginButton")}
+25 -52
View File
@@ -18,7 +18,6 @@ import { OAuthButton } from "@/components/ui/oauth-button";
import { SeperatorWithChildren } from "@/components/ui/separator";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context";
import { useOIDCParams } from "@/lib/hooks/oidc";
import { LoginSchema } from "@/schemas/login-schema";
import { useMutation } from "@tanstack/react-query";
import axios, { AxiosError } from "axios";
@@ -26,6 +25,11 @@ import { useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
import {
recompileScreenParams,
useScreenParams,
} from "@/lib/hooks/screen-params";
import { useLoginFor } from "@/lib/hooks/login-for";
const iconMap: Record<string, React.ReactNode> = {
google: <GoogleIcon />,
@@ -46,7 +50,9 @@ export const LoginPage = () => {
const { t } = useTranslation();
const [showRedirectButton, setShowRedirectButton] = useState(false);
const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== undefined);
const [useTailscale, setUseTailscale] = useState(
tailscale.nodeName !== undefined,
);
const hasAutoRedirectedRef = useRef(false);
@@ -56,17 +62,22 @@ export const LoginPage = () => {
const formId = useId();
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri") || undefined;
const oidcParams = useOIDCParams(searchParams);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const loginForUrl = useLoginFor({
login_for: screenParams.login_for,
compiledParams,
});
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauth.autoRedirect) !==
undefined && redirectUri !== undefined,
undefined && screenParams.redirect_uri !== undefined,
);
const oauthProviders = providers.filter(
(provider) => provider.id !== "local" && provider.id !== "ldap",
);
const userAuthConfigured =
providers.find(
(provider) => provider.id === "local" || provider.id === "ldap",
@@ -79,16 +90,7 @@ export const LoginPage = () => {
variables: oauthVariables,
} = useMutation({
mutationFn: (provider: string) => {
const getParams = function (): string {
if (oidcParams.isOidc) {
return `?${oidcParams.compiled}`;
}
if (redirectUri) {
return `?redirect_uri=${encodeURIComponent(redirectUri)}`;
}
return "";
};
return axios.get(`/api/oauth/url/${provider}${getParams()}`);
return axios.get(`/api/oauth/url/${provider}${compiledParams}`);
},
mutationKey: ["oauth"],
onSuccess: (data) => {
@@ -119,13 +121,7 @@ export const LoginPage = () => {
mutationKey: ["login"],
onSuccess: (data) => {
if (data.data.totpPending) {
if (oidcParams.isOidc) {
window.location.replace(`/totp?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
);
window.location.replace(`/totp${compiledParams}`);
return;
}
@@ -134,13 +130,7 @@ export const LoginPage = () => {
});
redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
);
window.location.replace(loginForUrl);
}, 500);
},
onError: (error: AxiosError) => {
@@ -163,13 +153,7 @@ export const LoginPage = () => {
});
redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
);
window.location.replace(loginForUrl);
}, 500);
},
onError: () => {
@@ -184,7 +168,8 @@ export const LoginPage = () => {
!auth.authenticated &&
isOauthAutoRedirect &&
!hasAutoRedirectedRef.current &&
redirectUri !== undefined
screenParams.redirect_uri &&
screenParams.login_for
) {
hasAutoRedirectedRef.current = true;
oauthMutate(oauth.autoRedirect);
@@ -195,7 +180,8 @@ export const LoginPage = () => {
hasAutoRedirectedRef,
oauth.autoRedirect,
isOauthAutoRedirect,
redirectUri,
screenParams.login_for,
screenParams.redirect_uri,
]);
useEffect(() => {
@@ -210,21 +196,8 @@ export const LoginPage = () => {
};
}, [redirectTimer, redirectButtonTimer]);
if (auth.authenticated && oidcParams.isOidc) {
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
}
if (auth.authenticated && redirectUri !== undefined) {
return (
<Navigate
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
replace
/>
);
}
if (auth.authenticated) {
return <Navigate to="/logout" replace />;
return <Navigate to={loginForUrl} replace />;
}
if (isOauthAutoRedirect) {
+11 -2
View File
@@ -15,12 +15,21 @@ import { Navigate } from "react-router";
import { toast } from "sonner";
import { type UseMutationResult } from "@tanstack/react-query";
import { type AxiosResponse } from "axios";
import { useLocation } from "react-router";
import {
useScreenParams,
recompileScreenParams,
} from "@/lib/hooks/screen-params";
export const LogoutPage = () => {
const { auth, oauth, tailscale } = useUserContext();
const { t } = useTranslation();
const { search } = useLocation();
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"),
@@ -31,7 +40,7 @@ export const LogoutPage = () => {
});
redirectTimer.current = window.setTimeout(() => {
window.location.replace("/login");
window.location.replace(`/login${compiledParams}`);
}, 500);
},
onError: () => {
@@ -50,7 +59,7 @@ export const LogoutPage = () => {
}, [redirectTimer]);
if (!auth.authenticated) {
return <Navigate to="/login" replace />;
return <Navigate to={`/login${compiledParams}`} replace />;
}
if (oauth.active) {
+17 -13
View File
@@ -16,10 +16,14 @@ import { useEffect, useId, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
import { useOIDCParams } from "@/lib/hooks/oidc";
import {
recompileScreenParams,
useScreenParams,
} from "@/lib/hooks/screen-params";
import { useLoginFor } from "@/lib/hooks/login-for";
export const TotpPage = () => {
const { totp } = useUserContext();
const { totp, auth } = useUserContext();
const { t } = useTranslation();
const { search } = useLocation();
const formId = useId();
@@ -27,8 +31,12 @@ export const TotpPage = () => {
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri") || undefined;
const oidcParams = useOIDCParams(searchParams);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const loginForUrl = useLoginFor({
login_for: screenParams.login_for,
compiledParams,
});
const totpMutation = useMutation({
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
@@ -39,14 +47,7 @@ export const TotpPage = () => {
});
redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
);
window.location.replace(loginForUrl);
}, 500);
},
onError: () => {
@@ -65,7 +66,10 @@ export const TotpPage = () => {
}, [redirectTimer]);
if (!totp.pending) {
return <Navigate to="/" replace />;
if (auth.authenticated) {
return <Navigate to={loginForUrl} replace />;
}
return <Navigate to={`/login${compiledParams}`} replace />;
}
return (
-5
View File
@@ -1,5 +0,0 @@
import { z } from "zod";
export const getOidcClientInfoSchema = z.object({
name: z.string(),
});
+5
View File
@@ -57,6 +57,11 @@ export default defineConfig({
changeOrigin: true,
rewrite: (path) => path.replace(/^\/robots.txt/, ""),
},
"/authorize": {
target: "http://tinyauth-backend:3000/authorize",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/authorize/, ""),
},
},
allowedHosts: true,
},
+12 -10
View File
@@ -1,6 +1,6 @@
module github.com/tinyauthapp/tinyauth
go 1.26.3
go 1.26.4
require (
charm.land/huh/v2 v2.0.3
@@ -9,10 +9,11 @@ require (
github.com/gin-gonic/gin v1.12.0
github.com/go-jose/go-jose/v4 v4.1.4
github.com/go-ldap/ldap/v3 v3.4.13
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/go-querystring v1.2.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/jackc/pgx/v5 v5.10.0
github.com/mdp/qrterminal/v3 v3.2.1
github.com/pquerna/otp v1.5.0
github.com/rs/zerolog v1.35.1
@@ -20,13 +21,14 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
github.com/weppos/publicsuffix-go v0.50.3
go.uber.org/dig v1.19.0
golang.org/x/crypto v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.44.0
golang.org/x/tools v0.45.0
k8s.io/apimachinery v0.36.1
k8s.io/client-go v0.36.1
modernc.org/sqlite v1.50.1
tailscale.com v1.98.3
modernc.org/sqlite v1.51.0
tailscale.com v1.100.0
)
require (
@@ -77,7 +79,7 @@ require (
github.com/gaissmai/bart v0.26.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-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // 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
@@ -126,7 +128,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
@@ -137,7 +139,7 @@ require (
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e // indirect
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
@@ -156,8 +158,8 @@ require (
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/term v0.43.0 // indirect
+26 -20
View File
@@ -181,8 +181,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -206,6 +206,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
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/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -214,6 +216,8 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
@@ -259,8 +263,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.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
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=
@@ -382,8 +386,8 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
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.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -435,8 +439,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e h1:GexFR7ak1iz26fxg8HWCpOEqAOL8UEZJ7J3JxeCalDs=
github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
@@ -481,6 +485,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
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.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
@@ -499,12 +505,12 @@ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
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/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -520,8 +526,8 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
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.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
@@ -587,8 +593,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/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.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
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=
@@ -605,5 +611,5 @@ sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.98.3 h1:caAbG4UfkKfKPE6b1fj5t4ep5qrwEis5AJu91ruvePw=
tailscale.com v1.98.3/go.mod h1:U23ZwbZlKJMNU7CScy+lCVVlece/S5n09q0nyudncBI=
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
+34 -7
View File
@@ -18,6 +18,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/steveiliop56/ding"
"go.uber.org/dig"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository"
@@ -56,6 +57,7 @@ type BootstrapApp struct {
db *sql.DB
ding *ding.Ding
listeners []Listener
dig *dig.Container
}
func NewBootstrapApp(config model.Config) *BootstrapApp {
@@ -70,7 +72,11 @@ func (app *BootstrapApp) Setup() error {
app.ctx = ctx
app.cancel = cancel
// Create a ding instance
// create the dig container
c := dig.New()
app.dig = c
// create a ding instance
dg := ding.New(ctx)
app.ding = dg
@@ -157,12 +163,6 @@ func (app *BootstrapApp) Setup() error {
app.runtime.OAuthProviders[id] = provider
}
// setup oidc clients
for id, client := range app.config.OIDC.Clients {
client.ID = id
app.runtime.OIDCClients = append(app.runtime.OIDCClients, client)
}
// cookie domain
cookieDomainResolver := utils.GetCookieDomain
@@ -211,6 +211,33 @@ func (app *BootstrapApp) Setup() error {
// store
app.queries = store
// provide basic utilities to container
type utilityProvider struct {
dig.Out
Log *logger.Logger
Config *model.Config
Runtime *model.RuntimeConfig
Ding *ding.Ding
Ctx context.Context
Queries repository.Store
}
err = app.dig.Provide(func() utilityProvider {
return utilityProvider{
Log: app.log,
Config: &app.config,
Runtime: &app.runtime,
Ding: app.ding,
Ctx: app.ctx,
Queries: app.queries,
}
})
if err != nil {
return fmt.Errorf("failed to provide utilities to container: %w", err)
}
// services
err = app.setupServices()
+106 -17
View File
@@ -13,6 +13,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/middleware"
"github.com/tinyauthapp/tinyauth/internal/model"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
)
@@ -40,31 +41,119 @@ func (app *BootstrapApp) setupRouter() error {
}
}
contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService)
engine.Use(contextMiddleware.Middleware())
uiMiddleware, err := middleware.NewUIMiddleware()
err := app.dig.Provide(middleware.NewContextMiddleware)
if err != nil {
return fmt.Errorf("failed to initialize UI middleware: %w", err)
return fmt.Errorf("failed to provide context middleware: %w", err)
}
engine.Use(uiMiddleware.Middleware())
err = app.dig.Provide(middleware.NewUIMiddleware)
zerologMiddleware := middleware.NewZerologMiddleware(app.log)
if err != nil {
return fmt.Errorf("failed to provide ui middleware: %w", err)
}
engine.Use(zerologMiddleware.Middleware())
err = app.dig.Provide(middleware.NewZerologMiddleware)
apiRouter := engine.Group("/api")
if err != nil {
return fmt.Errorf("failed to provide zerolog middleware: %w", err)
}
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine)
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
controller.NewResourcesController(app.config, &engine.RouterGroup)
controller.NewHealthController(apiRouter)
controller.NewWellKnownController(app.services.oidcService, &engine.RouterGroup)
type middlewareInput struct {
dig.In
ContextMiddleware *middleware.ContextMiddleware
UIMiddleware *middleware.UIMiddleware
ZerologMiddleware *middleware.ZerologMiddleware
}
err = app.dig.Invoke(func(mi middlewareInput) {
engine.Use(mi.ContextMiddleware.Middleware())
engine.Use(mi.UIMiddleware.Middleware())
engine.Use(mi.ZerologMiddleware.Middleware())
})
if err != nil {
return fmt.Errorf("failed to invoke middleware: %w", err)
}
err = app.dig.Provide(func() *gin.RouterGroup {
return &engine.RouterGroup
}, dig.Name("mainRouterGroup"))
if err != nil {
return fmt.Errorf("failed to provide main router group: %w", err)
}
err = app.dig.Provide(func() *gin.RouterGroup {
return engine.Group("/api")
}, dig.Name("apiRouterGroup"))
if err != nil {
return fmt.Errorf("failed to provide api router group: %w", err)
}
err = app.dig.Provide(controller.NewContextController)
if err != nil {
return fmt.Errorf("failed to provide context controller: %w", err)
}
err = app.dig.Provide(controller.NewOAuthController)
if err != nil {
return fmt.Errorf("failed to provide oauth controller: %w", err)
}
err = app.dig.Provide(controller.NewOIDCController)
if err != nil {
return fmt.Errorf("failed to provide oidc controller: %w", err)
}
err = app.dig.Provide(controller.NewProxyController)
if err != nil {
return fmt.Errorf("failed to provide proxy controller: %w", err)
}
err = app.dig.Provide(controller.NewUserController)
if err != nil {
return fmt.Errorf("failed to provide user controller: %w", err)
}
err = app.dig.Provide(controller.NewResourcesController)
if err != nil {
return fmt.Errorf("failed to provide resources controller: %w", err)
}
err = app.dig.Provide(controller.NewHealthController)
if err != nil {
return fmt.Errorf("failed to provide health controller: %w", err)
}
err = app.dig.Provide(controller.NewWellKnownController)
if err != nil {
return fmt.Errorf("failed to provide well-known controller: %w", err)
}
type controllerInput struct {
dig.In
ContextController *controller.ContextController
OAuthController *controller.OAuthController
OIDCController *controller.OIDCController
ProxyController *controller.ProxyController
UserController *controller.UserController
ResourcesController *controller.ResourcesController
HealthController *controller.HealthController
WellKnownController *controller.WellKnownController
}
// force dig to build all controllers and register their routes
err = app.dig.Invoke(func(ci controllerInput) error {
return nil
})
if err != nil {
return fmt.Errorf("failed to invoke controllers: %w", err)
}
app.router = engine
return nil
+115 -58
View File
@@ -5,54 +5,84 @@ import (
"os"
"github.com/tinyauthapp/tinyauth/internal/service"
"go.uber.org/dig"
)
func (app *BootstrapApp) setupServices() error {
ldapService, err := service.NewLdapService(app.log, app.config, app.ding)
err := app.setupPolicyEngine()
if err != nil {
app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it")
return fmt.Errorf("failed to setup policy engine: %w", err)
}
app.services.ldapService = ldapService
labelProvider, err := app.getLabelProvider()
if err != nil {
return fmt.Errorf("failed to initialize label provider: %w", err)
return fmt.Errorf("failed to get label provider: %w", err)
}
tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, app.ding)
err = app.dig.Provide(func() service.LabelProvider {
return labelProvider
})
if err != nil {
app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it")
return fmt.Errorf("failed to provide label provider: %w", err)
}
app.services.tailscaleService = tailscaleService
err = app.dig.Provide(service.NewLdapService)
if err != nil {
return fmt.Errorf("failed to provide ldap service: %w", err)
}
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
app.services.accessControlService = accessControlsService
err = app.dig.Provide(service.NewTailscaleService)
if err != nil {
return fmt.Errorf("failed to provide tailscale service: %w", err)
}
err = app.setupPolicyEngine()
err = app.dig.Provide(service.NewAccessControlsService)
if err != nil {
return fmt.Errorf("failed to provide access controls service: %w", err)
}
err = app.dig.Provide(service.NewOAuthBrokerService)
if err != nil {
return fmt.Errorf("failed to provide oauth broker service: %w", err)
}
err = app.dig.Provide(service.NewAuthService)
if err != nil {
return fmt.Errorf("failed to provide auth service: %w", err)
}
err = app.dig.Provide(service.NewOIDCService)
if err != nil {
return fmt.Errorf("failed to provide oidc service: %w", err)
}
type svcInput struct {
dig.In
AccessControlService *service.AccessControlsService
AuthService *service.AuthService
LDAPService *service.LdapService
OAuthBrokerService *service.OAuthBrokerService
OIDCService *service.OIDCService
TailscaleService *service.TailscaleService
}
err = app.dig.Invoke(func(i svcInput) error {
app.services.accessControlService = i.AccessControlService
app.services.authService = i.AuthService
app.services.ldapService = i.LDAPService
app.services.oauthBrokerService = i.OAuthBrokerService
app.services.tailscaleService = i.TailscaleService
return nil
})
if err != nil {
return fmt.Errorf("failed to initialize policy engine: %w", err)
return fmt.Errorf("failed to invoke services: %w", err)
}
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
app.services.oauthBrokerService = oauthBrokerService
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, app.ding, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine)
app.services.authService = authService
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ding)
if err != nil {
return fmt.Errorf("failed to initialize oidc service: %w", err)
}
app.services.oidcService = oidcService
return nil
}
@@ -69,66 +99,93 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
if useKubernetes {
app.log.App.Debug().Msg("Using Kubernetes label provider")
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, app.ding)
err := app.dig.Provide(service.NewKubernetesService)
if err != nil {
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
return nil, fmt.Errorf("failed to provide kubernetes service: %w", err)
}
app.services.kubernetesService = kubernetesService
return kubernetesService, nil
err = app.dig.Invoke(func(k *service.KubernetesService) error {
app.services.kubernetesService = k
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to invoke kubernetes service: %w", err)
}
// Kubernetes will fail to initialize with an error if it cannot connect to the cluster
// but just to be safe, we check if the service is nil and log a warning if it is
if app.services.kubernetesService == nil {
if app.config.LabelProvider == "kubernetes" {
app.log.App.Warn().Msg("Kubernetes label provider selected but Kubernetes is not available, will continue without it")
}
return nil, nil
}
return app.services.kubernetesService, nil
}
app.log.App.Debug().Msg("Using Docker label provider")
dockerService, err := service.NewDockerService(app.log, app.ctx, app.ding)
err := app.dig.Provide(service.NewDockerService)
if err != nil {
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
return nil, fmt.Errorf("failed to provide docker service: %w", err)
}
if dockerService == nil {
err = app.dig.Invoke(func(d *service.DockerService) error {
app.services.dockerService = d
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to invoke docker service: %w", err)
}
if app.services.dockerService == nil {
if app.config.LabelProvider == "docker" {
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
}
return nil, nil
}
app.services.dockerService = dockerService
return dockerService, nil
return app.services.dockerService, nil
default:
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
}
}
func (app *BootstrapApp) setupPolicyEngine() error {
policyEngine, err := service.NewPolicyEngine(app.config, app.log)
err := app.dig.Provide(service.NewPolicyEngine)
if err != nil {
return fmt.Errorf("failed to initialize policy engine: %w", err)
return fmt.Errorf("failed to create policy engine: %w", err)
}
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
Log: app.log,
Config: app.config,
})
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
Log: app.log,
Config: app.config,
err = app.dig.Invoke(func(policyEngine *service.PolicyEngine) error {
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
Log: app.log,
})
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
Log: app.log,
Config: app.config,
})
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
Log: app.log,
Config: app.config,
})
return nil
})
app.services.policyEngine = policyEngine
return nil
return err
}
+21 -16
View File
@@ -3,6 +3,7 @@ package controller
import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
)
@@ -71,29 +72,33 @@ type AppContextResponse struct {
App ACRApp `json:"app"`
}
type ContextController struct {
log *logger.Logger
config model.Config
runtime model.RuntimeConfig
type ContextControllerInput struct {
dig.In
Log *logger.Logger
Config *model.Config
Runtime *model.RuntimeConfig
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
}
func NewContextController(
log *logger.Logger,
config model.Config,
runtimeConfig model.RuntimeConfig,
router *gin.RouterGroup,
) *ContextController {
type ContextController struct {
log *logger.Logger
config *model.Config
runtime *model.RuntimeConfig
}
func NewContextController(i ContextControllerInput) *ContextController {
controller := &ContextController{
log: log,
config: config,
runtime: runtimeConfig,
log: i.Log,
config: i.Config,
runtime: i.Runtime,
}
if !config.UI.WarningsEnabled {
log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.")
if !i.Config.UI.WarningsEnabled {
i.Log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.")
}
contextGroup := router.Group("/context")
contextGroup := i.RouterGroup.Group("/context")
contextGroup.GET("/user", controller.userContextHandler)
contextGroup.GET("/app", controller.appContextHandler)
@@ -121,7 +121,12 @@ func TestContextController(t *testing.T) {
group := router.Group("/api")
gin.SetMode(gin.TestMode)
controller.NewContextController(log, cfg, runtime, group)
controller.NewContextController(controller.ContextControllerInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
RouterGroup: group,
})
recorder := httptest.NewRecorder()
+9 -1
View File
@@ -1,5 +1,12 @@
package controller
type FrontendLoginFor string
const (
FrontendLoginForOIDC FrontendLoginFor = "oidc"
FrontendLoginForApp FrontendLoginFor = "app"
)
type UnauthorizedQuery struct {
Username string `url:"username"`
Resource string `url:"resource"`
@@ -8,5 +15,6 @@ type UnauthorizedQuery struct {
}
type RedirectQuery struct {
RedirectURI string `url:"redirect_uri"`
RedirectURI string `url:"redirect_uri"`
LoginFor FrontendLoginFor `url:"login_for"`
}
+13 -4
View File
@@ -1,15 +1,24 @@
package controller
import "github.com/gin-gonic/gin"
import (
"github.com/gin-gonic/gin"
"go.uber.org/dig"
)
type HealthController struct {
}
func NewHealthController(router *gin.RouterGroup) *HealthController {
type HealthControllerInput struct {
dig.In
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
}
func NewHealthController(i HealthControllerInput) *HealthController {
controller := &HealthController{}
router.GET("/healthz", controller.healthHandler)
router.HEAD("/healthz", controller.healthHandler)
i.RouterGroup.GET("/healthz", controller.healthHandler)
i.RouterGroup.HEAD("/healthz", controller.healthHandler)
return controller
}
@@ -55,7 +55,9 @@ func TestHealthController(t *testing.T) {
group := router.Group("/api")
gin.SetMode(gin.TestMode)
controller.NewHealthController(group)
controller.NewHealthController(controller.HealthControllerInput{
RouterGroup: group,
})
recorder := httptest.NewRecorder()
+25 -22
View File
@@ -11,6 +11,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
"github.com/google/go-querystring/query"
@@ -22,26 +23,30 @@ type OAuthRequest struct {
type OAuthController struct {
log *logger.Logger
config model.Config
runtime model.RuntimeConfig
config *model.Config
runtime *model.RuntimeConfig
auth *service.AuthService
}
func NewOAuthController(
log *logger.Logger,
config model.Config,
runtimeConfig model.RuntimeConfig,
router *gin.RouterGroup,
auth *service.AuthService,
) *OAuthController {
type OAuthControllerInput struct {
dig.In
Log *logger.Logger
Config *model.Config
RuntimeConfig *model.RuntimeConfig
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
AuthService *service.AuthService
}
func NewOAuthController(i OAuthControllerInput) *OAuthController {
controller := &OAuthController{
log: log,
config: config,
runtime: runtimeConfig,
auth: auth,
log: i.Log,
config: i.Config,
runtime: i.RuntimeConfig,
auth: i.AuthService,
}
oauthGroup := router.Group("/oauth")
oauthGroup := i.RouterGroup.Group("/oauth")
oauthGroup.GET("/url/:provider", controller.oauthURLHandler)
oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler)
@@ -61,7 +66,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
var reqParams service.OAuthURLParams
var reqParams service.OAuthCallbackParams
err = c.BindQuery(&reqParams)
@@ -83,7 +88,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
}
}
sessionId, _, err := controller.auth.NewOAuthSession(req.Provider, reqParams)
sessionId, err := controller.auth.NewOAuthSession(req.Provider, reqParams)
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to create new OAuth session")
@@ -272,13 +277,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/authorize?%s", controller.runtime.AppURL, queries.Encode()))
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
return
}
if oauthPendingSession.CallbackParams.RedirectURI != "" {
queries, err := query.Values(RedirectQuery{
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
LoginFor: FrontendLoginForApp,
})
if err != nil {
@@ -294,11 +300,8 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL)
}
func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams) bool {
return params.Scope != "" &&
params.ResponseType != "" &&
params.ClientID != "" &&
params.RedirectURI != ""
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
return params.LoginFor == string(FrontendLoginForOIDC)
}
func (controller *OAuthController) getCookieDomain() string {
+213 -96
View File
@@ -9,7 +9,9 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/google/go-querystring/query"
"go.uber.org/dig"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/service"
@@ -23,12 +25,13 @@ type authorizeErrorParams struct {
callback string
callbackError string
state string
json bool
}
type OIDCController struct {
log *logger.Logger
oidc *service.OIDCService
runtime model.RuntimeConfig
runtime *model.RuntimeConfig
}
type AuthorizeCallback struct {
@@ -65,20 +68,39 @@ type ClientCredentials struct {
ClientSecret string
}
func NewOIDCController(
log *logger.Logger,
oidcService *service.OIDCService,
runtimeConfig model.RuntimeConfig,
router *gin.RouterGroup) *OIDCController {
type AuthorizeScreenParams struct {
LoginFor FrontendLoginFor `url:"login_for"`
OIDCTicket string `url:"oidc_ticket"`
OIDCScope string `url:"oidc_scope"`
OIDCName string `url:"oidc_name"`
}
type AuthorizeCompleteRequest struct {
Ticket string `json:"ticket" binding:"required"`
}
type OIDCControllerInput struct {
dig.In
Log *logger.Logger
OIDCService *service.OIDCService
RuntimeConfig *model.RuntimeConfig
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
MainRouter *gin.RouterGroup `name:"mainRouterGroup"`
}
func NewOIDCController(i OIDCControllerInput) *OIDCController {
controller := &OIDCController{
log: log,
oidc: oidcService,
runtime: runtimeConfig,
log: i.Log,
oidc: i.OIDCService,
runtime: i.RuntimeConfig,
}
oidcGroup := router.Group("/oidc")
oidcGroup.GET("/clients/:id", controller.GetClientInfo)
oidcGroup.POST("/authorize", controller.Authorize)
i.MainRouter.POST("/authorize", controller.authorize)
i.MainRouter.GET("/authorize", controller.authorize)
oidcGroup := i.RouterGroup.Group("/oidc")
oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
oidcGroup.POST("/token", controller.Token)
oidcGroup.GET("/userinfo", controller.Userinfo)
oidcGroup.POST("/userinfo", controller.Userinfo)
@@ -86,47 +108,10 @@ func NewOIDCController(
return controller
}
func (controller *OIDCController) GetClientInfo(c *gin.Context) {
if controller.oidc == nil {
controller.log.App.Warn().Msg("Received OIDC client info request but OIDC server is not configured")
c.JSON(500, gin.H{
"status": 500,
"message": "OIDC not configured",
})
return
}
var req ClientRequest
err := c.BindUri(&req)
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to bind URI")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return
}
client, ok := controller.oidc.GetClient(req.ClientID)
if !ok {
controller.log.App.Warn().Str("clientId", req.ClientID).Msg("Client not found")
c.JSON(404, gin.H{
"status": 404,
"message": "Client not found",
})
return
}
c.JSON(200, gin.H{
"status": 200,
"client": client.ClientID,
"name": client.Name,
})
}
func (controller *OIDCController) Authorize(c *gin.Context) {
// This endpoint does **not** return a code, it handles param validation, ticket creation
// and then redirects to the frontend to handle the consent screen. It performs no destructive
// actions (like logging out an existing session)
func (controller *OIDCController) authorize(c *gin.Context) {
if controller.oidc == nil {
controller.authorizeError(c, authorizeErrorParams{
err: errors.New("err_oidc_not_configured"),
@@ -136,40 +121,19 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
return
}
userContext, err := new(model.UserContext).NewFromGin(c)
req, err := controller.resolveAuthorizeRequest(c)
if err != nil {
controller.log.App.Warn().Err(err).Msg("Failed to resolve authorize request")
controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to get user context",
reasonPublic: "User is not logged in or the session is invalid",
reason: "Failed to resolve authorize request",
reasonPublic: "The authorization request is invalid",
})
return
}
if !userContext.Authenticated {
controller.authorizeError(c, authorizeErrorParams{
err: errors.New("err user not logged in"),
reason: "User not logged in",
reasonPublic: "The user is not logged in",
})
return
}
var req service.AuthorizeRequest
err = c.Bind(&req)
if err != nil {
controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to bind JSON",
reasonPublic: "The client provided an invalid authorization request",
})
return
}
_, ok := controller.oidc.GetClient(req.ClientID)
client, ok := controller.oidc.GetClient(req.ClientID)
if !ok {
controller.authorizeError(c, authorizeErrorParams{
@@ -180,7 +144,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
return
}
err = controller.oidc.ValidateAuthorizeParams(req)
err = controller.oidc.ValidateAuthorizeParams(*req)
if err != nil {
controller.log.App.Warn().Err(err).Msg("Failed to validate authorize params")
@@ -203,8 +167,97 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
return
}
ticket := controller.oidc.CreateAuthorizeRequestTicket(*req)
queries, err := query.Values(AuthorizeScreenParams{
LoginFor: FrontendLoginForOIDC,
OIDCTicket: ticket,
OIDCScope: req.Scope,
OIDCName: client.Name,
})
if err != nil {
controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to compile authorize queries",
reasonPublic: "An internal error occured while processing your request",
})
return
}
redirectUrl := fmt.Sprintf("%s/oidc/authorize?%s", controller.oidc.GetIssuer(), queries.Encode())
c.Redirect(http.StatusFound, redirectUrl)
}
// The actual **internal** endpoint that actually creates the code and session.
// It is called by the frontend after the user has logged in and given consent.
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
if controller.oidc == nil {
// For this endpoint we return JSON errors since it's called
// by the frontend and not an external client, so there's
// no redirect_uri to send the user to in case of error
controller.authorizeError(c, authorizeErrorParams{
err: errors.New("err_oidc_not_configured"),
reason: "OIDC not configured",
reasonPublic: "This instance is not configured for OIDC",
json: true,
})
return
}
userContext, err := new(model.UserContext).NewFromGin(c)
if err != nil {
controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to get user context",
reasonPublic: "User is not logged in or the session is invalid",
json: true,
})
return
}
if !userContext.Authenticated {
controller.authorizeError(c, authorizeErrorParams{
err: errors.New("err user not logged in"),
reason: "User not logged in",
reasonPublic: "The user is not logged in",
json: true,
})
return
}
var req AuthorizeCompleteRequest
err = c.BindJSON(&req)
if err != nil {
controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to bind JSON",
reasonPublic: "The client provided an invalid authorization request",
json: true,
})
return
}
authorizeReq, ok := controller.oidc.GetAuthorizeRequestByTicket(req.Ticket)
if !ok {
controller.authorizeError(c, authorizeErrorParams{
err: errors.New("authorize request not found for ticket"),
reason: "Invalid or expired ticket",
reasonPublic: "The authorization request has expired or is invalid",
json: true,
})
return
}
// We no longer need the ticket
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
// Create the sub to find and delete old sessions
sub := controller.oidc.CreateSub(*userContext, req.ClientID)
sub := controller.oidc.CreateSub(*userContext, authorizeReq.ClientID)
// Before storing the code, delete old session
err = controller.oidc.DeleteOldSession(c, sub)
@@ -213,19 +266,20 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
err: err,
reason: "Failed to delete old sessions",
reasonPublic: "Failed to delete old sessions",
callback: req.RedirectURI,
callback: authorizeReq.RedirectURI,
callbackError: "server_error",
state: req.State,
state: authorizeReq.State,
json: true,
})
return
}
// Create the authorization code
code := controller.oidc.CreateCode(req, *userContext)
code := controller.oidc.CreateCode(*authorizeReq, *userContext)
queries, err := query.Values(AuthorizeCallback{
Code: code,
State: req.State,
State: authorizeReq.State,
})
if err != nil {
@@ -233,16 +287,17 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
err: err,
reason: "Failed to build query",
reasonPublic: "Failed to build query",
callback: req.RedirectURI,
callback: authorizeReq.RedirectURI,
callbackError: "server_error",
state: req.State,
state: authorizeReq.State,
json: true,
})
return
}
c.JSON(200, gin.H{
"status": 200,
"redirect_uri": fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode()),
"redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
})
}
@@ -533,14 +588,22 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
queries, err := query.Values(errorQueries)
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to build callback error query")
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.JSON(200, gin.H{
"status": 200,
"redirect_uri": fmt.Sprintf("%s?%s", params.callback, queries.Encode()),
})
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
if params.json {
c.JSON(200, gin.H{
"status": 200,
"redirect_uri": redirectUrl,
})
return
}
c.Redirect(http.StatusFound, redirectUrl)
return
}
@@ -551,6 +614,7 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
queries, err := query.Values(errorQueries)
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to build error query")
c.AbortWithStatus(http.StatusInternalServerError)
return
}
@@ -563,8 +627,61 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
redirectUrl = fmt.Sprintf("%s/error?%s", controller.runtime.AppURL, queries.Encode())
}
c.JSON(200, gin.H{
"status": 200,
"redirect_uri": redirectUrl,
})
if params.json {
c.JSON(200, gin.H{
"status": 200,
"redirect_uri": redirectUrl,
})
return
}
c.Redirect(http.StatusFound, redirectUrl)
}
func (controller *OIDCController) resolveAuthorizeRequest(c *gin.Context) (*service.AuthorizeRequest, error) {
// step 1: if we have a request object, decode it and ignore other params. If not, bind the params as usual
// we check both query and form parameters for the request object since this endpoint can be called with both GET and POST
requestObject, err := controller.resolveRequestObject(c)
if err != nil {
return nil, err
}
if requestObject != nil {
return requestObject, nil
}
// step 2: by default we assume normal GET query parameters
// step 3: if it's a POST request, we try form parameters
return controller.resolveNormalParams(c)
}
func (controller *OIDCController) resolveRequestObject(c *gin.Context) (*service.AuthorizeRequest, error) {
raw := c.Query("request")
if raw == "" && c.Request.Method == http.MethodPost {
raw = c.PostForm("request")
}
if raw == "" {
return nil, nil
}
return controller.oidc.DecodeAuthorizeJWT(raw)
}
func (controller *OIDCController) resolveNormalParams(c *gin.Context) (*service.AuthorizeRequest, error) {
var req service.AuthorizeRequest
bind := binding.Query
if c.Request.Method == http.MethodPost {
bind = binding.Form
}
if err := c.ShouldBindWith(&req, bind); err != nil {
return nil, err
}
return &req, nil
}
File diff suppressed because it is too large Load Diff
+21 -15
View File
@@ -13,6 +13,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
"github.com/google/go-querystring/query"
@@ -53,29 +54,33 @@ type ProxyContext struct {
type ProxyController struct {
log *logger.Logger
runtime model.RuntimeConfig
runtime *model.RuntimeConfig
acls *service.AccessControlsService
auth *service.AuthService
policyEngine *service.PolicyEngine
}
func NewProxyController(
log *logger.Logger,
runtime model.RuntimeConfig,
router *gin.RouterGroup,
acls *service.AccessControlsService,
auth *service.AuthService,
policyEngine *service.PolicyEngine,
) *ProxyController {
type ProxyControllerInput struct {
dig.In
Log *logger.Logger
RuntimeConfig *model.RuntimeConfig
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
ACLsService *service.AccessControlsService
AuthService *service.AuthService
PolicyEngine *service.PolicyEngine
}
func NewProxyController(i ProxyControllerInput) *ProxyController {
controller := &ProxyController{
log: log,
runtime: runtime,
acls: acls,
auth: auth,
policyEngine: policyEngine,
log: i.Log,
runtime: i.RuntimeConfig,
acls: i.ACLsService,
auth: i.AuthService,
policyEngine: i.PolicyEngine,
}
proxyGroup := router.Group("/auth")
proxyGroup := i.RouterGroup.Group("/auth")
proxyGroup.Any("/:proxy", controller.proxyHandler)
return controller
@@ -275,6 +280,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
queries, err := query.Values(RedirectQuery{
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
LoginFor: FrontendLoginForApp,
})
if err != nil {
+53 -11
View File
@@ -3,6 +3,7 @@ package controller_test
import (
"context"
"net/http/httptest"
"net/url"
"testing"
"github.com/gin-gonic/gin"
@@ -76,7 +77,9 @@ func TestProxyController(t *testing.T) {
assert.Equal(t, 307, recorder.Code)
location := recorder.Header().Get("Location")
assert.Equal(t, "https://tinyauth.example.com/login?redirect_uri=https%3A%2F%2Ftest.example.com%2F", location)
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
assert.Contains(t, location, "login_for=app")
assert.Contains(t, location, "https://tinyauth.example.com/login")
},
},
{
@@ -89,7 +92,9 @@ func TestProxyController(t *testing.T) {
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
location := recorder.Header().Get("x-tinyauth-location")
assert.Equal(t, "https://tinyauth.example.com/login?redirect_uri=https%3A%2F%2Ftest.example.com%2F", location)
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
assert.Contains(t, location, "login_for=app")
assert.Contains(t, location, "https://tinyauth.example.com/login")
},
},
{
@@ -103,7 +108,9 @@ func TestProxyController(t *testing.T) {
router.ServeHTTP(recorder, req)
assert.Equal(t, 307, recorder.Code)
location := recorder.Header().Get("Location")
assert.Equal(t, "https://tinyauth.example.com/login?redirect_uri=https%3A%2F%2Ftest.example.com%2Fhello", location)
assert.Contains(t, location, url.QueryEscape("https://test.example.com/hello"))
assert.Contains(t, location, "login_for=app")
assert.Contains(t, location, "https://tinyauth.example.com/login")
},
},
{
@@ -119,7 +126,9 @@ func TestProxyController(t *testing.T) {
assert.Equal(t, 307, recorder.Code)
location := recorder.Header().Get("Location")
assert.Equal(t, "https://tinyauth.example.com/login?redirect_uri=https%3A%2F%2Ftest.example.com%2F", location)
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
assert.Contains(t, location, "login_for=app")
assert.Contains(t, location, "https://tinyauth.example.com/login")
},
},
{
@@ -134,7 +143,9 @@ func TestProxyController(t *testing.T) {
router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
location := recorder.Header().Get("x-tinyauth-location")
assert.Equal(t, "https://tinyauth.example.com/login?redirect_uri=https%3A%2F%2Ftest.example.com%2F", location)
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
assert.Contains(t, location, "login_for=app")
assert.Contains(t, location, "https://tinyauth.example.com/login")
},
},
{
@@ -150,7 +161,9 @@ func TestProxyController(t *testing.T) {
router.ServeHTTP(recorder, req)
assert.Equal(t, 307, recorder.Code)
location := recorder.Header().Get("Location")
assert.Equal(t, "https://tinyauth.example.com/login?redirect_uri=https%3A%2F%2Ftest.example.com%2Fhello", location)
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
assert.Contains(t, location, "login_for=app")
assert.Contains(t, location, "https://tinyauth.example.com/login")
},
},
{
@@ -356,10 +369,21 @@ func TestProxyController(t *testing.T) {
ctx := context.TODO()
dg := ding.New(ctx)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
aclsService := service.NewAccessControlsService(log, cfg, nil)
broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{
Log: log,
Runtime: &runtime,
Ctx: ctx,
})
aclsService := service.NewAccessControlsService(service.AccessControlServiceInput{
Log: log,
Config: &cfg,
LabelProvider: nil,
})
policyEngine, err := service.NewPolicyEngine(cfg, log)
policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
require.NoError(t, err)
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
@@ -382,7 +406,18 @@ func TestProxyController(t *testing.T) {
Log: log,
})
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
authService := service.NewAuthService(service.AuthServiceInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
Ctx: ctx,
Ding: dg,
LDAP: nil,
Queries: store,
OAuthBroker: broker,
Tailscale: nil,
PolicyEngine: policyEngine,
})
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
@@ -397,7 +432,14 @@ func TestProxyController(t *testing.T) {
recorder := httptest.NewRecorder()
controller.NewProxyController(log, runtime, group, aclsService, authService, policyEngine)
controller.NewProxyController(controller.ProxyControllerInput{
Log: log,
RuntimeConfig: &runtime,
RouterGroup: group,
ACLsService: aclsService,
AuthService: authService,
PolicyEngine: policyEngine,
})
test.run(t, router, recorder)
})
+13 -8
View File
@@ -5,25 +5,30 @@ import (
"github.com/gin-gonic/gin"
"github.com/tinyauthapp/tinyauth/internal/model"
"go.uber.org/dig"
)
type ResourcesController struct {
config model.Config
config *model.Config
fileServer http.Handler
}
func NewResourcesController(
config model.Config,
router *gin.RouterGroup,
) *ResourcesController {
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.Resources.Path)))
type ResourcesControllerInput struct {
dig.In
RouterGroup *gin.RouterGroup `name:"mainRouterGroup"`
Config *model.Config
}
func NewResourcesController(i ResourcesControllerInput) *ResourcesController {
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(i.Config.Resources.Path)))
controller := &ResourcesController{
config: config,
config: i.Config,
fileServer: fileServer,
}
router.GET("/resources/*resource", controller.resourcesHandler)
i.RouterGroup.GET("/resources/*resource", controller.resourcesHandler)
return controller
}
@@ -69,7 +69,10 @@ func TestResourcesController(t *testing.T) {
group := router.Group("/")
gin.SetMode(gin.TestMode)
controller.NewResourcesController(cfg, group)
controller.NewResourcesController(controller.ResourcesControllerInput{
RouterGroup: group,
Config: &cfg,
})
recorder := httptest.NewRecorder()
test.run(t, router, recorder)
+16 -11
View File
@@ -11,6 +11,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
@@ -27,23 +28,27 @@ type TotpRequest struct {
type UserController struct {
log *logger.Logger
runtime model.RuntimeConfig
runtime *model.RuntimeConfig
auth *service.AuthService
}
func NewUserController(
log *logger.Logger,
runtimeConfig model.RuntimeConfig,
router *gin.RouterGroup,
auth *service.AuthService,
) *UserController {
type UserControllerInput struct {
dig.In
Log *logger.Logger
RuntimeConfig *model.RuntimeConfig
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
AuthService *service.AuthService
}
func NewUserController(i UserControllerInput) *UserController {
controller := &UserController{
log: log,
runtime: runtimeConfig,
auth: auth,
log: i.Log,
runtime: i.RuntimeConfig,
auth: i.AuthService,
}
userGroup := router.Group("/user")
userGroup := i.RouterGroup.Group("/user")
userGroup.POST("/login", controller.loginHandler)
userGroup.POST("/logout", controller.logoutHandler)
userGroup.POST("/totp", controller.totpHandler)
+27 -4
View File
@@ -414,11 +414,29 @@ func TestUserController(t *testing.T) {
ctx := context.TODO()
dg := ding.New(ctx)
policyEngine, err := service.NewPolicyEngine(cfg, log)
policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
require.NoError(t, err)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{
Log: log,
Runtime: &runtime,
Ctx: ctx,
})
authService := service.NewAuthService(service.AuthServiceInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
Ctx: ctx,
Ding: dg,
LDAP: nil,
Queries: store,
OAuthBroker: broker,
Tailscale: nil,
PolicyEngine: policyEngine,
})
beforeEach := func() {
// Clear failed login attempts before each test
@@ -437,7 +455,12 @@ func TestUserController(t *testing.T) {
group := router.Group("/api")
gin.SetMode(gin.TestMode)
controller.NewUserController(log, runtime, group, authService)
controller.NewUserController(controller.UserControllerInput{
Log: log,
RuntimeConfig: &runtime,
RouterGroup: group,
AuthService: authService,
})
recorder := httptest.NewRecorder()
+12 -4
View File
@@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/tinyauthapp/tinyauth/internal/service"
"go.uber.org/dig"
)
type OpenIDConnectConfiguration struct {
@@ -30,13 +31,20 @@ type WellKnownController struct {
oidc *service.OIDCService
}
func NewWellKnownController(oidc *service.OIDCService, router *gin.RouterGroup) *WellKnownController {
type WellKnownControllerInput struct {
dig.In
OIDCService *service.OIDCService
RouterGroup *gin.RouterGroup `name:"mainRouterGroup"`
}
func NewWellKnownController(i WellKnownControllerInput) *WellKnownController {
controller := &WellKnownController{
oidc: oidc,
oidc: i.OIDCService,
}
router.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
router.GET("/.well-known/jwks.json", controller.JWKS)
i.RouterGroup.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
i.RouterGroup.GET("/.well-known/jwks.json", controller.JWKS)
return controller
}
@@ -93,7 +93,13 @@ func TestWellKnownController(t *testing.T) {
store := memory.New()
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg)
oidcService, err := service.NewOIDCService(service.OIDCServiceInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
Queries: store,
Ding: dg,
})
require.NoError(t, err)
for _, test := range tests {
@@ -103,7 +109,10 @@ func TestWellKnownController(t *testing.T) {
recorder := httptest.NewRecorder()
controller.NewWellKnownController(oidcService, &router.RouterGroup)
controller.NewWellKnownController(controller.WellKnownControllerInput{
OIDCService: oidcService,
RouterGroup: &router.RouterGroup,
})
test.run(t, router, recorder)
})
+18 -13
View File
@@ -11,6 +11,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
)
@@ -37,25 +38,29 @@ var (
type ContextMiddleware struct {
log *logger.Logger
runtime model.RuntimeConfig
runtime *model.RuntimeConfig
auth *service.AuthService
broker *service.OAuthBrokerService
tailscale *service.TailscaleService
}
func NewContextMiddleware(
log *logger.Logger,
runtime model.RuntimeConfig,
auth *service.AuthService,
broker *service.OAuthBrokerService,
tailscale *service.TailscaleService,
) *ContextMiddleware {
type ContextMiddlewareInput struct {
dig.In
Log *logger.Logger
RuntimeConfig *model.RuntimeConfig
AuthService *service.AuthService
BrokerService *service.OAuthBrokerService
TailscaleService *service.TailscaleService
}
func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware {
return &ContextMiddleware{
log: log,
runtime: runtime,
auth: auth,
broker: broker,
tailscale: tailscale,
log: i.Log,
runtime: i.RuntimeConfig,
auth: i.AuthService,
broker: i.BrokerService,
tailscale: i.TailscaleService,
}
}
+28 -4
View File
@@ -254,13 +254,37 @@ func TestContextMiddleware(t *testing.T) {
store := memory.New()
policyEngine, err := service.NewPolicyEngine(cfg, log)
policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
require.NoError(t, err)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{
Log: log,
Runtime: &runtime,
Ctx: ctx,
})
authService := service.NewAuthService(service.AuthServiceInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
Ctx: ctx,
Ding: dg,
LDAP: nil,
Queries: store,
OAuthBroker: broker,
Tailscale: nil,
PolicyEngine: policyEngine,
})
contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil)
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareInput{
Log: log,
RuntimeConfig: &runtime,
AuthService: authService,
BrokerService: broker,
TailscaleService: nil,
})
for _, test := range tests {
authService.ClearLoginAttempts()
+8 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/tinyauthapp/tinyauth/internal/assets"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
)
@@ -18,7 +19,12 @@ type UIMiddleware struct {
uiFileServer http.Handler
}
func NewUIMiddleware() (*UIMiddleware, error) {
// for future use if we need to inject dependencies into the middleware
type UIMiddlewareInput struct {
dig.In
}
func NewUIMiddleware(_ UIMiddlewareInput) (*UIMiddleware, error) {
m := &UIMiddleware{}
ui, err := fs.Sub(assets.FrontendAssets, "dist")
@@ -38,7 +44,7 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
path := strings.TrimPrefix(c.Request.URL.Path, "/")
switch strings.SplitN(path, "/", 2)[0] {
case "api", "resources", ".well-known":
case "api", "resources", ".well-known", "authorize":
c.Next()
return
case "robots.txt":
+9 -2
View File
@@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
)
// See context middleware for explanation of why we have to do this
@@ -21,9 +22,15 @@ type ZerologMiddleware struct {
log *logger.Logger
}
func NewZerologMiddleware(log *logger.Logger) *ZerologMiddleware {
type ZerologMiddlewareInput struct {
dig.In
Log *logger.Logger
}
func NewZerologMiddleware(i ZerologMiddlewareInput) *ZerologMiddleware {
return &ZerologMiddleware{
log: log,
log: i.Log,
}
}
+10 -9
View File
@@ -178,15 +178,16 @@ type UIConfig struct {
}
type LDAPConfig struct {
Address string `description:"LDAP server address." yaml:"address"`
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
Address string `description:"LDAP server address." yaml:"address"`
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile"`
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
}
type LogConfig struct {
-1
View File
@@ -12,7 +12,6 @@ type RuntimeConfig struct {
OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string
ConfiguredProviders []Provider
OIDCClients []OIDCClientConfig
TrustedDomains []string
}
+17 -11
View File
@@ -5,6 +5,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
)
type LabelProvider interface {
@@ -13,19 +14,24 @@ type LabelProvider interface {
type AccessControlsService struct {
log *logger.Logger
config model.Config
labelProvider *LabelProvider
config *model.Config
labelProvider LabelProvider
}
func NewAccessControlsService(
log *logger.Logger,
config model.Config,
labelProvider *LabelProvider) *AccessControlsService {
type AccessControlServiceInput struct {
dig.In
Log *logger.Logger
Config *model.Config
LabelProvider LabelProvider `optional:"true"`
}
func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsService {
return &AccessControlsService{
log: log,
config: config,
labelProvider: labelProvider,
log: i.Log,
config: i.Config,
labelProvider: i.LabelProvider,
}
}
@@ -57,8 +63,8 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A
}
// If we have a label provider configured, try to get ACLs from it
if service.labelProvider != nil && *service.labelProvider != nil {
return (*service.labelProvider).GetLabels(domain)
if service.labelProvider != nil {
return service.labelProvider.GetLabels(domain)
}
// no labels
@@ -87,7 +87,11 @@ func TestLookupStaticACLs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := NewAccessControlsService(log, model.Config{Apps: tt.apps}, nil)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &model.Config{Apps: tt.apps},
LabelProvider: nil,
})
got := svc.lookupStaticACLs(tt.domain)
if tt.expectNil {
assert.Nil(t, got)
@@ -112,7 +116,11 @@ func TestGetAccessControls(t *testing.T) {
},
},
}
svc := NewAccessControlsService(log, config, nil)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &config,
LabelProvider: nil,
})
got, err := svc.GetAccessControls("foo.example.com")
@@ -123,7 +131,11 @@ func TestGetAccessControls(t *testing.T) {
})
t.Run("returns nil when no static match and no label provider", func(t *testing.T) {
svc := NewAccessControlsService(log, model.Config{}, nil)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &model.Config{},
LabelProvider: nil,
})
got, err := svc.GetAccessControls("unknown.example.com")
@@ -133,7 +145,11 @@ func TestGetAccessControls(t *testing.T) {
t.Run("returns nil when label provider pointer wraps a nil interface", func(t *testing.T) {
var provider LabelProvider
svc := NewAccessControlsService(log, model.Config{}, &provider)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &model.Config{},
LabelProvider: provider, // nil provider
})
got, err := svc.GetAccessControls("unknown.example.com")
@@ -152,7 +168,11 @@ func TestGetAccessControls(t *testing.T) {
},
}
var provider LabelProvider = mock
svc := NewAccessControlsService(log, model.Config{}, &provider)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &model.Config{},
LabelProvider: provider,
})
got, err := svc.GetAccessControls("dynamic.example.com")
@@ -170,7 +190,11 @@ func TestGetAccessControls(t *testing.T) {
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
},
}
svc := NewAccessControlsService(log, config, &provider)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &config,
LabelProvider: provider,
})
got, err := svc.GetAccessControls("foo.example.com")
@@ -188,7 +212,11 @@ func TestGetAccessControls(t *testing.T) {
},
}
var provider LabelProvider = mock
svc := NewAccessControlsService(log, model.Config{}, &provider)
svc := NewAccessControlsService(AccessControlServiceInput{
Log: log,
Config: &model.Config{},
LabelProvider: provider,
})
got, err := svc.GetAccessControls("dynamic.example.com")
+42 -40
View File
@@ -14,6 +14,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
@@ -30,17 +31,14 @@ var (
ErrUserNotFound = errors.New("user not found")
)
// 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"`
// We either store params for redirecting to an app after OAuth login,
// or for redirecting back to the authorize screen to continue OIDC
type OAuthCallbackParams struct {
LoginFor string `form:"login_for" url:"login_for"`
OIDCTicket string `form:"oidc_ticket" url:"oidc_ticket"`
OIDCScope string `form:"oidc_scope" url:"oidc_scope"`
OIDCName string `form:"oidc_name" url:"oidc_name"`
RedirectURI string `form:"redirect_uri" url:"redirect_uri"`
}
type OAuthPendingSession struct {
@@ -49,7 +47,7 @@ type OAuthPendingSession struct {
Token *oauth2.Token
Service *OAuthServiceImpl
ExpiresAt time.Time
CallbackParams OAuthURLParams
CallbackParams OAuthCallbackParams
}
type LoginAttempt struct {
@@ -60,8 +58,8 @@ type LoginAttempt struct {
type AuthService struct {
log *logger.Logger
config model.Config
runtime model.RuntimeConfig
config *model.Config
runtime *model.RuntimeConfig
ctx context.Context
ldap *LdapService
@@ -85,28 +83,32 @@ type AuthService struct {
}
}
func NewAuthService(
log *logger.Logger,
config model.Config,
runtime model.RuntimeConfig,
ctx context.Context,
dg *ding.Ding,
ldap *LdapService,
queries repository.Store,
oauthBroker *OAuthBrokerService,
tailscale *TailscaleService,
policy *PolicyEngine,
) *AuthService {
type AuthServiceInput struct {
dig.In
Log *logger.Logger
Config *model.Config
Runtime *model.RuntimeConfig
Ctx context.Context
Ding *ding.Ding
LDAP *LdapService `optional:"true"`
Queries repository.Store
OAuthBroker *OAuthBrokerService
Tailscale *TailscaleService `optional:"true"`
PolicyEngine *PolicyEngine
}
func NewAuthService(i AuthServiceInput) *AuthService {
service := &AuthService{
log: log,
runtime: runtime,
ctx: ctx,
config: config,
ldap: ldap,
queries: queries,
oauthBroker: oauthBroker,
tailscale: tailscale,
policyEngine: policy,
log: i.Log,
runtime: i.Runtime,
ctx: i.Ctx,
config: i.Config,
ldap: i.LDAP,
queries: i.Queries,
oauthBroker: i.OAuthBroker,
tailscale: i.Tailscale,
policyEngine: i.PolicyEngine,
}
// caches setup
@@ -118,7 +120,7 @@ func NewAuthService(
service.caches.login = loginCache
service.caches.ldap = ldapCache
dg.Go(func(ctx context.Context) {
i.Ding.Go(func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
@@ -516,17 +518,17 @@ func (auth *AuthService) LDAPAuthConfigured() bool {
return auth.ldap != nil
}
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthCallbackParams) (string, error) {
service, ok := auth.oauthBroker.GetService(serviceName)
if !ok {
return "", OAuthPendingSession{}, fmt.Errorf("oauth service not found: %s", serviceName)
return "", fmt.Errorf("oauth service not found: %s", serviceName)
}
sessionId, err := uuid.NewRandom()
if err != nil {
return "", OAuthPendingSession{}, fmt.Errorf("failed to generate session ID: %w", err)
return "", fmt.Errorf("failed to generate session ID: %w", err)
}
state := service.NewRandom()
@@ -542,7 +544,7 @@ func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLPara
auth.caches.oauth.Set(sessionId.String(), session, time.Minute*10)
return sessionId.String(), session, nil
return sessionId.String(), nil
}
func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
+16 -1
View File
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
)
@@ -12,9 +13,22 @@ func TestIsEmailWhitelistedUsesProviderSpecificList(t *testing.T) {
log := logger.NewLogger().WithTestConfig()
log.Init()
policyEngine, err := NewPolicyEngine(PolicyEngineInput{
Log: log,
Config: &model.Config{
Auth: model.AuthConfig{
ACLs: model.ACLsConfig{
Policy: string(PolicyAllow),
},
},
},
})
require.NoError(t, err)
auth := &AuthService{
log: log,
runtime: model.RuntimeConfig{
runtime: &model.RuntimeConfig{
OAuthWhitelist: []string{"global@example.com"},
OAuthProviders: map[string]model.OAuthServiceConfig{
"github": {
@@ -28,6 +42,7 @@ func TestIsEmailWhitelistedUsesProviderSpecificList(t *testing.T) {
},
},
},
policyEngine: policyEngine,
}
assert.True(t, auth.IsEmailWhitelisted("github", "github@example.com"))
+16 -11
View File
@@ -8,6 +8,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
@@ -21,36 +22,40 @@ type DockerService struct {
isConnected bool
}
func NewDockerService(
log *logger.Logger,
ctx context.Context,
dg *ding.Ding,
) (*DockerService, error) {
type DockerServiceInput struct {
dig.In
Log *logger.Logger
Ctx context.Context
Ding *ding.Ding
}
func NewDockerService(i DockerServiceInput) (*DockerService, error) {
client, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
client.NegotiateAPIVersion(ctx)
client.NegotiateAPIVersion(i.Ctx)
_, err = client.Ping(ctx)
_, err = client.Ping(i.Ctx)
if err != nil {
log.App.Debug().Err(err).Msg("Docker not connected")
i.Log.App.Debug().Err(err).Msg("Docker not connected")
return nil, nil
}
service := &DockerService{
log: log,
log: i.Log,
client: client,
context: ctx,
context: i.Ctx,
}
service.isConnected = true
service.log.App.Debug().Msg("Docker connected successfully")
dg.Go(service.watchAndClose, ding.RingMajor)
i.Ding.Go(service.watchAndClose, ding.RingMajor)
return service, nil
}
+16 -11
View File
@@ -12,6 +12,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -48,11 +49,15 @@ type KubernetesService struct {
appNameIndex map[string]ingressAppKey
}
func NewKubernetesService(
log *logger.Logger,
ctx context.Context,
dg *ding.Ding,
) (*KubernetesService, error) {
type KubernetesServiceInput struct {
dig.In
Log *logger.Logger
Ctx context.Context
Ding *ding.Ding
}
func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error) {
cfg, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed to get in-cluster kubernetes config: %w", err)
@@ -69,31 +74,31 @@ func NewKubernetesService(
Resource: "ingresses",
}
accessCtx, accessCancel := context.WithTimeout(ctx, 5*time.Second)
accessCtx, accessCancel := context.WithTimeout(i.Ctx, 5*time.Second)
defer accessCancel()
_, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
if err != nil {
log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled")
i.Log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled")
return nil, fmt.Errorf("failed to access ingress api: %w", err)
}
log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher")
i.Log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher")
service := &KubernetesService{
log: log,
log: i.Log,
client: client,
ingressApps: make(map[ingressKey][]ingressApp),
domainIndex: make(map[string]ingressAppKey),
appNameIndex: make(map[string]ingressAppKey),
}
dg.Go(func(ctx context.Context) {
i.Ding.Go(func(ctx context.Context) {
service.watchGVR(gvr, ctx)
}, ding.RingMajor)
service.started = true
log.App.Debug().Msg("Kubernetes label provider started successfully")
i.Log.App.Debug().Msg("Kubernetes label provider started successfully")
return service, nil
}
+26 -17
View File
@@ -11,41 +11,50 @@ import (
ldapgo "github.com/go-ldap/ldap/v3"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
)
type LdapService struct {
log *logger.Logger
config model.Config
config *model.Config
conn *ldapgo.Conn
mutex sync.RWMutex
cert *tls.Certificate
conn *ldapgo.Conn
mutex sync.RWMutex
cert *tls.Certificate
bindPw string
}
func NewLdapService(
log *logger.Logger,
config model.Config,
dg *ding.Ding,
) (*LdapService, error) {
if config.LDAP.Address == "" {
type LdapServiceInput struct {
dig.In
Log *logger.Logger
Config *model.Config
Ding *ding.Ding
}
func NewLdapService(i LdapServiceInput) (*LdapService, error) {
if i.Config.LDAP.Address == "" {
return nil, nil
}
ldap := &LdapService{
log: log,
config: config,
log: i.Log,
config: i.Config,
}
ldap.bindPw = utils.GetSecret(i.Config.LDAP.BindPassword, i.Config.LDAP.BindPasswordFile)
// Check whether authentication with client certificate is possible
if config.LDAP.AuthCert != "" && config.LDAP.AuthKey != "" {
cert, err := tls.LoadX509KeyPair(config.LDAP.AuthCert, config.LDAP.AuthKey)
if i.Config.LDAP.AuthCert != "" && i.Config.LDAP.AuthKey != "" {
cert, err := tls.LoadX509KeyPair(i.Config.LDAP.AuthCert, i.Config.LDAP.AuthKey)
if err != nil {
return nil, fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
}
log.App.Info().Msg("LDAP mTLS authentication configured successfully")
i.Log.App.Info().Msg("LDAP mTLS authentication configured successfully")
ldap.cert = &cert
@@ -67,7 +76,7 @@ func NewLdapService(
return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
}
dg.Go(func(ctx context.Context) {
i.Ding.Go(func(ctx context.Context) {
ldap.log.App.Debug().Msg("Starting LDAP connection heartbeat routine")
ticker := time.NewTicker(5 * time.Minute)
@@ -212,7 +221,7 @@ func (ldap *LdapService) BindService(rebind bool) error {
if ldap.cert != nil {
return ldap.conn.ExternalBind()
}
return ldap.conn.Bind(ldap.config.LDAP.BindDN, ldap.config.LDAP.BindPassword)
return ldap.conn.Bind(ldap.config.LDAP.BindDN, ldap.bindPw)
}
func (ldap *LdapService) Bind(userDN string, password string) error {
+15 -10
View File
@@ -5,6 +5,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"slices"
@@ -32,23 +33,27 @@ var presets = map[string]func(config model.OAuthServiceConfig, ctx context.Conte
"google": newGoogleOAuthService,
}
func NewOAuthBrokerService(
log *logger.Logger,
configs map[string]model.OAuthServiceConfig,
ctx context.Context,
) *OAuthBrokerService {
type OAuthBrokerServiceInput struct {
dig.In
Log *logger.Logger
Runtime *model.RuntimeConfig
Ctx context.Context
}
func NewOAuthBrokerService(i OAuthBrokerServiceInput) *OAuthBrokerService {
service := &OAuthBrokerService{
log: log,
log: i.Log,
services: make(map[string]OAuthServiceImpl),
configs: configs,
configs: i.Runtime.OAuthProviders,
}
for name, cfg := range configs {
for name, cfg := range service.configs {
if presetFunc, exists := presets[name]; exists {
service.services[name] = presetFunc(cfg, ctx)
service.services[name] = presetFunc(cfg, i.Ctx)
service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset")
} else {
service.services[name] = NewOAuthService(cfg, name, ctx)
service.services[name] = NewOAuthService(cfg, name, i.Ctx)
service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from custom config")
}
}
+131 -46
View File
@@ -14,17 +14,20 @@ import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"slices"
"github.com/go-jose/go-jose/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
)
var (
@@ -106,14 +109,14 @@ type TokenResponse struct {
}
type AuthorizeRequest struct {
Scope string `json:"scope" binding:"required"`
ResponseType string `json:"response_type" binding:"required"`
ClientID string `json:"client_id" binding:"required"`
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"`
Scope string `form:"scope" json:"scope" url:"scope"`
ResponseType string `form:"response_type" json:"response_type" url:"response_type"`
ClientID string `form:"client_id" json:"client_id" url:"client_id"`
RedirectURI string `form:"redirect_uri" json:"redirect_uri" url:"redirect_uri"`
State string `form:"state" json:"state" url:"state"`
Nonce string `form:"nonce" json:"nonce" url:"nonce"`
CodeChallenge string `form:"code_challenge" json:"code_challenge" url:"code_challenge"`
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" url:"code_challenge_method"`
}
type AuthorizeCodeEntry struct {
@@ -132,8 +135,8 @@ type UsedCodeEntry struct {
type OIDCService struct {
log *logger.Logger
config model.Config
runtime model.RuntimeConfig
config *model.Config
runtime *model.RuntimeConfig
queries repository.Store
clients map[string]model.OIDCClientConfig
@@ -142,24 +145,30 @@ type OIDCService struct {
issuer string
caches struct {
code *CacheStore[AuthorizeCodeEntry]
usedCode *CacheStore[UsedCodeEntry]
code *CacheStore[AuthorizeCodeEntry]
usedCode *CacheStore[UsedCodeEntry]
authorize *CacheStore[AuthorizeRequest]
}
}
func NewOIDCService(
log *logger.Logger,
config model.Config,
runtime model.RuntimeConfig,
queries repository.Store,
dg *ding.Ding) (*OIDCService, error) {
type OIDCServiceInput struct {
dig.In
Log *logger.Logger
Config *model.Config
Runtime *model.RuntimeConfig
Queries repository.Store
Ding *ding.Ding
}
func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
// If not configured, skip init
if len(runtime.OIDCClients) == 0 {
if len(i.Config.OIDC.Clients) == 0 {
return nil, nil
}
// Ensure issuer is https
uissuer, err := url.Parse(runtime.AppURL)
uissuer, err := url.Parse(i.Runtime.AppURL)
if err != nil {
return nil, fmt.Errorf("failed to parse app url: %w", err)
@@ -172,14 +181,14 @@ func NewOIDCService(
issuer := fmt.Sprintf("%s://%s", uissuer.Scheme, uissuer.Host)
// Create/load private and public keys
if strings.TrimSpace(config.OIDC.PrivateKeyPath) == "" ||
strings.TrimSpace(config.OIDC.PublicKeyPath) == "" {
if strings.TrimSpace(i.Config.OIDC.PrivateKeyPath) == "" ||
strings.TrimSpace(i.Config.OIDC.PublicKeyPath) == "" {
return nil, errors.New("private key path and public key path are required")
}
var privateKey *rsa.PrivateKey
fprivateKey, err := os.ReadFile(config.OIDC.PrivateKeyPath)
fprivateKey, err := os.ReadFile(i.Config.OIDC.PrivateKeyPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
@@ -198,8 +207,12 @@ func NewOIDCService(
Type: "RSA PRIVATE KEY",
Bytes: der,
})
log.App.Trace().Str("type", "RSA PRIVATE KEY").Msg("Generated private RSA key")
err = os.WriteFile(config.OIDC.PrivateKeyPath, encoded, 0600)
i.Log.App.Trace().Str("type", "RSA PRIVATE KEY").Msg("Generated private RSA key")
err := os.MkdirAll(filepath.Dir(i.Config.OIDC.PrivateKeyPath), 0700)
if err != nil {
return nil, fmt.Errorf("failed to create directory for private key: %w", err)
}
err = os.WriteFile(i.Config.OIDC.PrivateKeyPath, encoded, 0600)
if err != nil {
return nil, fmt.Errorf("failed to write private key to file: %w", err)
}
@@ -208,7 +221,7 @@ func NewOIDCService(
if block == nil {
return nil, errors.New("failed to decode private key")
}
log.App.Trace().Str("type", block.Type).Msg("Loaded private key")
i.Log.App.Trace().Str("type", block.Type).Msg("Loaded private key")
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
@@ -217,7 +230,7 @@ func NewOIDCService(
var publicKey crypto.PublicKey
fpublicKey, err := os.ReadFile(config.OIDC.PublicKeyPath)
fpublicKey, err := os.ReadFile(i.Config.OIDC.PublicKeyPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("failed to read public key: %w", err)
@@ -233,8 +246,12 @@ func NewOIDCService(
Type: "RSA PUBLIC KEY",
Bytes: der,
})
log.App.Trace().Str("type", "RSA PUBLIC KEY").Msg("Generated public RSA key")
err = os.WriteFile(config.OIDC.PublicKeyPath, encoded, 0644)
i.Log.App.Trace().Str("type", "RSA PUBLIC KEY").Msg("Generated public RSA key")
err := os.MkdirAll(filepath.Dir(i.Config.OIDC.PublicKeyPath), 0700)
if err != nil {
return nil, fmt.Errorf("failed to create directory for public key: %w", err)
}
err = os.WriteFile(i.Config.OIDC.PublicKeyPath, encoded, 0644)
if err != nil {
return nil, err
}
@@ -243,7 +260,7 @@ func NewOIDCService(
if block == nil {
return nil, errors.New("failed to decode public key")
}
log.App.Trace().Str("type", block.Type).Msg("Loaded public key")
i.Log.App.Trace().Str("type", block.Type).Msg("Loaded public key")
switch block.Type {
case "RSA PUBLIC KEY":
publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
@@ -273,7 +290,7 @@ func NewOIDCService(
// We will reorganize the client into a map with the client ID as the key
clients := make(map[string]model.OIDCClientConfig)
for id, client := range config.OIDC.Clients {
for id, client := range i.Config.OIDC.Clients {
client.ID = id
if client.Name == "" {
client.Name = utils.Capitalize(client.ID)
@@ -289,15 +306,15 @@ func NewOIDCService(
}
client.ClientSecretFile = ""
clients[id] = client
log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration")
i.Log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration")
}
// Initialize the service
service := &OIDCService{
log: log,
config: config,
runtime: runtime,
queries: queries,
log: i.Log,
config: i.Config,
runtime: i.Runtime,
queries: i.Queries,
clients: clients,
privateKey: privateKey,
@@ -306,16 +323,19 @@ func NewOIDCService(
}
// Start cleanup routine
dg.Go(service.cleanupRoutine, ding.RingMinor)
i.Ding.Go(service.cleanupRoutine, ding.RingMinor)
// Create caches
codeCash := NewCacheStore[AuthorizeCodeEntry](256)
usedCode := NewCacheStore[UsedCodeEntry](256)
authorize := NewCacheStore[AuthorizeRequest](256)
service.caches.code = codeCash
service.caches.usedCode = usedCode
service.caches.authorize = authorize
// Start cache cleanup routine
dg.Go(func(ctx context.Context) {
i.Ding.Go(func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
@@ -324,6 +344,7 @@ func NewOIDCService(
case <-ticker.C:
service.caches.code.Sweep()
service.caches.usedCode.Sweep()
service.caches.authorize.Sweep()
case <-ctx.Done():
return
}
@@ -465,19 +486,29 @@ func (service *OIDCService) ValidateGrantType(grantType string) error {
}
func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*AuthorizeCodeEntry, bool) {
entry, ok := service.caches.code.Get(codeHash)
var entry AuthorizeCodeEntry
var ok bool
service.caches.code.WithLock(func(actions CacheStoreActions[AuthorizeCodeEntry]) {
entry, ok = actions.Get(codeHash)
if !ok {
return
}
if entry.ClientID != clientId {
ok = false
return
}
// Since the code can only be used once, we delete it from the cache after retrieving it
actions.Delete(codeHash)
})
if !ok {
return nil, false
}
if entry.ClientID != clientId {
return nil, false
}
// Since the code can only be used once, we delete it from the cache after retrieving it
service.caches.code.Delete(codeHash)
return &entry, true
}
@@ -846,3 +877,57 @@ func (service *OIDCService) MarkCodeAsUsed(codeHash string, sub string) {
func (service *OIDCService) DeleteSessionBySub(ctx context.Context, sub string) error {
return service.queries.DeleteOIDCSessionBySub(ctx, sub)
}
func (service *OIDCService) CreateAuthorizeRequestTicket(req AuthorizeRequest) string {
ticket := utils.GenerateString(32)
service.caches.authorize.Set(ticket, req, 10*time.Minute)
return ticket
}
func (service *OIDCService) GetAuthorizeRequestByTicket(ticket string) (*AuthorizeRequest, bool) {
entry, ok := service.caches.authorize.Get(ticket)
if !ok {
return nil, false
}
return &entry, true
}
func (service *OIDCService) DeleteAuthorizeRequestTicket(ticket string) {
service.caches.authorize.Delete(ticket)
}
// TODO: support signed request objects in the future
func (service *OIDCService) DecodeAuthorizeJWT(tokenString string) (*AuthorizeRequest, error) {
var claims jwt.MapClaims
token, _, err := jwt.NewParser().ParseUnverified(tokenString, &claims)
if err != nil {
return nil, fmt.Errorf("failed to parse authorize request jwt: %w", err)
}
alg, ok := token.Header["alg"].(string)
if !ok || alg != "none" || string(token.Signature) != "" {
return nil, fmt.Errorf("only unsigned jwts are supported for authorize requests")
}
get := func(k string) string {
v, _ := claims[k].(string)
return v
}
return &AuthorizeRequest{
Scope: get("scope"),
ResponseType: get("response_type"),
ClientID: get("client_id"),
RedirectURI: get("redirect_uri"),
State: get("state"),
Nonce: get("nonce"),
CodeChallenge: get("code_challenge"),
CodeChallengeMethod: get("code_challenge_method"),
}, nil
}
+10 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
)
@@ -67,7 +68,15 @@ func TestCompileUserinfo(t *testing.T) {
ctx := context.TODO()
dg := ding.New(ctx)
svc, err := service.NewOIDCService(log, cfg, runtime, nil, dg)
store := memory.New()
svc, err := service.NewOIDCService(service.OIDCServiceInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
Queries: store,
Ding: dg,
})
require.NoError(t, err)
type testCase struct {
+14 -6
View File
@@ -6,6 +6,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
)
type Policy string
@@ -40,21 +41,28 @@ type PolicyEngine struct {
policy Policy
}
func NewPolicyEngine(config model.Config, log *logger.Logger) (*PolicyEngine, error) {
type PolicyEngineInput struct {
dig.In
Log *logger.Logger
Config *model.Config
}
func NewPolicyEngine(i PolicyEngineInput) (*PolicyEngine, error) {
engine := PolicyEngine{
log: log,
log: i.Log,
rules: make(map[RuleName]Rule),
}
switch config.Auth.ACLs.Policy {
switch i.Config.Auth.ACLs.Policy {
case string(PolicyAllow):
log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
i.Log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
engine.policy = PolicyAllow
case string(PolicyDeny):
log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
i.Log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
engine.policy = PolicyDeny
default:
return nil, fmt.Errorf("invalid acl policy: %s", config.Auth.ACLs.Policy)
return nil, fmt.Errorf("invalid acl policy: %s", i.Config.Auth.ACLs.Policy)
}
return &engine, nil
+24 -6
View File
@@ -33,23 +33,35 @@ func TestPolicyEngine(t *testing.T) {
// Engine should fail with invalid policy
cfg.Auth.ACLs.Policy = "invalid_policy"
_, err := service.NewPolicyEngine(cfg, log)
_, err := service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
assert.Error(t, err)
// Engine should initialize with 'allow' policy
cfg.Auth.ACLs.Policy = string(service.PolicyAllow)
engine, err := service.NewPolicyEngine(cfg, log)
engine, err := service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
assert.NoError(t, err)
assert.Equal(t, service.PolicyAllow, engine.Policy())
// Engine should initialize with 'deny' policy
cfg.Auth.ACLs.Policy = string(service.PolicyDeny)
engine, err = service.NewPolicyEngine(cfg, log)
engine, err = service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
assert.NoError(t, err)
assert.Equal(t, service.PolicyDeny, engine.Policy())
// Engine should allow adding rules
engine, err = service.NewPolicyEngine(cfg, log)
engine, err = service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
assert.NoError(t, err)
engine.RegisterRule("test-rule", testRule)
_, ok := engine.Rules()["test-rule"]
@@ -57,7 +69,10 @@ func TestPolicyEngine(t *testing.T) {
// Begin allow policy tests
cfg.Auth.ACLs.Policy = string(service.PolicyAllow)
engine, err = service.NewPolicyEngine(cfg, log)
engine, err = service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
assert.NoError(t, err)
engine.RegisterRule("test-rule", testRule)
@@ -75,7 +90,10 @@ func TestPolicyEngine(t *testing.T) {
// Begin deny policy tests
cfg.Auth.ACLs.Policy = string(service.PolicyDeny)
engine, err = service.NewPolicyEngine(cfg, log)
engine, err = service.NewPolicyEngine(service.PolicyEngineInput{
Log: log,
Config: &cfg,
})
assert.NoError(t, err)
engine.RegisterRule("test-rule", testRule)
+24 -14
View File
@@ -12,6 +12,7 @@ import (
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"go.uber.org/dig"
"tailscale.com/client/local"
"tailscale.com/tsnet"
)
@@ -25,7 +26,7 @@ type TailscaleWhoisResponse struct {
type TailscaleService struct {
log *logger.Logger
config model.Config
config *model.Config
ctx context.Context
srv *tsnet.Server
@@ -34,22 +35,31 @@ type TailscaleService struct {
mu sync.Mutex
}
func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, dg *ding.Ding) (*TailscaleService, error) {
if !config.Tailscale.Enabled {
type TailscaleServiceInput struct {
dig.In
Log *logger.Logger
Config *model.Config
Ctx context.Context
Ding *ding.Ding
}
func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
if !i.Config.Tailscale.Enabled {
return nil, nil
}
srv := new(tsnet.Server)
// node options
srv.Dir = config.Tailscale.Dir
srv.Hostname = config.Tailscale.Hostname
srv.AuthKey = config.Tailscale.AuthKey
srv.Ephemeral = config.Tailscale.Ephemeral
srv.Dir = i.Config.Tailscale.Dir
srv.Hostname = i.Config.Tailscale.Hostname
srv.AuthKey = i.Config.Tailscale.AuthKey
srv.Ephemeral = i.Config.Tailscale.Ephemeral
// redirect logs to zerolog
srv.Logf = log.App.Printf
srv.UserLogf = log.App.Printf
srv.Logf = i.Log.App.Printf
srv.UserLogf = i.Log.App.Printf
err := srv.Start()
@@ -65,14 +75,14 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co
}
service := &TailscaleService{
log: log,
config: config,
ctx: ctx,
log: i.Log,
config: i.Config,
ctx: i.Ctx,
srv: srv,
lc: lc,
}
connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed
connectCtx, cancel := context.WithTimeout(i.Ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed
defer cancel()
err = service.waitForConn(connectCtx)
@@ -82,7 +92,7 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co
return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
}
dg.Go(service.watchAndClose, ding.RingMajor)
i.Ding.Go(service.watchAndClose, ding.RingMajor)
return service, nil
}
-8
View File
@@ -121,14 +121,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
CookieDomain: "example.com",
AppURL: "https://tinyauth.example.com",
SessionCookieName: "tinyauth-session",
OIDCClients: func() []model.OIDCClientConfig {
var clients []model.OIDCClientConfig
for id, client := range config.OIDC.Clients {
client.ID = id
clients = append(clients, client)
}
return clients
}(),
}
return config, runtime