mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-09-01 02:53:30 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8624bb9562 |
@@ -235,8 +235,6 @@ TINYAUTH_LDAP_GROUPCACHETTL=900
|
||||
|
||||
# Enable the OAuth bridge, uses a new way to format OAuth user information.
|
||||
TINYAUTH_EXPERIMENTAL_OAUTHBRIDGEENABLED=false
|
||||
# Disable the fallback to forward_auth modules when auth_request or ext_authz fail.
|
||||
TINYAUTH_EXPERIMENTAL_DISABLEAUTHMODULEFALLBACK=false
|
||||
|
||||
# tailscale config
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: Run e2e tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -38,6 +38,6 @@ jobs:
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ COPY ./frontend/vite.config.ts ./
|
||||
RUN pnpm run build
|
||||
|
||||
# Builder
|
||||
FROM golang:1.26-alpine3.23 AS builder
|
||||
FROM golang:1.27-alpine3.23 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26-alpine3.23
|
||||
FROM golang:1.27-alpine3.23
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ COPY ./frontend/vite.config.ts ./
|
||||
RUN pnpm run build
|
||||
|
||||
# Builder
|
||||
FROM golang:1.26-alpine3.23 AS builder
|
||||
FROM golang:1.27-alpine3.23 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
|
||||
@@ -81,6 +81,7 @@ A huge thank you to [selfh.st](https://selfh.st) for their generous donation to
|
||||
|
||||
- [JetBrains for Open-Source](https://jb.gg/OpenSource)
|
||||
- [CodeRabbit AI](https://www.coderabbit.ai)
|
||||
- [Dosu](https://dosu.dev/)
|
||||
- [InstaPods](https://instapods.com) - [Deploy Tinyauth from 3$/month](https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
+15
-15
@@ -12,27 +12,27 @@
|
||||
},
|
||||
"packageManager": "pnpm@11.1.2",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.9.1",
|
||||
"@hookform/resolvers": "^5.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.24",
|
||||
"@radix-ui/react-label": "^2.1.15",
|
||||
"@radix-ui/react-select": "^2.3.7",
|
||||
"@radix-ui/react-separator": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.3.3",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/react-query": "^5.102.8",
|
||||
"axios": "^1.20.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"axios": "^1.19.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^26.4.0",
|
||||
"i18next": "^26.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.3",
|
||||
"lucide-react": "^1.35.0",
|
||||
"lucide-react": "^1.31.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hook-form": "^7.86.0",
|
||||
"react-i18next": "^17.0.12",
|
||||
"react-hook-form": "^7.85.0",
|
||||
"react-i18next": "^17.0.11",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^8.3.0",
|
||||
"sonner": "^2.0.8",
|
||||
@@ -42,19 +42,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.102.8",
|
||||
"@types/node": "^26.4.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.101.4",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"eslint": "^10.9.1",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.5",
|
||||
"eslint-plugin-react-refresh": "^0.5.4",
|
||||
"globals": "^17.11.0",
|
||||
"rollup-plugin-visualizer": "^7.1.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.68.0",
|
||||
"vite": "^8.2.2"
|
||||
"typescript-eslint": "^8.67.0",
|
||||
"vite": "^8.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+562
-487
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { useUserContext } from "@/context/user-context";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Navigate, useNavigate } from "react-router";
|
||||
import { useLocation } from "react-router";
|
||||
import {
|
||||
@@ -25,8 +25,7 @@ import {
|
||||
searchParamsFromObject,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
import {useEffect, useState} from "react";
|
||||
import { z } from "zod";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type Scope = {
|
||||
id: string;
|
||||
@@ -35,10 +34,6 @@ type Scope = {
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
const skipConsentResponseSchema = z.object({
|
||||
skipConsent: z.boolean(),
|
||||
})
|
||||
|
||||
const scopeMapIconProps = {
|
||||
className: "stroke-muted-foreground stroke-[1.75] h-4",
|
||||
};
|
||||
@@ -101,8 +96,14 @@ export const AuthorizePage = () => {
|
||||
}
|
||||
return "";
|
||||
})();
|
||||
const [autoAuthorize, setAutoAuthorize] = useState(false);
|
||||
const [skipConsentChecked, setSkipConsentChecked] = useState(false);
|
||||
|
||||
// TODO: maybe a better way to do this
|
||||
const shouldAutoAuthorize =
|
||||
auth.authenticated &&
|
||||
isOidc &&
|
||||
screenParams.oidc_ticket !== undefined &&
|
||||
screenParams.oidc_scope !== undefined &&
|
||||
screenParams.oidc_prompt === "none";
|
||||
|
||||
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
|
||||
mutationFn: () => {
|
||||
@@ -125,34 +126,10 @@ export const AuthorizePage = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const controller = new AbortController();
|
||||
|
||||
const checkSkipConsent = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/oidc/skip-consent?oidc_ticket=${encodeURIComponent( screenParams.oidc_ticket ?? "")}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const parsed = skipConsentResponseSchema.safeParse(await res.json());
|
||||
if (!active || !parsed.success || !parsed.data.skipConsent) return;
|
||||
setAutoAuthorize(true);
|
||||
authorizeMutate();
|
||||
} catch {
|
||||
// Fall back to manual consent on any failure (including abort).
|
||||
} finally {
|
||||
if (active) setSkipConsentChecked(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkSkipConsent();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
controller.abort();
|
||||
};
|
||||
}, [authorizeMutate, screenParams.oidc_ticket]);
|
||||
if (shouldAutoAuthorize) {
|
||||
authorizeMutate();
|
||||
}
|
||||
}, [shouldAutoAuthorize, authorizeMutate]);
|
||||
|
||||
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
|
||||
return (
|
||||
@@ -213,13 +190,13 @@ export const AuthorizePage = () => {
|
||||
<CardFooter className="flex flex-col items-stretch gap-3">
|
||||
<Button
|
||||
onClick={() => authorizeMutate()}
|
||||
loading={authorizePending || autoAuthorize || !skipConsentChecked}
|
||||
loading={authorizePending || shouldAutoAuthorize}
|
||||
>
|
||||
{t("authorizeTitle")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(`/logout${compiledParams}`)}
|
||||
disabled={authorizePending || autoAuthorize}
|
||||
disabled={authorizePending || shouldAutoAuthorize}
|
||||
variant="outline"
|
||||
>
|
||||
{t("cancelTitle")}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
plugins: [react(), tailwindcss(), visualizer()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(import.meta.dirname, "./src"),
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -28,9 +28,9 @@ require (
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/tools v0.49.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.37.0
|
||||
k8s.io/client-go v0.37.0
|
||||
modernc.org/sqlite v1.57.0
|
||||
k8s.io/apimachinery v0.36.3
|
||||
k8s.io/client-go v0.36.3
|
||||
modernc.org/sqlite v1.56.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -70,12 +70,13 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
@@ -116,6 +117,7 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // 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
|
||||
@@ -129,7 +131,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
@@ -138,19 +140,19 @@ require (
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||
modernc.org/libc v1.74.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -105,8 +105,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
@@ -124,34 +124,12 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
|
||||
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
|
||||
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
|
||||
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
|
||||
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
|
||||
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
|
||||
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
|
||||
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
|
||||
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
|
||||
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
|
||||
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
|
||||
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
|
||||
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -210,6 +188,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
@@ -224,6 +204,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
@@ -349,8 +331,8 @@ 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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
@@ -373,8 +355,8 @@ golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
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.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
@@ -398,18 +380,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4=
|
||||
k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk=
|
||||
k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
|
||||
k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
|
||||
k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
|
||||
k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
|
||||
k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
|
||||
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
|
||||
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
|
||||
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
|
||||
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
|
||||
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
@@ -432,8 +414,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.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
|
||||
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
|
||||
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||
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=
|
||||
@@ -444,7 +426,7 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 h1:u08YRbVUi59ri4YD6cg0UqNM4Dimn0sIl+wldcx5PYw=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -18,8 +18,3 @@ type RedirectQuery struct {
|
||||
RedirectURI string `url:"redirect_uri"`
|
||||
LoginFor FrontendLoginFor `url:"login_for"`
|
||||
}
|
||||
|
||||
type SimpleResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@@ -294,9 +294,7 @@ func (controller *OAuthController) getCookieDomain() string {
|
||||
|
||||
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
|
||||
v := validators.NewDomainValidator(validators.DomainValidatorOptions{
|
||||
WithPort: true,
|
||||
WithScheme: true,
|
||||
AllowedSchemes: []string{"https", "http"},
|
||||
WithPort: true,
|
||||
})
|
||||
|
||||
_, err := v.SafeHostname(controller.runtime.AppURL)
|
||||
|
||||
@@ -62,19 +62,15 @@ type ErrorScreen struct {
|
||||
Error string `url:"error"`
|
||||
}
|
||||
|
||||
type ClientRequest struct {
|
||||
ClientID string `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
type SkipConsentRequest struct {
|
||||
OIDCTicket string `form:"oidc_ticket" binding:"required"`
|
||||
}
|
||||
|
||||
type SkipConsentResponse struct {
|
||||
SkipConsent bool `json:"skipConsent"`
|
||||
}
|
||||
|
||||
type AuthorizeScreenParams struct {
|
||||
LoginFor FrontendLoginFor `url:"login_for"`
|
||||
OIDCTicket string `url:"oidc_ticket"`
|
||||
@@ -109,7 +105,6 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
||||
|
||||
oidcGroup := i.RouterGroup.Group("/oidc")
|
||||
oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
|
||||
oidcGroup.GET("/skip-consent", controller.skipConsent)
|
||||
oidcGroup.POST("/token", controller.Token)
|
||||
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||
oidcGroup.POST("/userinfo", controller.Userinfo)
|
||||
@@ -247,6 +242,16 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if userContext != nil && userContext.Authenticated && values.OIDCPrompt != service.OIDCPromptLogin {
|
||||
consent, err := controller.oidc.GetOIDCConsent(c, userContext.GetUsername(), req.ClientID)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to get OIDC consent")
|
||||
} else if consent != nil && scopesGranted(consent.Scope, req.Scope) {
|
||||
values.OIDCPrompt = service.OIDCPromptNone
|
||||
}
|
||||
}
|
||||
|
||||
queries, err := query.Values(values)
|
||||
|
||||
if err != nil {
|
||||
@@ -265,90 +270,6 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
func (controller *OIDCController) skipConsent(c *gin.Context) {
|
||||
c.Header("cache-control", "no-store")
|
||||
c.Header("pragma", "no-cache")
|
||||
|
||||
if controller.oidc == nil {
|
||||
c.JSON(500, SimpleResponse{
|
||||
Status: 500,
|
||||
Message: "OIDC not configured",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to get user context")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !userContext.Authenticated {
|
||||
c.JSON(401, SimpleResponse{
|
||||
Status: 401,
|
||||
Message: "User not logged in",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req SkipConsentRequest
|
||||
|
||||
err = c.BindQuery(&req)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, SimpleResponse{
|
||||
Status: 400,
|
||||
Message: "Bad request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.log.App.Debug().Interface("req", req).Msg("Received skip consent request")
|
||||
|
||||
authorizeReq, ok := controller.oidc.GetAuthorizeRequestByTicket(req.OIDCTicket)
|
||||
|
||||
if !ok {
|
||||
c.JSON(200, SkipConsentResponse{
|
||||
SkipConsent: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.log.App.Debug().Str("client", authorizeReq.ClientID).Str("user", userContext.GetUsername()).Msg("User consented to OIDC")
|
||||
|
||||
if authorizeReq.Prompt == service.OIDCPromptLogin.String() {
|
||||
c.JSON(200, SkipConsentResponse{
|
||||
SkipConsent: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
consent, err := controller.oidc.GetOIDCConsent(c, userContext.GetUsername(), authorizeReq.ClientID)
|
||||
|
||||
if err != nil || consent == nil {
|
||||
if err != nil {
|
||||
controller.log.App.Warn().Err(err).Msg("Failed to get OIDC consent")
|
||||
}
|
||||
c.JSON(200, SkipConsentResponse{
|
||||
SkipConsent: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !scopesGranted(consent.Scope, authorizeReq.Scope) {
|
||||
c.JSON(200, SkipConsentResponse{
|
||||
SkipConsent: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, SkipConsentResponse{
|
||||
SkipConsent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -409,9 +330,6 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// We no longer need the ticket
|
||||
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
|
||||
|
||||
// Get the client
|
||||
client, ok := controller.oidc.GetClient(authorizeReq.ClientID)
|
||||
|
||||
@@ -425,6 +343,9 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
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, authorizeReq.ClientID)
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestOIDCController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Authorize does not skip the consent screen even when consent was already granted",
|
||||
description: "Authorize skips the consent screen when all requested scopes were already granted",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
@@ -189,9 +189,77 @@ func TestOIDCController(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
// The consent skip check now happens after auth via the
|
||||
// skip-consent endpoint, so authorize should never set
|
||||
// prompt=none on its own.
|
||||
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
|
||||
assert.Contains(t, location, "oidc_prompt=none")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Authorize shows the consent screen when a new scope is requested",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "testuser", ClientID: "some-client-id",
|
||||
Scope: "openid", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("scope", "openid profile")
|
||||
q.Set("response_type", "code")
|
||||
q.Set("client_id", "some-client-id")
|
||||
q.Set("redirect_uri", "https://test.example.com/callback")
|
||||
|
||||
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
|
||||
assert.NotContains(t, location, "oidc_prompt=none")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Authorize skips the consent screen for a subset of already granted scopes",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "testuser", ClientID: "some-client-id",
|
||||
Scope: "openid profile email", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("scope", "openid profile")
|
||||
q.Set("response_type", "code")
|
||||
q.Set("client_id", "some-client-id")
|
||||
q.Set("redirect_uri", "https://test.example.com/callback")
|
||||
|
||||
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
|
||||
assert.Contains(t, location, "oidc_prompt=none")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Authorize shows the consent screen when no consent was granted yet",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "some-client-id"))
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("scope", "openid profile")
|
||||
q.Set("response_type", "code")
|
||||
q.Set("client_id", "some-client-id")
|
||||
q.Set("redirect_uri", "https://test.example.com/callback")
|
||||
|
||||
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
|
||||
@@ -236,232 +304,6 @@ func TestOIDCController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
|
||||
// --- skip-consent ---
|
||||
{
|
||||
description: "Skip consent returns 500 when OIDC is not configured",
|
||||
oidcDisabled: true,
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=some-ticket", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns 401 when the user context is missing",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=some-ticket", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns 401 when the user is not authenticated",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &model.UserContext{
|
||||
Authenticated: false,
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "testuser"},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=some-ticket", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns 400 when the ticket is missing",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns false when the ticket is unknown",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=nonexistent-ticket", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.False(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns true when the authorize request has prompt none",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "testuser", ClientID: "some-client-id",
|
||||
Scope: "openid profile", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid profile",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
Prompt: "none",
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.True(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns false when no consent was granted yet",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "some-client-id"))
|
||||
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid profile",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.False(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns false when a new scope is requested",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "testuser", ClientID: "some-client-id",
|
||||
Scope: "openid", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid profile",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.False(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns true when all requested scopes were already granted",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "testuser", ClientID: "some-client-id",
|
||||
Scope: "openid profile", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid profile",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.True(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns true for a subset of already granted scopes",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "testuser", ClientID: "some-client-id",
|
||||
Scope: "openid profile email", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid profile",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.True(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skip consent returns false when the consent belongs to another user",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "some-client-id"))
|
||||
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
Username: "otheruser", ClientID: "some-client-id",
|
||||
Scope: "openid profile", CreatedAt: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid profile",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res SkipConsentResponse
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
assert.False(t, res.SkipConsent)
|
||||
},
|
||||
},
|
||||
|
||||
// --- authorize-complete ---
|
||||
{
|
||||
description: "Should fail if oidc is disabled",
|
||||
@@ -581,60 +423,6 @@ func TestOIDCController(t *testing.T) {
|
||||
assert.Contains(t, redirectURI, "state=state-123")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Authorize complete deletes the ticket on success",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: ticket})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
_, ok := oidcService.GetAuthorizeRequestByTicket(ticket)
|
||||
assert.False(t, ok)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Authorize complete deletes the ticket even when the client is unknown",
|
||||
middlewares: []gin.HandlerFunc{authedUser},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "unknown-client",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
})
|
||||
|
||||
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: ticket})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||
redirectURI, ok := res["redirect_uri"].(string)
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, redirectURI, oidcService.GetIssuer()+"/error")
|
||||
|
||||
_, ok = oidcService.GetAuthorizeRequestByTicket(ticket)
|
||||
assert.False(t, ok)
|
||||
},
|
||||
},
|
||||
|
||||
// --- token ---
|
||||
{
|
||||
|
||||
@@ -57,7 +57,6 @@ type ProxyContext struct {
|
||||
type ProxyController struct {
|
||||
log *logger.Logger
|
||||
runtime *model.RuntimeConfig
|
||||
config *model.Config
|
||||
acls *service.AccessControlsService
|
||||
auth *service.AuthService
|
||||
policyEngine *service.PolicyEngine
|
||||
@@ -68,7 +67,6 @@ type ProxyControllerInput struct {
|
||||
|
||||
Log *logger.Logger
|
||||
RuntimeConfig *model.RuntimeConfig
|
||||
Config *model.Config
|
||||
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||
ACLsService *service.AccessControlsService
|
||||
AuthService *service.AuthService
|
||||
@@ -79,7 +77,6 @@ func NewProxyController(i ProxyControllerInput) *ProxyController {
|
||||
controller := &ProxyController{
|
||||
log: i.Log,
|
||||
runtime: i.RuntimeConfig,
|
||||
config: i.Config,
|
||||
acls: i.ACLsService,
|
||||
auth: i.AuthService,
|
||||
policyEngine: i.PolicyEngine,
|
||||
@@ -468,10 +465,6 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont
|
||||
// We get the path from the query string
|
||||
path := c.Query("path")
|
||||
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return ProxyContext{}, errors.New("path not found")
|
||||
}
|
||||
|
||||
// For envoy we need to support every method
|
||||
method := c.Request.Method
|
||||
|
||||
@@ -484,22 +477,14 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (controller *ProxyController) determineAuthModules(proxy ProxyType, fallbacks bool) []AuthModuleType {
|
||||
func (controller *ProxyController) determineAuthModules(proxy ProxyType) []AuthModuleType {
|
||||
switch proxy {
|
||||
case Traefik, Caddy:
|
||||
return []AuthModuleType{ForwardAuth}
|
||||
case Envoy:
|
||||
authModules := []AuthModuleType{ExtAuthz}
|
||||
if fallbacks {
|
||||
authModules = append(authModules, ForwardAuth)
|
||||
}
|
||||
return authModules
|
||||
return []AuthModuleType{ExtAuthz, ForwardAuth}
|
||||
case Nginx:
|
||||
authModules := []AuthModuleType{AuthRequest}
|
||||
if fallbacks {
|
||||
authModules = append(authModules, ForwardAuth)
|
||||
}
|
||||
return authModules
|
||||
return []AuthModuleType{AuthRequest, ForwardAuth}
|
||||
default:
|
||||
return []AuthModuleType{}
|
||||
}
|
||||
@@ -529,39 +514,6 @@ func (controller *ProxyController) getContextFromAuthModule(c *gin.Context, modu
|
||||
return ProxyContext{}, fmt.Errorf("unsupported auth module: %v", module)
|
||||
}
|
||||
|
||||
func (controller *ProxyController) authModuleIdentifiersPresent(c *gin.Context, module AuthModuleType) bool {
|
||||
switch module {
|
||||
case ForwardAuth:
|
||||
_, host := controller.getHeader(c, "x-forwarded-host")
|
||||
_, uri := controller.getHeader(c, "x-forwarded-uri")
|
||||
return host || uri
|
||||
case AuthRequest:
|
||||
_, ok := controller.getHeader(c, "x-original-url")
|
||||
return ok
|
||||
case ExtAuthz:
|
||||
return strings.TrimSpace(c.Query("path")) != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *ProxyController) ensureNoMultipleAuthModules(c *gin.Context, authModules []AuthModuleType) error {
|
||||
present := 0
|
||||
|
||||
for _, module := range authModules {
|
||||
if controller.authModuleIdentifiersPresent(c, module) {
|
||||
present++
|
||||
}
|
||||
}
|
||||
|
||||
if present > 1 {
|
||||
controller.log.App.Warn().Msg("Request carries headers for multiple auth modules, possible spoofing attempt, denying")
|
||||
return fmt.Errorf("conflicting auth module headers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext, error) {
|
||||
var req Proxy
|
||||
|
||||
@@ -578,34 +530,26 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
|
||||
|
||||
controller.log.App.Debug().Msgf("Determined proxy type: %v", proxy)
|
||||
|
||||
authModules := controller.determineAuthModules(proxy, !controller.config.Experimental.DisableAuthModuleFallback)
|
||||
authModules := controller.determineAuthModules(proxy)
|
||||
|
||||
if len(authModules) == 0 {
|
||||
return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy)
|
||||
}
|
||||
|
||||
err = controller.ensureNoMultipleAuthModules(c, controller.determineAuthModules(proxy, true))
|
||||
|
||||
if err != nil {
|
||||
return ProxyContext{}, err
|
||||
}
|
||||
|
||||
var ctx *ProxyContext
|
||||
var ctx ProxyContext
|
||||
|
||||
for _, module := range authModules {
|
||||
controller.log.App.Debug().Msgf("Trying to get context from auth module %v", module)
|
||||
authModuleCtx, err := controller.getContextFromAuthModule(c, module)
|
||||
if err != nil {
|
||||
controller.log.App.Debug().Msgf("Failed to get context from auth module %v: %v", module, err)
|
||||
continue
|
||||
ctx, err = controller.getContextFromAuthModule(c, module)
|
||||
if err == nil {
|
||||
controller.log.App.Debug().Msgf("Successfully got context from auth module %v", module)
|
||||
break
|
||||
}
|
||||
controller.log.App.Debug().Msgf("Successfully got context from auth module %v", module)
|
||||
ctx = &authModuleCtx
|
||||
break
|
||||
controller.log.App.Debug().Msgf("Failed to get context from auth module %v: %v", module, err)
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
return ProxyContext{}, fmt.Errorf("failed to get context from any auth module")
|
||||
if err != nil {
|
||||
return ProxyContext{}, err
|
||||
}
|
||||
|
||||
// Parse the raw path to populate the cleaned path used for ACLs
|
||||
@@ -633,5 +577,5 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
|
||||
|
||||
ctx.IsBrowser = isBrowser
|
||||
ctx.ProxyType = proxy
|
||||
return *ctx, nil
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ func TestProxyController(t *testing.T) {
|
||||
description: "Ensure forward auth fallback for envoy",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy", nil)
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Host = ""
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
@@ -261,7 +261,7 @@ func TestProxyController(t *testing.T) {
|
||||
description: "Ensure extauthz with envoy non browser returns json",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy", nil)
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/hello")
|
||||
@@ -877,32 +877,6 @@ func TestProxyController(t *testing.T) {
|
||||
assert.Equal(t, "bar", recorder.Header().Get("x-foo"))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Forward auth and auth request headers should fail for nginx",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-forwarded-host", "foo.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/foo?bar=foo")
|
||||
req.Header.Set("x-original-url", "https://foo.example.com/foo?bar=foo")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Forward auth and ext authz headers should fail for envoy",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Host = "foo.example.com"
|
||||
req.Header.Set("x-forwarded-host", "foo.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/foo?bar=foo")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
store := memory.New()
|
||||
@@ -918,7 +892,6 @@ func TestProxyController(t *testing.T) {
|
||||
aclsService := service.NewAccessControlsService(service.AccessControlServiceInput{
|
||||
Log: log,
|
||||
Config: &cfg,
|
||||
Runtime: &runtime,
|
||||
LabelProvider: nil,
|
||||
})
|
||||
|
||||
@@ -979,7 +952,6 @@ func TestProxyController(t *testing.T) {
|
||||
NewProxyController(ProxyControllerInput{
|
||||
Log: log,
|
||||
RuntimeConfig: &runtime,
|
||||
Config: &cfg,
|
||||
RouterGroup: group,
|
||||
ACLsService: aclsService,
|
||||
AuthService: authService,
|
||||
|
||||
@@ -239,8 +239,7 @@ type LogStreamConfig struct {
|
||||
}
|
||||
|
||||
type ExperimentalConfig struct {
|
||||
OAuthBridgeEnabled bool `description:"Enable the OAuth bridge, uses a new way to format OAuth user information." yaml:"oauthBridgeEnabled,omitempty"`
|
||||
DisableAuthModuleFallback bool `description:"Disable the fallback to forward_auth modules when auth_request or ext_authz fail." yaml:"disableAuthModuleFallback,omitempty"`
|
||||
OAuthBridgeEnabled bool `description:"Enable the OAuth bridge, uses a new way to format OAuth user information." yaml:"oauthBridgeEnabled,omitempty"`
|
||||
}
|
||||
|
||||
type TailscaleConfig struct {
|
||||
|
||||
@@ -2,13 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"github.com/tinyauthapp/tinyauth/pkg/validators"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
@@ -19,7 +17,6 @@ type LabelProvider interface {
|
||||
type AccessControlsService struct {
|
||||
log *logger.Logger
|
||||
config *model.Config
|
||||
runtime *model.RuntimeConfig
|
||||
labelProvider LabelProvider
|
||||
}
|
||||
|
||||
@@ -28,7 +25,6 @@ type AccessControlServiceInput struct {
|
||||
|
||||
Log *logger.Logger
|
||||
Config *model.Config
|
||||
Runtime *model.RuntimeConfig
|
||||
LabelProvider LabelProvider `optional:"true"`
|
||||
}
|
||||
|
||||
@@ -37,38 +33,12 @@ func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsServic
|
||||
return &AccessControlsService{
|
||||
log: i.Log,
|
||||
config: i.Config,
|
||||
runtime: i.Runtime,
|
||||
labelProvider: i.LabelProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) ensureAscii(str string) bool {
|
||||
for i := 0; i < len(str); i++ {
|
||||
if str[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) normalizeDomain(domain string) string {
|
||||
if host, _, err := net.SplitHostPort(domain); err == nil {
|
||||
domain = host
|
||||
}
|
||||
domain = strings.TrimRight(domain, ".")
|
||||
return strings.ToLower(domain)
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) {
|
||||
if !service.ensureAscii(domain) {
|
||||
return nil, errors.New("domain contains non-ascii characters")
|
||||
}
|
||||
|
||||
normalizedDomain := service.normalizeDomain(domain)
|
||||
|
||||
if !strings.HasSuffix(normalizedDomain, "."+service.runtime.CookieDomain) && normalizedDomain != service.runtime.CookieDomain {
|
||||
return nil, fmt.Errorf("domain does not match cookie domain, expected %s (or a subdomain), got %s", service.runtime.CookieDomain, domain)
|
||||
}
|
||||
v := validators.NewDomainValidator(validators.DomainValidatorOptions{})
|
||||
|
||||
var domainMatch *model.App
|
||||
var nameMatch *model.App
|
||||
@@ -76,18 +46,16 @@ func (service *AccessControlsService) getACLs(domain string, lookup func(locator
|
||||
|
||||
locatorFunc := func(name string, app *model.App) bool {
|
||||
if app.Config.Domain != "" {
|
||||
if !service.ensureAscii(app.Config.Domain) {
|
||||
service.log.App.Warn().Str("name", name).Str("domain", app.Config.Domain).Msg("Domain contains non-ascii characters, skipping")
|
||||
return false
|
||||
}
|
||||
if normalizedDomain == service.normalizeDomain(app.Config.Domain) {
|
||||
err := v.Validate(app.Config.Domain, domain)
|
||||
if err == nil {
|
||||
service.log.App.Debug().Str("name", name).Msg("Found matching container by domain")
|
||||
domainMatch = app
|
||||
return true
|
||||
} else if !errors.Is(err, validators.ErrHostnameMismatch) {
|
||||
service.log.App.Debug().Str("name", name).Err(err).Msg("Domain validation failed")
|
||||
}
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(normalizedDomain, strings.ToLower(name+".")) {
|
||||
if strings.HasPrefix(strings.ToLower(domain), strings.ToLower(name+".")) {
|
||||
service.log.App.Debug().Str("name", name).Msg("Found matching container by app name")
|
||||
nameMatch = app
|
||||
nameMatchedApps = append(nameMatchedApps, name)
|
||||
@@ -111,7 +79,7 @@ func (service *AccessControlsService) getACLs(domain string, lookup func(locator
|
||||
}
|
||||
|
||||
if len(nameMatchedApps) > 1 {
|
||||
return nil, fmt.Errorf("domain matched multiple apps by name prefix, use explicit domain config")
|
||||
service.log.App.Warn().Str("domain", domain).Strs("apps", nameMatchedApps).Msg("Multiple apps matched domain by name, app names must be unique, using last match")
|
||||
}
|
||||
|
||||
service.log.App.Debug().Str("domain", domain).Msg("Found matching app by app name")
|
||||
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
@@ -36,25 +34,14 @@ func TestAccessControlsService(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
_, runtime := test.CreateTestConfigs(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
acls map[string]model.App
|
||||
want *model.App
|
||||
errorFunc func(t *testing.T, e error)
|
||||
name string
|
||||
domain string
|
||||
acls map[string]model.App
|
||||
want *model.App
|
||||
}{
|
||||
{
|
||||
name: "returns ACLs for domain",
|
||||
domain: "app.example.com",
|
||||
acls: map[string]model.App{
|
||||
"foo": {Config: model.AppConfig{Domain: "app.example.com"}},
|
||||
},
|
||||
want: &model.App{Config: model.AppConfig{Domain: "app.example.com"}},
|
||||
},
|
||||
{
|
||||
name: "returns ACLs for root domain",
|
||||
domain: "example.com",
|
||||
acls: map[string]model.App{
|
||||
"foo": {Config: model.AppConfig{Domain: "example.com"}},
|
||||
@@ -78,11 +65,20 @@ func TestAccessControlsService(t *testing.T) {
|
||||
want: &model.App{Config: model.AppConfig{Domain: "example.com"}},
|
||||
},
|
||||
{
|
||||
name: "returns error for non-ascii domain",
|
||||
name: "returns ACLs for non-ascii domain",
|
||||
domain: "bücher.example.com",
|
||||
errorFunc: func(t *testing.T, e error) {
|
||||
assert.ErrorContains(t, e, "domain contains non-ascii characters")
|
||||
acls: map[string]model.App{
|
||||
"foo": {Config: model.AppConfig{Domain: "bücher.example.com"}},
|
||||
},
|
||||
want: &model.App{Config: model.AppConfig{Domain: "bücher.example.com"}},
|
||||
},
|
||||
{
|
||||
name: "returns ACLs for punycode domain and non-ascii config",
|
||||
domain: "bücher.example.com",
|
||||
acls: map[string]model.App{
|
||||
"foo": {Config: model.AppConfig{Domain: "xn--bcher-kva.example.com"}},
|
||||
},
|
||||
want: &model.App{Config: model.AppConfig{Domain: "xn--bcher-kva.example.com"}},
|
||||
},
|
||||
{
|
||||
name: "returns ACLs with case-insensitive matching",
|
||||
@@ -114,33 +110,6 @@ func TestAccessControlsService(t *testing.T) {
|
||||
acls: map[string]model.App{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "App in domain not matching with the cookie domain should return nothing with name matching",
|
||||
domain: "foo.bad_example.com",
|
||||
acls: map[string]model.App{
|
||||
"foo": {
|
||||
Path: model.AppPath{Allow: "/foo"},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
errorFunc: func(t *testing.T, e error) {
|
||||
assert.ErrorContains(t, e, "domain does not match cookie domain")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "App in domain not matching with the cookie domain should return nothing with domain matching",
|
||||
domain: "foo.bad_example.com",
|
||||
acls: map[string]model.App{
|
||||
"foo": {
|
||||
Path: model.AppPath{Allow: "/foo"},
|
||||
Config: model.AppConfig{Domain: "foo.bad_example.com"},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
errorFunc: func(t *testing.T, e error) {
|
||||
assert.ErrorContains(t, e, "domain does not match cookie domain")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// run once for a mock provider
|
||||
@@ -149,15 +118,10 @@ func TestAccessControlsService(t *testing.T) {
|
||||
mock := newMockProvider(test.acls, false)
|
||||
acls := NewAccessControlsService(AccessControlServiceInput{
|
||||
Log: log,
|
||||
Runtime: &runtime,
|
||||
Config: &model.Config{},
|
||||
LabelProvider: mock,
|
||||
})
|
||||
app, err := acls.getACLs(test.domain, mock.Lookup)
|
||||
if test.errorFunc != nil {
|
||||
test.errorFunc(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.want, app)
|
||||
})
|
||||
@@ -167,17 +131,12 @@ func TestAccessControlsService(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name+"(staticACLs)", func(t *testing.T) {
|
||||
acls := NewAccessControlsService(AccessControlServiceInput{
|
||||
Log: log,
|
||||
Runtime: &runtime,
|
||||
Log: log,
|
||||
Config: &model.Config{
|
||||
Apps: test.acls,
|
||||
},
|
||||
})
|
||||
app, err := acls.lookupStaticACLs(test.domain)
|
||||
if test.errorFunc != nil {
|
||||
test.errorFunc(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.want, app)
|
||||
})
|
||||
@@ -186,32 +145,16 @@ func TestAccessControlsService(t *testing.T) {
|
||||
// get acls should return an error when the provider fails
|
||||
mock := newMockProvider(map[string]model.App{}, true)
|
||||
acls := NewAccessControlsService(AccessControlServiceInput{
|
||||
Log: log,
|
||||
Runtime: &runtime,
|
||||
Config: &model.Config{},
|
||||
Log: log,
|
||||
Config: &model.Config{},
|
||||
})
|
||||
_, err := acls.getACLs("example.com", mock.Lookup)
|
||||
assert.Error(t, err)
|
||||
|
||||
// get acls should return an error when multiple apps with the same domain exist
|
||||
acls = NewAccessControlsService(AccessControlServiceInput{
|
||||
Log: log,
|
||||
Runtime: &runtime,
|
||||
Config: &model.Config{
|
||||
Apps: map[string]model.App{
|
||||
"foo": {Path: model.AppPath{Allow: "/foo"}},
|
||||
"foo.bar": {Path: model.AppPath{Allow: "/bar"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err = acls.GetAccessControls("foo.bar.example.com")
|
||||
assert.ErrorContains(t, err, "domain matched multiple apps by name prefix, use explicit domain config")
|
||||
require.Error(t, err)
|
||||
|
||||
// get access controls should get acls from
|
||||
// static when static acls are configured
|
||||
acls = NewAccessControlsService(AccessControlServiceInput{
|
||||
Log: log,
|
||||
Runtime: &runtime,
|
||||
Log: log,
|
||||
Config: &model.Config{
|
||||
Apps: map[string]model.App{
|
||||
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
||||
@@ -220,12 +163,12 @@ func TestAccessControlsService(t *testing.T) {
|
||||
})
|
||||
app, err := acls.GetAccessControls("foo.example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &model.App{Config: model.AppConfig{Domain: "foo.example.com"}}, app)
|
||||
require.Equal(t, &model.App{Config: model.AppConfig{Domain: "foo.example.com"}}, app)
|
||||
|
||||
// should return nil for no apps
|
||||
app, err = acls.GetAccessControls("bar.example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, app)
|
||||
require.Nil(t, app)
|
||||
|
||||
// Should use label provider if available
|
||||
mock = newMockProvider(map[string]model.App{
|
||||
@@ -235,11 +178,10 @@ func TestAccessControlsService(t *testing.T) {
|
||||
}, false)
|
||||
acls = NewAccessControlsService(AccessControlServiceInput{
|
||||
Log: log,
|
||||
Runtime: &runtime,
|
||||
Config: &model.Config{},
|
||||
LabelProvider: mock,
|
||||
})
|
||||
app, err = acls.GetAccessControls("bar.example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &model.App{Config: model.AppConfig{Domain: "bar.example.com"}}, app)
|
||||
require.Equal(t, &model.App{Config: model.AppConfig{Domain: "bar.example.com"}}, app)
|
||||
}
|
||||
|
||||
@@ -53,17 +53,6 @@ const (
|
||||
OIDCPromptNone OIDCPrompt = "none"
|
||||
)
|
||||
|
||||
func (p OIDCPrompt) String() string {
|
||||
switch p {
|
||||
case OIDCPromptLogin:
|
||||
return "login"
|
||||
case OIDCPromptNone:
|
||||
return "none"
|
||||
default:
|
||||
return "login"
|
||||
}
|
||||
}
|
||||
|
||||
var SupportedPrompts = []string{string(OIDCPromptLogin), string(OIDCPromptNone)}
|
||||
|
||||
// This is not spec-compliant, the ID token SHOULD NOT contain user info claims but,
|
||||
|
||||
@@ -38,16 +38,7 @@ func SafeParseAppURL(str string) (string, error) {
|
||||
return "", fmt.Errorf("ip addresses not allowed")
|
||||
}
|
||||
|
||||
i := idna.New(
|
||||
idna.MapForLookup(),
|
||||
idna.Transitional(false),
|
||||
idna.BidiRule(),
|
||||
idna.StrictDomainName(false),
|
||||
idna.CheckHyphens(true),
|
||||
idna.CheckJoiners(false),
|
||||
)
|
||||
|
||||
hostname, err = i.ToASCII(hostname)
|
||||
hostname, err = idna.Lookup.ToASCII(hostname)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to convert hostname to ascii: %w", err)
|
||||
|
||||
@@ -43,13 +43,6 @@ func TestSafeParseAPPURL(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Underscores
|
||||
appURL = "http://sub_tinyauth.app"
|
||||
expected = "http://sub_tinyauth.app"
|
||||
result, err = utils.SafeParseAppURL(appURL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Lowercase
|
||||
appURL = "HTTP://SUb.tinyAUth.aPP"
|
||||
expected = "http://sub.tinyauth.app"
|
||||
@@ -73,7 +66,7 @@ func TestSafeParseAPPURL(t *testing.T) {
|
||||
assert.ErrorContains(t, err, "invalid url")
|
||||
|
||||
// Invalid punycode
|
||||
appURL = "http://xn--h-kva.example.com"
|
||||
appURL = "http://ab--cd.example.com"
|
||||
_, err = utils.SafeParseAppURL(appURL)
|
||||
assert.ErrorContains(t, err, "failed to convert hostname to ascii")
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// Errors
|
||||
@@ -113,10 +115,14 @@ func (v *DomainValidator) getURL(i string) (*url.URL, error) {
|
||||
|
||||
func (v *DomainValidator) getHostname(hostname string) (string, error) {
|
||||
hostname = strings.ToLower(hostname)
|
||||
hostname = strings.TrimRight(hostname, ".")
|
||||
hostname = strings.TrimSuffix(hostname, ".")
|
||||
if net.ParseIP(hostname) != nil {
|
||||
return "", fmt.Errorf("ip addresses are not supported")
|
||||
}
|
||||
hostname, err := idna.Lookup.ToASCII(hostname)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to convert hostname to ascii: %w", err)
|
||||
}
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,18 @@ func TestDomainValidator_SafeHostname(t *testing.T) {
|
||||
assert.ErrorContains(t, e, "ip addresses are not supported")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Domains with unicode characters should be allowed",
|
||||
input: "bücher.example.com",
|
||||
expected: "xn--bcher-kva.example.com",
|
||||
},
|
||||
{
|
||||
description: "Invalid IDNA domain should fail",
|
||||
input: "ab--cd.example.com",
|
||||
errorFunc: func(t *testing.T, e error) {
|
||||
assert.ErrorContains(t, e, "invalid label")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "With port enabled without any port should work",
|
||||
options: DomainValidatorOptions{WithPort: true},
|
||||
@@ -182,6 +194,22 @@ func TestDomainValidator_Validate(t *testing.T) {
|
||||
expected: "https://example.com:443",
|
||||
actual: "https://example.com:443",
|
||||
},
|
||||
{
|
||||
description: "Failure to format expected domain should fail",
|
||||
expected: "ab--cd.example.com",
|
||||
actual: "example.com",
|
||||
errorFunc: func(t *testing.T, e error) {
|
||||
assert.ErrorContains(t, e, "idna: invalid label")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Failure to format check domain should fail",
|
||||
expected: "example.com",
|
||||
actual: "ab--cd.example.com",
|
||||
errorFunc: func(t *testing.T, e error) {
|
||||
assert.ErrorContains(t, e, "idna: invalid label")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Valid domains with matching schemes and ports should pass",
|
||||
options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}, WithPort: true},
|
||||
@@ -208,6 +236,16 @@ func TestDomainValidator_Validate(t *testing.T) {
|
||||
actual: "example.com",
|
||||
expected: "example.com",
|
||||
},
|
||||
{
|
||||
description: "Unicode valid domains should pass",
|
||||
expected: "xn--bcher-kva.example.com",
|
||||
actual: "bücher.example.com",
|
||||
},
|
||||
{
|
||||
description: "Unicode valid domains should pass (reverse)",
|
||||
expected: "bücher.example.com",
|
||||
actual: "xn--bcher-kva.example.com",
|
||||
},
|
||||
{
|
||||
description: "Non matching hostnames should fail",
|
||||
expected: "example.com",
|
||||
|
||||
Reference in New Issue
Block a user