Compare commits

..

5 Commits

Author SHA1 Message Date
Stavros
1feffaa4d2 fix: fix envoy tests 2026-03-14 12:09:18 +02:00
Stavros
c2a57b57b8 Merge branch 'refactor/proxy-controller' into feat/proxy-integration-tests 2026-03-13 23:08:18 +02:00
Stavros
5b2fa47c0e wip 2026-03-13 23:04:47 +02:00
Stavros
13bb3ca682 fix: add extauthz to friendly error messages 2026-03-13 23:03:11 +02:00
Stavros
81cb695d0b wip 2026-03-13 17:45:11 +02:00
22 changed files with 738 additions and 163 deletions

View File

@@ -18,7 +18,7 @@ jobs:
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: "^1.26.0"
go-version: "^1.24.0"
- name: Initialize submodules
run: |

View File

@@ -61,7 +61,7 @@ jobs:
- name: Install go
uses: actions/setup-go@v5
with:
go-version: "^1.26.0"
go-version: "^1.24.0"
- name: Initialize submodules
run: |
@@ -116,7 +116,7 @@ jobs:
- name: Install go
uses: actions/setup-go@v5
with:
go-version: "^1.26.0"
go-version: "^1.24.0"
- name: Initialize submodules
run: |

View File

@@ -39,7 +39,7 @@ jobs:
- name: Install go
uses: actions/setup-go@v5
with:
go-version: "^1.26.0"
go-version: "^1.24.0"
- name: Initialize submodules
run: |
@@ -91,7 +91,7 @@ jobs:
- name: Install go
uses: actions/setup-go@v5
with:
go-version: "^1.26.0"
go-version: "^1.24.0"
- name: Initialize submodules
run: |

View File

@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
RUN bun run build
# Builder
FROM golang:1.26-alpine3.21 AS builder
FROM golang:1.25-alpine3.21 AS builder
ARG VERSION
ARG COMMIT_HASH

View File

@@ -1,4 +1,4 @@
FROM golang:1.26-alpine3.21
FROM golang:1.25-alpine3.21
WORKDIR /tinyauth

View File

@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
RUN bun run build
# Builder
FROM golang:1.26-alpine3.21 AS builder
FROM golang:1.25-alpine3.21 AS builder
ARG VERSION
ARG COMMIT_HASH

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"charm.land/huh/v2"
"github.com/charmbracelet/huh"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/traefik/paerser/cli"
"golang.org/x/crypto/bcrypt"
@@ -61,8 +61,9 @@ func createUserCmd() *cli.Command {
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)

View File

@@ -9,7 +9,7 @@ import (
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"charm.land/huh/v2"
"github.com/charmbracelet/huh"
"github.com/mdp/qrterminal/v3"
"github.com/pquerna/otp/totp"
"github.com/traefik/paerser/cli"
@@ -54,8 +54,9 @@ func generateTotpCmd() *cli.Command {
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)

View File

@@ -3,7 +3,6 @@ package main
import (
"fmt"
"charm.land/huh/v2"
"github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
@@ -124,9 +123,3 @@ func runCmd(cfg config.Config) error {
return nil
}
type themeBase struct{}
func (t *themeBase) Theme(isDark bool) *huh.Styles {
return huh.ThemeBase(isDark)
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"charm.land/huh/v2"
"github.com/charmbracelet/huh"
"github.com/pquerna/otp/totp"
"github.com/traefik/paerser/cli"
"golang.org/x/crypto/bcrypt"
@@ -71,8 +71,9 @@ func verifyUserCmd() *cli.Command {
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
var baseTheme *huh.Theme = huh.ThemeBase()
err := form.WithTheme(baseTheme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)

32
go.mod
View File

@@ -1,12 +1,12 @@
module github.com/steveiliop56/tinyauth
go 1.26.0
go 1.25.0
replace github.com/traefik/paerser v0.2.2 => ./paerser
require (
charm.land/huh/v2 v2.0.3
github.com/cenkalti/backoff/v5 v5.0.3
github.com/charmbracelet/huh v0.8.0
github.com/docker/docker v28.5.2+incompatible
github.com/gin-gonic/gin v1.12.0
github.com/go-jose/go-jose/v4 v4.1.3
@@ -27,9 +27,6 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0 // indirect
charm.land/bubbletea/v2 v2.0.2 // indirect
charm.land/lipgloss/v2 v2.0.1 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -37,21 +34,20 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
@@ -60,6 +56,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
@@ -77,10 +74,11 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
@@ -90,7 +88,9 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect

76
go.sum
View File

@@ -1,11 +1,3 @@
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
@@ -27,8 +19,10 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -44,34 +38,34 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -97,6 +91,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -179,8 +175,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -188,8 +184,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
@@ -217,8 +215,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -240,6 +242,7 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -328,6 +331,7 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

16
integration/.env Normal file
View File

@@ -0,0 +1,16 @@
# Nginx configuration
NGINX_VERSION=1.29
# Whoami configuration
WHOAMI_VERSION=latest
# Traefik configuration
TRAEFIK_VERSION=v3.6
TINYAUTH_HOST=tinyauth.127.0.0.1.sslip.io
WHOAMI_HOST=whoami.127.0.0.1.sslip.io
# Envoy configuration
ENVOY_VERSION=v1.33-latest
# Tinyauth configuration
TINYAUTH_VERSION=dev

15
integration/config.yml Normal file
View File

@@ -0,0 +1,15 @@
appUrl: http://tinyauth.127.0.0.1.sslip.io
auth:
users: test:$2a$10$eG88kFow83l5YRSlTSL2o.sZimjxFHrpiKdaSUZqpLBGX7Y2.4PZG
log:
json: true
level: trace
apps:
whoami:
config:
domain: whoami.127.0.0.1.sslip.io
path:
allow: /allow

View File

@@ -0,0 +1,16 @@
services:
envoy:
image: envoyproxy/envoy:${ENVOY_VERSION}
ports:
- 80:80
volumes:
- ./envoy.yml:/etc/envoy/envoy.yaml:ro
whoami:
image: traefik/whoami:${WHOAMI_VERSION}
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:${TINYAUTH_VERSION}
command: --experimental.configfile=/data/config.yml
volumes:
- ./config.yml:/data/config.yml:ro

View File

@@ -0,0 +1,16 @@
services:
nginx:
image: nginx:${NGINX_VERSION}
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
whoami:
image: traefik/whoami:${WHOAMI_VERSION}
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:${TINYAUTH_VERSION}
command: --experimental.configfile=/data/config.yml
volumes:
- ./config.yml:/data/config.yml:ro

View File

@@ -0,0 +1,28 @@
services:
traefik:
image: traefik:${TRAEFIK_VERSION}
command: |
--api.insecure=true
--providers.docker
--entryPoints.web.address=:80
ports:
- 80:80
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: traefik/whoami:${WHOAMI_VERSION}
labels:
traefik.enable: true
traefik.http.routers.whoami.rule: Host(`${WHOAMI_HOST}`)
traefik.http.routers.whoami.middlewares: tinyauth
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:${TINYAUTH_VERSION}
command: --experimental.configfile=/data/config.yml
volumes:
- ./config.yml:/data/config.yml:ro
labels:
traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`${TINYAUTH_HOST}`)
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik

105
integration/envoy.yml Normal file
View File

@@ -0,0 +1,105 @@
static_resources:
listeners:
- name: "listener_http"
address:
socket_address:
address: "0.0.0.0"
port_value: 80
filter_chains:
- filters:
- name: "envoy.filters.network.http_connection_manager"
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
stat_prefix: "ingress_http"
use_remote_address: true
skip_xff_append: false
route_config:
name: "local_route"
virtual_hosts:
- name: "whoami_service"
domains: ["whoami.127.0.0.1.sslip.io"]
routes:
- match:
prefix: "/"
route:
cluster: "whoami"
- name: "tinyauth_service"
domains: ["tinyauth.127.0.0.1.sslip.io"]
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute"
disabled: true
routes:
- match:
prefix: "/"
route:
cluster: "tinyauth"
http_filters:
- name: "envoy.filters.http.ext_authz"
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz"
transport_api_version: "v3"
http_service:
path_prefix: "/api/auth/envoy?path="
server_uri:
uri: "tinyauth:3000"
cluster: "tinyauth"
timeout: "0.25s"
authorization_request:
allowed_headers:
patterns:
- exact: "authorization"
- exact: "accept"
- exact: "cookie"
headers_to_add:
- key: "x-forwarded-proto"
value: "%REQ(:SCHEME)%"
- key: "x-forwarded-host"
value: "%REQ(:AUTHORITY)%"
- key: "x-forwarded-uri"
value: "%REQ(:PATH)%"
authorization_response:
allowed_upstream_headers:
patterns:
- prefix: "remote-"
allowed_client_headers:
patterns:
- exact: "set-cookie"
- exact: "location"
allowed_client_headers_on_success:
patterns:
- exact: "set-cookie"
- exact: "location"
failure_mode_allow: false
- name: "envoy.filters.http.router"
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
clusters:
- name: "whoami"
connect_timeout: "0.25s"
type: "logical_dns"
dns_lookup_family: "v4_only"
lb_policy: "round_robin"
load_assignment:
cluster_name: "whoami"
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: "whoami"
port_value: 80
- name: "tinyauth"
connect_timeout: "0.25s"
type: "logical_dns"
dns_lookup_family: "v4_only"
lb_policy: "round_robin"
load_assignment:
cluster_name: "tinyauth"
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: "tinyauth"
port_value: 3000

View File

@@ -0,0 +1,62 @@
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func testUnauthorized(client *http.Client) error {
req, err := http.NewRequest("GET", WhoamiURL, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
// nginx and envoy will throw us at the frontend
if resp.StatusCode != http.StatusUnauthorized && !strings.Contains(string(body), "<div id=\"root\"></div>") {
return fmt.Errorf("expected status code %d or to to contain '<div id=\"root\"></div>', got %d", http.StatusUnauthorized, resp.StatusCode)
}
return nil
}
func testLoggedIn(client *http.Client) error {
req, err := http.NewRequest("GET", WhoamiURL, nil)
if err != nil {
return err
}
req.SetBasicAuth(DefaultUsername, DefaultPassword)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d, got %d", http.StatusOK, resp.StatusCode)
}
return nil
}
func testACLAllowed(client *http.Client) error {
req, err := http.NewRequest("GET", WhoamiURL+"/allow", nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d, got %d", http.StatusOK, resp.StatusCode)
}
return nil
}

166
integration/integration.go Normal file
View File

@@ -0,0 +1,166 @@
package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/exec"
"time"
)
var ProxiesToTest = []string{"traefik", "nginx", "envoy"}
const (
EnvFile = ".env"
ConfigFile = "config.yml"
)
const (
TinyauthURL = "http://tinyauth.127.0.0.1.sslip.io"
WhoamiURL = "http://whoami.127.0.0.1.sslip.io"
)
const (
DefaultUsername = "test"
DefaultPassword = "password"
)
func main() {
logFlag := flag.Bool("log", false, "enable stack logging")
flag.Parse()
for _, proxy := range ProxiesToTest {
slog.Info("begin", "proxy", proxy)
compose := fmt.Sprintf("docker-compose.%s.yml", proxy)
if _, err := os.Stat(compose); err != nil {
slog.Error("fail", "proxy", proxy, "error", err)
os.Exit(1)
}
if err := createInstanceAndRunTests(compose, *logFlag, proxy); err != nil {
slog.Error("fail", "proxy", proxy, "error", err)
os.Exit(1)
}
slog.Info("end", "proxy", proxy)
}
}
func runTests(client *http.Client, name string) error {
if err := testUnauthorized(client); err != nil {
slog.Error("fail unauthorized test", "name", name)
return err
}
slog.Info("unauthorized test passed", "name", name)
if err := testLoggedIn(client); err != nil {
slog.Error("fail logged in test", "name", name)
return err
}
slog.Info("logged in test passed", "name", name)
if err := testACLAllowed(client); err != nil {
slog.Error("fail acl test", "name", name)
return err
}
slog.Info("acl test passed", "name", name)
return nil
}
func createInstanceAndRunTests(compose string, log bool, name string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmdArgs := []string{"compose", "-f", compose, "--env-file", EnvFile, "up", "--build", "--force-recreate", "--remove-orphans"}
cmd := exec.CommandContext(ctx, "docker", cmdArgs...)
if log {
setupCmdLogging(cmd)
}
if err := cmd.Start(); err != nil {
return err
}
defer func() {
cmd.Process.Signal(os.Interrupt)
cmd.Wait()
}()
slog.Info("instance created", "name", name)
if err := waitForHealthy(); err != nil {
return err
}
slog.Info("instance healthy", "name", name)
client := &http.Client{}
if err := runTests(client, name); err != nil {
return err
}
slog.Info("tests passed", "name", name)
cmd.Process.Signal(os.Interrupt)
if err := cmd.Wait(); cmd.ProcessState.ExitCode() != 130 && err != nil {
return err
}
return nil
}
func setupCmdLogging(cmd *exec.Cmd) {
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
slog.Info("docker out", "stdout", scanner.Text())
}
}()
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
slog.Error("docker out", "stderr", scanner.Text())
}
}()
}
func waitForHealthy() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
ticker := time.NewTicker(10 * time.Second)
client := http.Client{}
for {
select {
case <-ctx.Done():
return errors.New("tinyauth not healthy after 5 minutes")
case <-ticker.C:
req, err := http.NewRequestWithContext(ctx, "GET", TinyauthURL+"/api/healthz", nil)
if err != nil {
return err
}
res, err := client.Do(req)
if err != nil {
continue
}
if res.StatusCode == http.StatusOK {
return nil
}
}
}
}

36
integration/nginx.conf Normal file
View File

@@ -0,0 +1,36 @@
server {
listen 80;
listen [::]:80;
server_name tinyauth.127.0.0.1.sslip.io;
location / {
proxy_pass http://tinyauth:3000;
}
}
server {
listen 80;
listen [::]:80;
server_name whoami.127.0.0.1.sslip.io;
location / {
proxy_pass http://whoami;
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
}
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_set_header x-original-url $scheme://$http_host$request_uri;
proxy_set_header x-forwarded-proto $scheme;
proxy_set_header x-forwarded-host $host;
proxy_set_header x-forwarded-uri $request_uri;
}
location @tinyauth_login {
return 302 http://tinyauth.127.0.0.1.sslip.io/login?redirect_uri=$scheme://$http_host$request_uri;
}
}

View File

@@ -1,9 +1,11 @@
package controller
import (
"errors"
"fmt"
"net/http"
"slices"
"net/url"
"regexp"
"strings"
"github.com/steveiliop56/tinyauth/internal/config"
@@ -15,12 +17,31 @@ import (
"github.com/google/go-querystring/query"
)
type RequestType int
const (
AuthRequest RequestType = iota
ExtAuthz
ForwardAuth
)
var BrowserUserAgentRegex = regexp.MustCompile("Chrome|Gecko|AppleWebKit|Opera|Edge")
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
type Proxy struct {
Proxy string `uri:"proxy" binding:"required"`
}
type ProxyContext struct {
Host string
Proto string
Path string
Method string
Type RequestType
IsBrowser bool
}
type ProxyControllerConfig struct {
AppURL string
}
@@ -43,82 +64,30 @@ func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, a
func (controller *ProxyController) SetupRoutes() {
proxyGroup := controller.router.Group("/auth")
// There is a later check to control allowed methods per proxy
proxyGroup.Any("/:proxy", controller.proxyHandler)
}
func (controller *ProxyController) proxyHandler(c *gin.Context) {
var req Proxy
// Load proxy context based on the request type
proxyCtx, err := controller.getProxyContext(c)
err := c.BindUri(&req)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to bind URI")
tlog.App.Warn().Err(err).Msg("Failed to get proxy context")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
"message": "Bad request",
})
return
}
if !slices.Contains(SupportedProxies, req.Proxy) {
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return
}
// Only allow GET for non-envoy proxies.
// Envoy uses the original client method for the external auth request
// so we allow Any standard HTTP method for /api/auth/envoy
if req.Proxy != "envoy" && c.Request.Method != http.MethodGet {
tlog.App.Warn().Str("method", c.Request.Method).Msg("Invalid method for proxy")
c.Header("Allow", "GET")
c.JSON(405, gin.H{
"status": 405,
"message": "Method Not Allowed",
})
return
}
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
if isBrowser {
tlog.App.Debug().Msg("Request identified as (most likely) coming from a browser")
} else {
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
}
// We are not marking the URI as a required header because it may be missing
// and we only use it for the auth enabled check which will simply not match
// if the header is missing. For deployments like Kubernetes, we use the
// x-original-uri header instead.
uri, ok := controller.getHeader(c, "x-forwarded-uri")
if !ok {
originalUri, ok := controller.getHeader(c, "x-original-uri")
if ok {
uri = originalUri
}
}
host, ok := controller.requireHeader(c, "x-forwarded-host")
if !ok {
return
}
proto, ok := controller.requireHeader(c, "x-forwarded-proto")
if !ok {
return
}
tlog.App.Trace().Interface("ctx", proxyCtx).Msg("Got proxy context")
// Get acls
acls, err := controller.acls.GetAccessControls(host)
acls, err := controller.acls.GetAccessControls(proxyCtx.Host)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to get access controls for resource")
controller.handleError(c, req, isBrowser)
controller.handleError(c, proxyCtx)
return
}
@@ -135,11 +104,11 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return
}
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls.Path)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
controller.handleError(c, req, isBrowser)
controller.handleError(c, proxyCtx)
return
}
@@ -154,7 +123,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
if !controller.auth.CheckIP(acls.IP, clientIP) {
if req.Proxy == "nginx" || !isBrowser {
if !controller.useFriendlyError(proxyCtx) {
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -163,7 +132,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
queries, err := query.Values(config.UnauthorizedQuery{
Resource: strings.Split(host, ".")[0],
Resource: strings.Split(proxyCtx.Host, ".")[0],
IP: clientIP,
})
@@ -196,9 +165,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
if !userAllowed {
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
if req.Proxy == "nginx" || !isBrowser {
if !controller.useFriendlyError(proxyCtx) {
c.JSON(403, gin.H{
"status": 403,
"message": "Forbidden",
@@ -207,7 +176,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
queries, err := query.Values(config.UnauthorizedQuery{
Resource: strings.Split(host, ".")[0],
Resource: strings.Split(proxyCtx.Host, ".")[0],
})
if err != nil {
@@ -236,9 +205,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
if !groupOK {
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User groups do not match resource requirements")
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
if req.Proxy == "nginx" || !isBrowser {
if !controller.useFriendlyError(proxyCtx) {
c.JSON(403, gin.H{
"status": 403,
"message": "Forbidden",
@@ -247,7 +216,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
queries, err := query.Values(config.UnauthorizedQuery{
Resource: strings.Split(host, ".")[0],
Resource: strings.Split(proxyCtx.Host, ".")[0],
GroupErr: true,
})
@@ -289,7 +258,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
return
}
if req.Proxy == "nginx" || !isBrowser {
if !controller.useFriendlyError(proxyCtx) {
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -298,7 +267,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
queries, err := query.Values(config.RedirectQuery{
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
})
if err != nil {
@@ -328,8 +297,8 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
}
}
func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrowser bool) {
if req.Proxy == "nginx" || !isBrowser {
func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyContext) {
if !controller.useFriendlyError(proxyCtx) {
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
@@ -340,20 +309,166 @@ func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrow
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
}
func (controller *ProxyController) requireHeader(c *gin.Context, header string) (string, bool) {
val, ok := controller.getHeader(c, header)
if !ok {
tlog.App.Error().Str("header", header).Msg("Header not found")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return "", false
}
return val, true
}
func (controller *ProxyController) getHeader(c *gin.Context, header string) (string, bool) {
val := c.Request.Header.Get(header)
return val, strings.TrimSpace(val) != ""
}
func (controller *ProxyController) useFriendlyError(proxyCtx ProxyContext) bool {
return (proxyCtx.Type == ForwardAuth || proxyCtx.Type == ExtAuthz) && proxyCtx.IsBrowser
}
// Code below is inspired from https://github.com/authelia/authelia/blob/master/internal/handlers/handler_authz.go
// and thus it may be subject to Apache 2.0 License
func (controller *ProxyController) getForwardAuthContext(c *gin.Context) (ProxyContext, error) {
host, ok := controller.getHeader(c, "x-forwarded-host")
if !ok {
return ProxyContext{}, errors.New("x-forwarded-host not found")
}
uri, ok := controller.getHeader(c, "x-forwarded-uri")
if !ok {
return ProxyContext{}, errors.New("x-forwarded-uri not found")
}
proto, ok := controller.getHeader(c, "x-forwarded-proto")
if !ok {
return ProxyContext{}, errors.New("x-forwarded-proto not found")
}
method := c.Request.Method
if method != http.MethodGet {
return ProxyContext{}, errors.New("method not allowed")
}
return ProxyContext{
Host: host,
Proto: proto,
Path: uri,
Method: method,
Type: ForwardAuth,
}, nil
}
func (controller *ProxyController) getAuthRequestContext(c *gin.Context) (ProxyContext, error) {
xOriginalUrl, ok := controller.getHeader(c, "x-original-url")
if !ok {
return ProxyContext{}, errors.New("x-original-url not found")
}
url, err := url.Parse(xOriginalUrl)
if err != nil {
return ProxyContext{}, err
}
host := url.Host
proto := url.Scheme
path := url.Path
method := c.Request.Method
if method != http.MethodGet {
return ProxyContext{}, errors.New("method not allowed")
}
return ProxyContext{
Host: host,
Proto: proto,
Path: path,
Method: method,
Type: AuthRequest,
}, nil
}
func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyContext, error) {
proto, ok := controller.getHeader(c, "x-forwarded-proto")
if !ok {
return ProxyContext{}, errors.New("x-forwarded-proto not found")
}
host, ok := controller.getHeader(c, "host")
if !ok {
return ProxyContext{}, errors.New("host not found")
}
// Seems like we can't get the path?
// For envoy we need to support every method
method := c.Request.Method
return ProxyContext{
Host: host,
Proto: proto,
Method: method,
Type: ExtAuthz,
}, nil
}
func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext, error) {
var req Proxy
err := c.BindUri(&req)
if err != nil {
return ProxyContext{}, err
}
var ctx ProxyContext
switch req.Proxy {
// For nginx we need to handle both forward_auth and auth_request extraction since it can be
// used either with something line nginx proxy manager with advanced config or with
// the kubernetes ingress controller
case "nginx":
tlog.App.Debug().Str("proxy", req.Proxy).Msg("Attempting forward_auth compatible extraction")
forwardAuthCtx, err := controller.getForwardAuthContext(c)
if err == nil {
tlog.App.Debug().Str("proxy", req.Proxy).Msg("Extractions success using forward_auth")
ctx = forwardAuthCtx
} else {
tlog.App.Debug().Str("proxy", req.Proxy).Msg("Extractions failed using forward_auth trying with auth_request")
authRequestCtx, err := controller.getAuthRequestContext(c)
if err != nil {
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Failed to determine required module for header extraction")
return ProxyContext{}, err
}
ctx = authRequestCtx
}
case "envoy":
tlog.App.Debug().Str("proxy", req.Proxy).Msg("Attempting ext_authz compatible extraction")
extAuthzCtx, err := controller.getExtAuthzContext(c)
if err != nil {
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Failed to determine required module for header extraction")
return ProxyContext{}, err
}
ctx = extAuthzCtx
// By default we fallback to the forward_auth module which supports most proxies like traefik or caddy
default:
tlog.App.Debug().Str("proxy", req.Proxy).Msg("Attempting forward_auth compatible extraction")
forwardAuthCtx, err := controller.getForwardAuthContext(c)
if err != nil {
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Failed to determine required module for header extraction")
return ProxyContext{}, err
}
ctx = forwardAuthCtx
}
// We don't care if the header is empty, we will just assume it's not a browser
userAgent, _ := controller.getHeader(c, "user-agent")
isBrowser := BrowserUserAgentRegex.MatchString(userAgent)
if isBrowser {
tlog.App.Debug().Msg("Request identified as (most likely) coming from a browser")
} else {
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
}
ctx.IsBrowser = isBrowser
return ctx, nil
}