diff --git a/.env.example b/.env.example index 670e5c876..4a430482e 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,8 @@ TINYAUTH_SERVER_PORT=3000 TINYAUTH_SERVER_ADDRESS="0.0.0.0" # The path to the Unix socket. TINYAUTH_SERVER_SOCKETPATH= +# Enable API docs with Scalar under /scalar. +TINYAUTH_SERVER_SCALARENABLED=true # auth config diff --git a/Makefile b/Makefile index 714ceb07c..99cd91887 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c .DEFAULT_GOAL := binary -.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless +.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless swagger swagger-fmt # Deps deps: @@ -102,3 +102,11 @@ docker: # Docker image distroless docker-distroless: docker buildx build -t tinyauthapp/tinyauth:dev-distroless --build-arg=VERSION=$(TAG_NAME) --build-arg=COMMIT_HASH=$(COMMIT_HASH) --build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) -f Dockerfile.distroless . + +# Swagger +swagger: + swag init -d ./internal -g bootstrap/router_bootstrap.go -o ./internal/swagger + +# Swagger Format +swagger-fmt: + swag fmt -d ./internal -g bootstrap/router_bootstrap.go \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index cc5214a39..8f50541d7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -62,6 +62,11 @@ export default defineConfig({ changeOrigin: true, rewrite: (path) => path.replace(/^\/authorize/, ""), }, + "/scalar": { + target: "http://tinyauth-backend:3000/scalar", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/scalar/, ""), + } }, allowedHosts: true, }, diff --git a/go.mod b/go.mod index 0693637f6..eb3bfb216 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.26.4 require ( charm.land/huh/v2 v2.0.3 + charm.land/lipgloss/v2 v2.0.1 github.com/cenkalti/backoff/v5 v5.0.3 github.com/docker/docker v28.5.2+incompatible github.com/gin-gonic/gin v1.12.0 @@ -19,12 +20,15 @@ require ( github.com/rs/zerolog v1.35.1 github.com/steveiliop56/ding v0.2.0 github.com/stretchr/testify v1.11.1 + github.com/swaggo/swag v1.16.6 + github.com/tinyauthapp/gin-scalar v0.0.0-20260704144252-280c60a0cf2c github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 github.com/weppos/publicsuffix-go v0.50.3 go.uber.org/dig v1.19.0 golang.org/x/crypto v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/tools v0.47.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 modernc.org/sqlite v1.53.0 @@ -34,11 +38,11 @@ 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 dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/BurntSushi/toml v1.6.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -82,6 +86,10 @@ require ( github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.23.0 // 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 @@ -98,12 +106,14 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.5 // 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/mailru/easyjson v0.7.7 // 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 @@ -169,7 +179,6 @@ require ( golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect k8s.io/klog/v2 v2.140.0 // indirect diff --git a/go.sum b/go.sum index 17f80f2f7..f417270f1 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -30,6 +32,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= @@ -133,6 +137,7 @@ github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7u github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008= github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -192,10 +197,17 @@ 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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 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.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 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/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 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= @@ -297,8 +309,11 @@ github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -307,6 +322,9 @@ 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/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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= @@ -361,6 +379,7 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -414,6 +433,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -421,6 +441,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc= @@ -445,6 +467,8 @@ github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tinyauthapp/gin-scalar v0.0.0-20260704144252-280c60a0cf2c h1:N91CdjSrEXoFtC+buYFz4CVcOQwAu3u7xHLlzkpuctA= +github.com/tinyauthapp/gin-scalar v0.0.0-20260704144252-280c60a0cf2c/go.mod h1:fIFpOPONYJcCZr0oJBV8kXCVxJu+0Wc73bnkITihYnY= github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ= github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -509,6 +533,7 @@ golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -516,16 +541,22 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= @@ -542,13 +573,19 @@ google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index f0ef90a98..b0e97b161 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -6,17 +6,26 @@ import ( "fmt" "net" "net/http" + "net/url" "os" "time" + ginScalar "github.com/tinyauthapp/gin-scalar" "github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/middleware" "github.com/tinyauthapp/tinyauth/internal/model" + docs "github.com/tinyauthapp/tinyauth/internal/swagger" "go.uber.org/dig" "github.com/gin-gonic/gin" ) +// @title Tinyauth API +// @version development +// @description Documentation for Tinyauth's API. +// @license.name AGPL-3.0 +// @license.url https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE +// @BasePath / func (app *BootstrapApp) setupRouter() error { // we don't want gin debug mode gin.SetMode(gin.ReleaseMode) @@ -80,6 +89,14 @@ func (app *BootstrapApp) setupRouter() error { return fmt.Errorf("failed to provide api router group: %w", err) } + if app.config.Server.ScalarEnabled { + err = app.setupScalar() + + if err != nil { + return fmt.Errorf("failed to setup scalar: %w", err) + } + } + controllerProvideFor := []any{ controller.NewContextController, controller.NewOAuthController, @@ -125,6 +142,34 @@ func (app *BootstrapApp) setupRouter() error { return nil } +func (app *BootstrapApp) setupScalar() error { + appUrl, err := url.Parse(app.runtime.AppURL) + + if err != nil { + return fmt.Errorf("failed to parse app url: %w", err) + } + + docs.SwaggerInfo.Host = appUrl.Host + docs.SwaggerInfo.Schemes = []string{appUrl.Scheme} + docs.SwaggerInfo.Version = model.Version + + type scalarInput struct { + dig.In + + RouterGroup *gin.RouterGroup `name:"mainRouterGroup"` + } + + err = app.dig.Invoke(func(i scalarInput) { + i.RouterGroup.GET("/scalar/*any", ginScalar.WrapHandler(nil)) + }) + + if err != nil { + return fmt.Errorf("failed to invoke scalar: %w", err) + } + + return nil +} + // Top down // 1. Tailscale (if tailscale.listen) // 2. Unix socket (if server.socketPath) diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index abfabaad3..44a26df3b 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -107,6 +107,14 @@ func NewContextController(i ContextControllerInput) *ContextController { return controller } +// UserContext godoc +// +// @Summary User context +// @Description Get the user context +// @Tags context +// @Produce json +// @Success 200 {object} UserContextResponse +// @Router /api/context/user [get] func (controller *ContextController) userContextHandler(c *gin.Context) { context, err := new(model.UserContext).NewFromGin(c) @@ -147,6 +155,14 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { c.JSON(200, userContext) } +// AppContext godoc +// +// @Summary App context +// @Description Get the app context +// @Tags context +// @Produce json +// @Success 200 {object} AppContextResponse +// @Router /api/context/app [get] func (controller *ContextController) appContextHandler(c *gin.Context) { c.JSON(200, AppContextResponse{ Status: 200, diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 8b6ab4f7d..54767818d 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -7,6 +7,10 @@ const ( FrontendLoginForApp FrontendLoginFor = "app" ) +type SimpleResponse struct { + Status int `json:"status"` + Message string `json:"message,omitempty"` +} type UnauthorizedQuery struct { Username string `url:"username"` Resource string `url:"resource"` diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 2b578978d..ca0792acc 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -23,9 +23,18 @@ func NewHealthController(i HealthControllerInput) *HealthController { return controller } +// HealthCheck godoc +// +// @Summary Healthcheck +// @Description Check if the server is up and running +// @Tags health +// @Produce json +// @Success 200 {object} SimpleResponse +// @Router /api/healthz [get] +// @Router /api/healthz [head] func (controller *HealthController) healthHandler(c *gin.Context) { - c.JSON(200, gin.H{ - "status": 200, - "message": "Healthy", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Healthy", }) } diff --git a/internal/controller/health_controller_test.go b/internal/controller/health_controller_test.go index d670f018c..6caf412a4 100644 --- a/internal/controller/health_controller_test.go +++ b/internal/controller/health_controller_test.go @@ -23,9 +23,9 @@ func TestHealthController(t *testing.T) { path: "/api/healthz", method: "GET", expected: func() string { - expectedHealthResponse := map[string]any{ - "status": 200, - "message": "Healthy", + expectedHealthResponse := SimpleResponse{ + Status: 200, + Message: "Healthy", } bytes, err := json.Marshal(expectedHealthResponse) require.NoError(t, err) @@ -37,9 +37,9 @@ func TestHealthController(t *testing.T) { path: "/api/healthz", method: "HEAD", expected: func() string { - expectedHealthResponse := map[string]any{ - "status": 200, - "message": "Healthy", + expectedHealthResponse := SimpleResponse{ + Status: 200, + Message: "Healthy", } bytes, err := json.Marshal(expectedHealthResponse) require.NoError(t, err) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 27fca2067..d0f7e52d9 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -54,6 +54,27 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController { return controller } +type OAuthURLSuccessResponse struct { + SimpleResponse + URL string `json:"url"` +} + +// OAuthURL godoc +// +// @Summary OAuth URL +// @Description Get an OAuth URL for the specified provider +// @Tags oauth +// @Produce json +// @Param id path string true "Provider ID" +// @Param login_for query string false "Login for" +// @Param oidc_ticket query string false "OpenID Connect Ticket" +// @Param oidc_scope query string false "OpenID Connect Scope" +// @Param oidc_name query string false "OpenID Connect Name" +// @Param redirect_uri query string false "Redirect URI" +// @Success 200 {object} OAuthURLSuccessResponse +// @Failure 400 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Router /api/oauth/url/{id} [get] func (controller *OAuthController) oauthURLHandler(c *gin.Context) { var req OAuthRequest @@ -111,23 +132,33 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true) - c.JSON(200, gin.H{ - "status": 200, - "message": "OK", - "url": authUrl, + c.JSON(200, OAuthURLSuccessResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + Message: "OK", + }, + URL: authUrl, }) } +// OAuthCallback godoc +// +// @Summary OAuth Callback +// @Description Callback URL for OAuth providers +// @Tags oauth +// @Param id path string true "Provider ID" +// @Param code query string true "State" +// @Param state query string true "Code" +// @Success 302 +// @Failure 302 +// @Router /api/oauth/callback/{id} [get] func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var req OAuthRequest err := c.BindUri(&req) if err != nil { - controller.log.App.Error().Err(err).Msg("Failed to bind URI") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", - }) + controller.log.App.Error().Err(err).Msg("Failed to get provider ID") + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -135,7 +166,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -145,7 +176,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -154,7 +185,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { state := c.Query("state") if state != oauthPendingSession.State { controller.log.App.Warn().Msg("OAuth state mismatch") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -163,7 +194,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to exchange code for token") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -171,19 +202,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } if user == nil { controller.log.App.Warn().Msg("OAuth provider did not return user info") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } if user.Email == "" { controller.log.App.Warn().Msg("OAuth provider did not return an email") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -191,13 +222,13 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } if svc.ID() != req.Provider { controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID()) - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -211,11 +242,11 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode())) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode())) return } @@ -260,7 +291,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to create session cookie") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -273,10 +304,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { queries, err := query.Values(oauthPendingSession.CallbackParams) if err != nil { controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode())) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode())) return } @@ -288,15 +319,15 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to encode redirect query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode())) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode())) return } - c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL) + c.Redirect(http.StatusFound, controller.runtime.AppURL) } func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool { diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index c40499532..edbdc637b 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -82,6 +82,15 @@ type AuthorizeCompleteRequest struct { Ticket string `json:"ticket" binding:"required"` } +type AuthorizeCompleteResponse struct { + SimpleResponse + RedirectURI string `json:"redirect_uri"` +} + +type OIDCErrorResponse struct { + Error string `json:"error"` +} + type OIDCControllerInput struct { dig.In @@ -114,6 +123,36 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController { // This endpoint does **not** return a code, it handles param validation, ticket creation // and then redirects to the frontend to handle the consent screen. It performs no destructive // actions (like logging out an existing session) +// Authorize godoc +// +// @Summary Authorize +// @Description OpenID Connect Authorize Endpoint +// @Accept x-www-form-urlencoded +// @Tags oidc +// @Param scope query string false "OAuth scopes (space separated, must include openid)" +// @Param response_type query string false "Response type (e.g. code)" +// @Param client_id query string false "Client ID" +// @Param redirect_uri query string false "Redirect URI" +// @Param state query string false "Opaque state value returned to the client" +// @Param nonce query string false "Nonce for ID token replay protection" +// @Param code_challenge query string false "PKCE code challenge" +// @Param code_challenge_method query string false "PKCE code challenge method (S256 or plain)" +// @Param prompt query string false "Prompt parameter (none, login, consent)" +// @Param max_age query string false "Max authentication age in seconds" +// @Param scope formData string false "OAuth scopes (space separated, must include openid)" +// @Param response_type formData string false "Response type (e.g. code)" +// @Param client_id formData string false "Client ID" +// @Param redirect_uri formData string false "Redirect URI" +// @Param state formData string false "Opaque state value returned to the client" +// @Param nonce formData string false "Nonce for ID token replay protection" +// @Param code_challenge formData string false "PKCE code challenge" +// @Param code_challenge_method formData string false "PKCE code challenge method (S256 or plain)" +// @Param prompt formData string false "Prompt parameter (none, login, consent)" +// @Param max_age formData string false "Max authentication age in seconds" +// @Success 302 +// @Failure 302 +// @Router /authorize [get] +// @Router /authorize [post] func (controller *OIDCController) authorize(c *gin.Context) { if controller.oidc == nil { controller.authorizeError(c, authorizeErrorParams{ @@ -261,6 +300,16 @@ func (controller *OIDCController) authorize(c *gin.Context) { // 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. +// AuthorizeComplete godoc +// +// @Summary Authorize Complete +// @Description Internal endpoint for the completion of the OpenID Connect authorization flow +// @Tags oidc +// @Accept json +// @Produce json +// @Success 200 {object} AuthorizeCompleteResponse +// @Failure 500 +// @Router /api/oidc/authorize-complete [post] func (controller *OIDCController) authorizeComplete(c *gin.Context) { if controller.oidc == nil { // For this endpoint we return JSON errors since it's called @@ -361,17 +410,44 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { return } - c.JSON(200, gin.H{ - "status": 200, - "redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), + c.JSON(200, AuthorizeCompleteResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + }, + RedirectURI: fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), }) } +// Token godoc +// +// @Summary Token +// @Description OpenID Connect Token Endpoint +// @Tags oidc +// @Accept x-www-form-urlencoded +// @Produce json +// @Param grant_type query string true "Grant type (authorization_code or refresh_token)" +// @Param code query string false "Authorization code (required for authorization_code grant)" +// @Param redirect_uri query string false "Redirect URI (must match the one from the authorize request)" +// @Param refresh_token query string false "Refresh token (required for refresh_token grant)" +// @Param client_id query string false "Client ID (required if not using Basic auth)" +// @Param client_secret query string false "Client secret (required for confidential clients without Basic auth)" +// @Param code_verifier query string false "PKCE code verifier (required if code_challenge was sent)" +// @Param grant_type formData string false "Grant type (authorization_code or refresh_token)" +// @Param code formData string false "Authorization code (required for authorization_code grant)" +// @Param redirect_uri formData string false "Redirect URI (must match the one from the authorize request)" +// @Param refresh_token formData string false "Refresh token (required for refresh_token grant)" +// @Param client_id formData string false "Client ID (required if not using Basic auth)" +// @Param client_secret formData string false "Client secret (required for confidential clients without Basic auth)" +// @Param code_verifier formData string false "PKCE code verifier (required if code_challenge was sent)" +// @Success 200 {object} service.TokenResponse +// @Failure 400 {object} OIDCErrorResponse +// @Failure 500 {object} OIDCErrorResponse +// @Router /oidc/token [post] func (controller *OIDCController) Token(c *gin.Context) { if controller.oidc == nil { controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured") - c.JSON(500, gin.H{ - "error": "server_error", + c.JSON(500, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -381,8 +457,8 @@ func (controller *OIDCController) Token(c *gin.Context) { err := c.Bind(&req) if err != nil { controller.log.App.Warn().Err(err).Msg("Failed to bind token request") - c.JSON(400, gin.H{ - "error": "invalid_request", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_request", }) return } @@ -390,8 +466,8 @@ func (controller *OIDCController) Token(c *gin.Context) { err = controller.oidc.ValidateGrantType(req.GrantType) if err != nil { controller.log.App.Warn().Err(err).Msg("Invalid grant type") - c.JSON(400, gin.H{ - "error": err.Error(), + c.JSON(400, OIDCErrorResponse{ + Error: err.Error(), }) return } @@ -411,8 +487,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if !ok { controller.log.App.Warn().Msg("Client credentials not found in basic auth") c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`) - c.JSON(400, gin.H{ - "error": "invalid_client", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_client", }) return } @@ -427,16 +503,16 @@ func (controller *OIDCController) Token(c *gin.Context) { if !ok { controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found") - c.JSON(400, gin.H{ - "error": "invalid_client", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_client", }) return } if client.ClientSecret != creds.ClientSecret { controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret") - c.JSON(400, gin.H{ - "error": "invalid_client", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_client", }) return } @@ -457,15 +533,15 @@ func (controller *OIDCController) Token(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code") } - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } controller.log.App.Warn().Msg("Code not found") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } @@ -475,8 +551,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if entry.RedirectURI != req.RedirectURI { controller.log.App.Warn().Msg("Redirect URI does not match") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } @@ -485,8 +561,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if !ok { controller.log.App.Warn().Msg("PKCE validation failed") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } @@ -495,8 +571,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to generate access token") - c.JSON(400, gin.H{ - "error": "server_error", + c.JSON(400, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -508,23 +584,23 @@ func (controller *OIDCController) Token(c *gin.Context) { if err != nil { if errors.Is(err, service.ErrTokenExpired) { controller.log.App.Warn().Msg("Refresh token expired") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } if errors.Is(err, service.ErrInvalidClient) { controller.log.App.Warn().Msg("Refresh token does not belong to client") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } controller.log.App.Error().Err(err).Msg("Failed to refresh access token") - c.JSON(400, gin.H{ - "error": "server_error", + c.JSON(400, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -538,11 +614,25 @@ func (controller *OIDCController) Token(c *gin.Context) { c.JSON(200, tokenResponse) } +// Userinfo godoc +// +// @Summary Userinfo +// @Description OpenID Connect Userinfo Endpoint +// @Accept x-www-form-urlencoded +// @Tags oidc +// @Param access_token formData string false "OpenID Connect Access Token" +// @Produce json +// @Success 200 {object} service.UserinfoResponse +// @Failure 400 {object} OIDCErrorResponse +// @Failure 401 {object} OIDCErrorResponse +// @Failure 500 {object} OIDCErrorResponse +// @Router /oidc/userinfo [get] +// @Router /oidc/userinfo [post] func (controller *OIDCController) Userinfo(c *gin.Context) { if controller.oidc == nil { controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured") - c.JSON(500, gin.H{ - "error": "server_error", + c.JSON(500, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -554,16 +644,16 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { tokenType, bearerToken, ok := strings.Cut(authorization, " ") if !ok { controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } if strings.ToLower(tokenType) != "bearer" { controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } @@ -572,23 +662,23 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { } else if c.Request.Method == http.MethodPost { if c.ContentType() != "application/x-www-form-urlencoded" { controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type") - c.JSON(400, gin.H{ - "error": "invalid_request", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_request", }) return } token = c.PostForm("access_token") if token == "" { controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } } else { controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } @@ -598,15 +688,15 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { if err != nil { if errors.Is(err, service.ErrTokenNotFound) { controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token") - c.JSON(401, gin.H{ - "error": "invalid_grant", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_grant", }) return } controller.log.App.Error().Err(err).Msg("Failed to get access token") - c.JSON(401, gin.H{ - "error": "server_error", + c.JSON(401, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -614,8 +704,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { // If we don't have the openid scope, return an error if !slices.Contains(strings.Split(entry.Scope, " "), "openid") { controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope") - c.JSON(401, gin.H{ - "error": "invalid_scope", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_scope", }) return } @@ -626,8 +716,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get user info") - c.JSON(401, gin.H{ - "error": "server_error", + c.JSON(401, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -662,9 +752,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode()) if params.json { - c.JSON(200, gin.H{ - "status": 200, - "redirect_uri": redirectUrl, + c.JSON(200, AuthorizeCompleteResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + }, + RedirectURI: redirectUrl, }) return } @@ -694,9 +786,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz } if params.json { - c.JSON(200, gin.H{ - "status": 200, - "redirect_uri": redirectUrl, + c.JSON(200, AuthorizeCompleteResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + }, + RedirectURI: redirectUrl, }) return } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index ffafaffd5..4ee15a0a2 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -86,15 +86,38 @@ func NewProxyController(i ProxyControllerInput) *ProxyController { return controller } +// Proxy godoc +// +// @Summary Proxy +// @Description Forward-Auth Proxy Endpoint +// @Tags forward-auth +// @Produce json +// @Param proxy path string true "Proxy Name" +// @Success 200 {object} SimpleResponse +// @Failure 302 +// @Failure 400 {object} SimpleResponse +// @Failure 401 {object} SimpleResponse +// @Failure 403 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Router /api/auth/traefik [get] +// @Router /api/auth/caddy [get] +// @Router /api/auth/nginx [get] +// @Router /api/auth/envoy [get] +// @Router /api/auth/envoy [post] +// @Router /api/auth/envoy [head] +// @Router /api/auth/envoy [put] +// @Router /api/auth/envoy [patch] +// @Router /api/auth/envoy [delete] +// @Router /api/auth/envoy [options] func (controller *ProxyController) proxyHandler(c *gin.Context) { // Load proxy context based on the request type proxyCtx, err := controller.getProxyContext(c) if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get proxy context from request") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad request", + c.JSON(400, SimpleResponse{ + Status: 400, + Message: "Bad request", }) return } @@ -118,9 +141,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) { controller.setHeaders(c, acls) - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Authenticated", }) return } @@ -128,9 +151,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) { controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication") controller.setHeaders(c, acls) - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Authenticated", }) return } @@ -151,9 +174,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !controller.useBrowserResponse(proxyCtx) { c.Header("x-tinyauth-location", redirectURL) - c.JSON(403, gin.H{ - "status": 403, - "message": "Forbidden", + c.JSON(403, SimpleResponse{ + Status: 403, + Message: "Forbidden", }) return } @@ -200,9 +223,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !controller.useBrowserResponse(proxyCtx) { c.Header("x-tinyauth-location", redirectURL) - c.JSON(403, gin.H{ - "status": 403, - "message": "Forbidden", + c.JSON(403, SimpleResponse{ + Status: 403, + Message: "Forbidden", }) return } @@ -244,9 +267,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !controller.useBrowserResponse(proxyCtx) { c.Header("x-tinyauth-location", redirectURL) - c.JSON(403, gin.H{ - "status": 403, - "message": "Forbidden", + c.JSON(403, SimpleResponse{ + Status: 403, + Message: "Forbidden", }) return } @@ -271,9 +294,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { controller.setHeaders(c, acls) - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Authenticated", }) return } @@ -293,9 +316,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !controller.useBrowserResponse(proxyCtx) { c.Header("x-tinyauth-location", redirectURL) - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -329,9 +352,9 @@ func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyCon if !controller.useBrowserResponse(proxyCtx) { c.Header("x-tinyauth-location", redirectURL) - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go index f4b720ed0..cb6a60c57 100644 --- a/internal/controller/resources_controller.go +++ b/internal/controller/resources_controller.go @@ -33,18 +33,28 @@ func NewResourcesController(i ResourcesControllerInput) *ResourcesController { return controller } +// Resources godoc +// +// @Summary Resources Endpoint +// @Description Get a resource by file name +// @Tags resources +// @Param resource path string true "Resource Name" +// @Success 200 +// @Failure 404 {object} SimpleResponse +// @Failure 403 {object} SimpleResponse +// @Router /resources/{resource} [get] func (controller *ResourcesController) resourcesHandler(c *gin.Context) { if controller.config.Resources.Path == "" { - c.JSON(404, gin.H{ - "status": 404, - "message": "Resource not found", + c.JSON(404, SimpleResponse{ + Status: 404, + Message: "Resource not found", }) return } if !controller.config.Resources.Enabled { - c.JSON(403, gin.H{ - "status": 403, - "message": "Resources are disabled", + c.JSON(403, SimpleResponse{ + Status: 403, + Message: "Resources are disabled", }) return } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index ae6c23bf5..fd7bf12ae 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -32,6 +32,11 @@ type UserController struct { auth *service.AuthService } +type TotpPendingResponse struct { + SimpleResponse + TotpPending bool `json:"totpPending"` +} + type UserControllerInput struct { dig.In @@ -57,15 +62,29 @@ func NewUserController(i UserControllerInput) *UserController { return controller } +// Login godoc +// +// @Summary Login +// @Description Login Endpoint +// @Tags accounts +// @Accept json +// @Produce json +// @Success 200 {object} SimpleResponse +// @Success 200 {object} TotpPendingResponse +// @Failure 400 {object} SimpleResponse +// @Failure 401 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Failure 429 {object} SimpleResponse +// @Router /api/user/login [post] func (controller *UserController) loginHandler(c *gin.Context) { var req LoginRequest err := c.ShouldBindJSON(&req) if err != nil { controller.log.App.Error().Err(err).Msg("Failed to bind JSON") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", + c.JSON(400, SimpleResponse{ + Status: 400, + Message: "Bad Request", }) return } @@ -79,9 +98,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "account locked") c.Writer.Header().Add("x-tinyauth-lock-locked", "true") c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) - c.JSON(429, gin.H{ - "status": 429, - "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining), + c.JSON(429, SimpleResponse{ + Status: 429, + Message: fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining), }) return } @@ -93,16 +112,16 @@ func (controller *UserController) loginHandler(c *gin.Context) { controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt") controller.auth.RecordLoginAttempt(req.Username, false) controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Error searching for user during login attempt") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } @@ -115,9 +134,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { } else { controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password") } - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -129,9 +148,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { if localUser == nil { controller.log.App.Error().Str("username", req.Username).Msg("Local user not found after successful password verification") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -159,19 +178,21 @@ func (controller *UserController) loginHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create pending TOTP session") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } http.SetCookie(c.Writer, cookie) - c.JSON(200, gin.H{ - "status": 200, - "message": "TOTP required", - "totpPending": true, + c.JSON(200, TotpPendingResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + Message: "TOTP required", + }, + TotpPending: true, }) return } @@ -204,9 +225,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create session cookie after successful login") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } @@ -223,12 +244,21 @@ func (controller *UserController) loginHandler(c *gin.Context) { controller.auth.RecordLoginAttempt(req.Username, true) - c.JSON(200, gin.H{ - "status": 200, - "message": "Login successful", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Login successful", }) } +// Logout godoc +// +// @Summary Logout +// @Description Logout Endpoint +// @Tags accounts +// @Produce json +// @Success 200 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Router /api/user/logout [post] func (controller *UserController) logoutHandler(c *gin.Context) { controller.log.App.Debug().Msg("Logout attempt") @@ -237,16 +267,16 @@ func (controller *UserController) logoutHandler(c *gin.Context) { if err != nil { if errors.Is(err, http.ErrNoCookie) { controller.log.App.Warn().Msg("Logout attempt without session cookie, treating as successful logout") - c.JSON(200, gin.H{ - "status": 200, - "message": "Logout successful", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Logout successful", }) return } controller.log.App.Error().Err(err).Msg("Error retrieving session cookie on logout") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } @@ -255,9 +285,9 @@ func (controller *UserController) logoutHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Error deleting session on logout") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } @@ -273,21 +303,34 @@ func (controller *UserController) logoutHandler(c *gin.Context) { http.SetCookie(c.Writer, cookie) - c.JSON(200, gin.H{ - "status": 200, - "message": "Logout successful", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Logout successful", }) } +// TOTP godoc +// +// @Summary TOTP +// @Description TOTP Endpoint +// @Tags accounts +// @Accept json +// @Produce json +// @Success 200 {object} SimpleResponse +// @Failure 400 {object} SimpleResponse +// @Failure 401 {object} SimpleResponse +// @Failure 429 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Router /api/user/totp [post] func (controller *UserController) totpHandler(c *gin.Context) { var req TotpRequest err := c.ShouldBindJSON(&req) if err != nil { controller.log.App.Error().Err(err).Msg("Failed to bind JSON for TOTP verification") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", + c.JSON(400, SimpleResponse{ + Status: 400, + Message: "Bad Request", }) return } @@ -297,25 +340,25 @@ func (controller *UserController) totpHandler(c *gin.Context) { if err != nil { if errors.Is(err, model.ErrUserContextNotFound) { controller.log.App.Warn().Msg("TOTP verification attempt without user context") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } if !context.TOTPPending() { controller.log.App.Warn().Str("username", context.GetUsername()).Msg("TOTP verification attempt without pending TOTP session") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -329,9 +372,9 @@ func (controller *UserController) totpHandler(c *gin.Context) { controller.log.AuditLoginFailure(context.GetUsername(), "local", c.ClientIP(), "account locked") c.Writer.Header().Add("x-tinyauth-lock-locked", "true") c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) - c.JSON(429, gin.H{ - "status": 429, - "message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining), + c.JSON(429, SimpleResponse{ + Status: 429, + Message: fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining), }) return } @@ -340,9 +383,9 @@ func (controller *UserController) totpHandler(c *gin.Context) { if user == nil { controller.log.App.Error().Str("username", context.GetUsername()).Msg("Local user not found during TOTP verification") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -353,9 +396,9 @@ func (controller *UserController) totpHandler(c *gin.Context) { controller.log.App.Warn().Str("username", context.GetUsername()).Msg("Invalid TOTP code during verification attempt") controller.auth.RecordLoginAttempt(context.GetUsername(), false) controller.log.AuditLoginFailure(context.GetUsername(), "local", c.ClientIP(), "invalid TOTP code") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -391,9 +434,9 @@ func (controller *UserController) totpHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful TOTP verification") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } @@ -403,37 +446,48 @@ func (controller *UserController) totpHandler(c *gin.Context) { controller.log.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful, login complete") controller.log.AuditLoginSuccess(context.GetUsername(), "local", c.ClientIP()) - c.JSON(200, gin.H{ - "status": 200, - "message": "Login successful", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Login successful", }) } +// Tailscale godoc +// +// @Summary Tailscale +// @Description Tailscale Auth Endpoint (Experimental) +// @Tags accounts +// @Accept json +// @Produce json +// @Success 200 {object} SimpleResponse +// @Failure 401 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Router /api/user/tailscale [post] func (controller *UserController) tailscaleHandler(c *gin.Context) { context, err := new(model.UserContext).NewFromGin(c) if err != nil { if errors.Is(err, model.ErrUserContextNotFound) { controller.log.App.Warn().Msg("Tailscale login attempt without user context") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } controller.log.App.Error().Err(err).Msg("Failed to create user context from request") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } if context.Tailscale == nil { controller.log.App.Warn().Msg("Tailscale login attempt without Tailscale context") - c.JSON(401, gin.H{ - "status": 401, - "message": "Unauthorized", + c.JSON(401, SimpleResponse{ + Status: 401, + Message: "Unauthorized", }) return } @@ -449,9 +503,9 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful Tailscale login") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "Internal Server Error", }) return } @@ -461,8 +515,8 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) { controller.log.App.Info().Str("username", context.GetUsername()).Msg("Tailscale login successful, login complete") controller.log.AuditLoginSuccess(context.GetUsername(), "tailscale", c.ClientIP()) - c.JSON(200, gin.H{ - "status": 200, - "message": "Login successful", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "Login successful", }) } diff --git a/internal/controller/well_known_controller.go b/internal/controller/well_known_controller.go index a32c3a065..65d647dc2 100644 --- a/internal/controller/well_known_controller.go +++ b/internal/controller/well_known_controller.go @@ -58,18 +58,27 @@ func NewWellKnownController(i WellKnownControllerInput) *WellKnownController { oidc: i.OIDCService, } - i.RouterGroup.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration) - i.RouterGroup.GET("/.well-known/jwks.json", controller.JWKS) - i.RouterGroup.GET("/.well-known/webfinger", controller.WebFinger) + i.RouterGroup.GET("/.well-known/openid-configuration", controller.openIDConnectConfiguration) + i.RouterGroup.GET("/.well-known/jwks.json", controller.jwks) + i.RouterGroup.GET("/.well-known/webfinger", controller.webFinger) return controller } -func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) { +// OpenIDConnectConfiguration godoc +// +// @Summary OpenID Connect Configuration +// @Description OpenID Connect Configuration Discovery Endpoint +// @Tags well-known +// @Produce json +// @Success 200 {object} OpenIDConnectConfiguration +// @Failure 500 {object} SimpleResponse +// @Router /.well-known/openid-configuration [get] +func (controller *WellKnownController) openIDConnectConfiguration(c *gin.Context) { if controller.oidc == nil { - c.JSON(500, gin.H{ - "status": 500, - "message": "OIDC service not configured", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "OIDC service not configured", }) return } @@ -94,11 +103,20 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context }) } -func (controller *WellKnownController) JWKS(c *gin.Context) { +// JWKS godoc +// +// @Summary JWKS +// @Description JWKS Endpoint +// @Tags well-known +// @Produce json +// @Success 200 +// @Failure 500 {object} SimpleResponse +// @Router /.well-known/jwks.json [get] +func (controller *WellKnownController) jwks(c *gin.Context) { if controller.oidc == nil { - c.JSON(500, gin.H{ - "status": 500, - "message": "OIDC service not configured", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "OIDC service not configured", }) return } @@ -106,9 +124,9 @@ func (controller *WellKnownController) JWKS(c *gin.Context) { jwks, err := controller.oidc.GetJWK() if err != nil { - c.JSON(500, gin.H{ - "status": 500, - "message": "failed to get JWK", + c.JSON(500, SimpleResponse{ + Status: 500, + Message: "failed to get JWK", }) return } @@ -122,16 +140,27 @@ func (controller *WellKnownController) JWKS(c *gin.Context) { c.Status(http.StatusOK) } -func (controller *WellKnownController) WebFinger(c *gin.Context) { +// WebFinger godoc +// +// @Summary WebFinger +// @Description WebFinger Endpoint +// @Tags well-known +// @Produce json +// @Param resource query string true "Resource" +// @Param rel query string false "Rel" +// @Success 200 {object} WebfingerResponse +// @Failure 400 {object} SimpleResponse +// @Router /.well-known/webfinger [get] +func (controller *WellKnownController) webFinger(c *gin.Context) { c.Header("Content-Type", "application/jrd+json") c.Header("Access-Control-Allow-Origin", "*") resource := c.Query("resource") if !controller.validateWebFingerResource(resource) { - c.JSON(400, gin.H{ - "status": 400, - "message": "invalid resource", + c.JSON(400, SimpleResponse{ + Status: 400, + Message: "invalid resource", }) return } diff --git a/internal/middleware/ui_middleware.go b/internal/middleware/ui_middleware.go index 3b706ecda..324c8ddaf 100644 --- a/internal/middleware/ui_middleware.go +++ b/internal/middleware/ui_middleware.go @@ -44,7 +44,7 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc { path := strings.TrimPrefix(c.Request.URL.Path, "/") switch strings.SplitN(path, "/", 2)[0] { - case "api", "resources", ".well-known", "authorize": + case "api", "resources", ".well-known", "authorize", "scalar": c.Next() return case "robots.txt": diff --git a/internal/model/config.go b/internal/model/config.go index b72c96568..66c42b31e 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -34,8 +34,9 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config { Path: "./resources", }, Server: ServerConfig{ - Port: 3000, - Address: "0.0.0.0", + Port: 3000, + Address: "0.0.0.0", + ScalarEnabled: true, }, Auth: AuthConfig{ SubdomainsEnabled: true, @@ -134,9 +135,10 @@ type ResourcesConfig struct { } type ServerConfig struct { - Port int `description:"The port on which the server listens." yaml:"port,omitempty"` - Address string `description:"The address on which the server listens." yaml:"address,omitempty"` - SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"` + Port int `description:"The port on which the server listens." yaml:"port,omitempty"` + Address string `description:"The address on which the server listens." yaml:"address,omitempty"` + SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"` + ScalarEnabled bool `description:"Enable API docs with Scalar under /scalar." yaml:"scalarEnabled,omitempty"` } type AuthConfig struct { diff --git a/internal/swagger/docs.go b/internal/swagger/docs.go new file mode 100644 index 000000000..a3f09176d --- /dev/null +++ b/internal/swagger/docs.go @@ -0,0 +1,1921 @@ +// Package swagger Code generated by swaggo/swag. DO NOT EDIT +package swagger + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": { + "name": "AGPL-3.0", + "url": "https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/.well-known/jwks.json": { + "get": { + "description": "JWKS Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "well-known" + ], + "summary": "JWKS", + "responses": { + "200": { + "description": "OK" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/.well-known/openid-configuration": { + "get": { + "description": "OpenID Connect Configuration Discovery Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "well-known" + ], + "summary": "OpenID Connect Configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.OpenIDConnectConfiguration" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/.well-known/webfinger": { + "get": { + "description": "WebFinger Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "well-known" + ], + "summary": "WebFinger", + "parameters": [ + { + "type": "string", + "description": "Resource", + "name": "resource", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Rel", + "name": "rel", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.WebfingerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/caddy": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/envoy": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "put": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "post": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "delete": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "options": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "head": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "patch": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/nginx": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/traefik": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/context/app": { + "get": { + "description": "Get the app context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "App context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AppContextResponse" + } + } + } + } + }, + "/api/context/user": { + "get": { + "description": "Get the user context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "User context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.UserContextResponse" + } + } + } + } + }, + "/api/healthz": { + "get": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "head": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oauth/callback/{id}": { + "get": { + "description": "Callback URL for OAuth providers", + "tags": [ + "oauth" + ], + "summary": "OAuth Callback", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "State", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Code", + "name": "state", + "in": "query", + "required": true + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/api/oauth/url/{id}": { + "get": { + "description": "Get an OAuth URL for the specified provider", + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "OAuth URL", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Login for", + "name": "login_for", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Ticket", + "name": "oidc_ticket", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Scope", + "name": "oidc_scope", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Name", + "name": "oidc_name", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.OAuthURLSuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oidc/authorize-complete": { + "post": { + "description": "Internal endpoint for the completion of the OpenID Connect authorization flow", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize Complete", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AuthorizeCompleteResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/user/login": { + "post": { + "description": "Login Endpoint", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Login", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.TotpPendingResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/user/logout": { + "post": { + "description": "Logout Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/user/tailscale": { + "post": { + "description": "Tailscale Auth Endpoint (Experimental)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Tailscale", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/user/totp": { + "post": { + "description": "TOTP Endpoint", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "TOTP", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/authorize": { + "get": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + }, + "post": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/oidc/token": { + "post": { + "description": "OpenID Connect Token Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Token", + "parameters": [ + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "query" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "query" + }, + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "formData" + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.TokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + }, + "/oidc/userinfo": { + "get": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + }, + "post": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + }, + "/resources/{resource}": { + "get": { + "description": "Get a resource by file name", + "tags": [ + "resources" + ], + "summary": "Resources Endpoint", + "parameters": [ + { + "type": "string", + "description": "Resource Name", + "name": "resource", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + } + }, + "definitions": { + "controller.ACRApp": { + "type": "object", + "properties": { + "appUrl": { + "type": "string" + }, + "cookieDomain": { + "type": "string" + }, + "subdomainsEnabled": { + "type": "boolean" + } + } + }, + "controller.ACRAuth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Provider" + } + } + } + }, + "controller.ACROAuth": { + "type": "object", + "properties": { + "autoRedirect": { + "type": "string" + } + } + }, + "controller.ACRUI": { + "type": "object", + "properties": { + "backgroundImage": { + "type": "string" + }, + "forgotPasswordMessage": { + "type": "string" + }, + "title": { + "type": "string" + }, + "warningsEnabled": { + "type": "boolean" + } + } + }, + "controller.AppContextResponse": { + "type": "object", + "properties": { + "app": { + "$ref": "#/definitions/controller.ACRApp" + }, + "auth": { + "$ref": "#/definitions/controller.ACRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.ACROAuth" + }, + "status": { + "type": "integer" + }, + "ui": { + "$ref": "#/definitions/controller.ACRUI" + } + } + }, + "controller.AuthorizeCompleteResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.OAuthURLSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "controller.OIDCErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "controller.OpenIDConnectConfiguration": { + "type": "object", + "properties": { + "authorization_endpoint": { + "type": "string" + }, + "claims_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "grant_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "id_token_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuer": { + "type": "string" + }, + "jwks_uri": { + "type": "string" + }, + "request_object_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "request_parameter_supported": { + "type": "boolean" + }, + "response_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "scopes_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "service_documentation": { + "type": "string" + }, + "subject_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint": { + "type": "string" + }, + "token_endpoint_auth_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "userinfo_endpoint": { + "type": "string" + } + } + }, + "controller.SimpleResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.TotpPendingResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "totpPending": { + "type": "boolean" + } + } + }, + "controller.UCRAuth": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "controller.UCROAuth": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "displayName": { + "type": "string" + } + } + }, + "controller.UCRTOTP": { + "type": "object", + "properties": { + "pending": { + "type": "boolean" + } + } + }, + "controller.UCRTailscale": { + "type": "object", + "properties": { + "nodeName": { + "type": "string" + } + } + }, + "controller.UserContextResponse": { + "type": "object", + "properties": { + "auth": { + "$ref": "#/definitions/controller.UCRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.UCROAuth" + }, + "status": { + "type": "integer" + }, + "tailscale": { + "$ref": "#/definitions/controller.UCRTailscale" + }, + "totp": { + "$ref": "#/definitions/controller.UCRTOTP" + } + } + }, + "controller.WebfingerResponse": { + "type": "object", + "properties": { + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/controller.WebfingerResponseLink" + } + }, + "subject": { + "type": "string" + } + } + }, + "controller.WebfingerResponseLink": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "rel": { + "type": "string" + } + } + }, + "model.AddressClaim": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "formatted": { + "type": "string" + }, + "locality": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "region": { + "type": "string" + }, + "street_address": { + "type": "string" + } + } + }, + "model.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "oauth": { + "type": "boolean" + } + } + }, + "service.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "id_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "service.UserinfoResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/model.AddressClaim" + }, + "birthdate": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "family_name": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "given_name": { + "type": "string" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "locale": { + "type": "string" + }, + "middle_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_number_verified": { + "type": "boolean" + }, + "picture": { + "type": "string" + }, + "preferred_username": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "website": { + "type": "string" + }, + "zoneinfo": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "development", + Host: "", + BasePath: "/", + Schemes: []string{}, + Title: "Tinyauth API", + Description: "Swagger documentation for Tinyauth's API.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/internal/swagger/swagger.json b/internal/swagger/swagger.json new file mode 100644 index 000000000..cd12591d2 --- /dev/null +++ b/internal/swagger/swagger.json @@ -0,0 +1,1896 @@ +{ + "swagger": "2.0", + "info": { + "description": "Swagger documentation for Tinyauth's API.", + "title": "Tinyauth API", + "contact": {}, + "license": { + "name": "AGPL-3.0", + "url": "https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE" + }, + "version": "development" + }, + "basePath": "/", + "paths": { + "/.well-known/jwks.json": { + "get": { + "description": "JWKS Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "well-known" + ], + "summary": "JWKS", + "responses": { + "200": { + "description": "OK" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/.well-known/openid-configuration": { + "get": { + "description": "OpenID Connect Configuration Discovery Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "well-known" + ], + "summary": "OpenID Connect Configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.OpenIDConnectConfiguration" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/.well-known/webfinger": { + "get": { + "description": "WebFinger Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "well-known" + ], + "summary": "WebFinger", + "parameters": [ + { + "type": "string", + "description": "Resource", + "name": "resource", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Rel", + "name": "rel", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.WebfingerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/caddy": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/envoy": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "put": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "post": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "delete": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "options": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "head": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "patch": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/nginx": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/auth/traefik": { + "get": { + "description": "Forward-Auth Proxy Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "forward-auth" + ], + "summary": "Proxy", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "302": { + "description": "Found" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/context/app": { + "get": { + "description": "Get the app context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "App context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AppContextResponse" + } + } + } + } + }, + "/api/context/user": { + "get": { + "description": "Get the user context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "User context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.UserContextResponse" + } + } + } + } + }, + "/api/healthz": { + "get": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "head": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oauth/callback/{id}": { + "get": { + "description": "Callback URL for OAuth providers", + "tags": [ + "oauth" + ], + "summary": "OAuth Callback", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "State", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Code", + "name": "state", + "in": "query", + "required": true + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/api/oauth/url/{id}": { + "get": { + "description": "Get an OAuth URL for the specified provider", + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "OAuth URL", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Login for", + "name": "login_for", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Ticket", + "name": "oidc_ticket", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Scope", + "name": "oidc_scope", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Name", + "name": "oidc_name", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.OAuthURLSuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oidc/authorize-complete": { + "post": { + "description": "Internal endpoint for the completion of the OpenID Connect authorization flow", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize Complete", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AuthorizeCompleteResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/user/login": { + "post": { + "description": "Login Endpoint", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Login", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.TotpPendingResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/user/logout": { + "post": { + "description": "Logout Endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/user/tailscale": { + "post": { + "description": "Tailscale Auth Endpoint (Experimental)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Tailscale", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/user/totp": { + "post": { + "description": "TOTP Endpoint", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "TOTP", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/authorize": { + "get": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + }, + "post": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/oidc/token": { + "post": { + "description": "OpenID Connect Token Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Token", + "parameters": [ + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "query" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "query" + }, + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "formData" + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.TokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + }, + "/oidc/userinfo": { + "get": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + }, + "post": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + }, + "/resources/{resource}": { + "get": { + "description": "Get a resource by file name", + "tags": [ + "resources" + ], + "summary": "Resources Endpoint", + "parameters": [ + { + "type": "string", + "description": "Resource Name", + "name": "resource", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + } + }, + "definitions": { + "controller.ACRApp": { + "type": "object", + "properties": { + "appUrl": { + "type": "string" + }, + "cookieDomain": { + "type": "string" + }, + "subdomainsEnabled": { + "type": "boolean" + } + } + }, + "controller.ACRAuth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Provider" + } + } + } + }, + "controller.ACROAuth": { + "type": "object", + "properties": { + "autoRedirect": { + "type": "string" + } + } + }, + "controller.ACRUI": { + "type": "object", + "properties": { + "backgroundImage": { + "type": "string" + }, + "forgotPasswordMessage": { + "type": "string" + }, + "title": { + "type": "string" + }, + "warningsEnabled": { + "type": "boolean" + } + } + }, + "controller.AppContextResponse": { + "type": "object", + "properties": { + "app": { + "$ref": "#/definitions/controller.ACRApp" + }, + "auth": { + "$ref": "#/definitions/controller.ACRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.ACROAuth" + }, + "status": { + "type": "integer" + }, + "ui": { + "$ref": "#/definitions/controller.ACRUI" + } + } + }, + "controller.AuthorizeCompleteResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.OAuthURLSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "controller.OIDCErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "controller.OpenIDConnectConfiguration": { + "type": "object", + "properties": { + "authorization_endpoint": { + "type": "string" + }, + "claims_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "grant_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "id_token_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuer": { + "type": "string" + }, + "jwks_uri": { + "type": "string" + }, + "request_object_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "request_parameter_supported": { + "type": "boolean" + }, + "response_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "scopes_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "service_documentation": { + "type": "string" + }, + "subject_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint": { + "type": "string" + }, + "token_endpoint_auth_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "userinfo_endpoint": { + "type": "string" + } + } + }, + "controller.SimpleResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.TotpPendingResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "totpPending": { + "type": "boolean" + } + } + }, + "controller.UCRAuth": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "controller.UCROAuth": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "displayName": { + "type": "string" + } + } + }, + "controller.UCRTOTP": { + "type": "object", + "properties": { + "pending": { + "type": "boolean" + } + } + }, + "controller.UCRTailscale": { + "type": "object", + "properties": { + "nodeName": { + "type": "string" + } + } + }, + "controller.UserContextResponse": { + "type": "object", + "properties": { + "auth": { + "$ref": "#/definitions/controller.UCRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.UCROAuth" + }, + "status": { + "type": "integer" + }, + "tailscale": { + "$ref": "#/definitions/controller.UCRTailscale" + }, + "totp": { + "$ref": "#/definitions/controller.UCRTOTP" + } + } + }, + "controller.WebfingerResponse": { + "type": "object", + "properties": { + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/controller.WebfingerResponseLink" + } + }, + "subject": { + "type": "string" + } + } + }, + "controller.WebfingerResponseLink": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "rel": { + "type": "string" + } + } + }, + "model.AddressClaim": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "formatted": { + "type": "string" + }, + "locality": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "region": { + "type": "string" + }, + "street_address": { + "type": "string" + } + } + }, + "model.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "oauth": { + "type": "boolean" + } + } + }, + "service.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "id_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "service.UserinfoResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/model.AddressClaim" + }, + "birthdate": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "family_name": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "given_name": { + "type": "string" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "locale": { + "type": "string" + }, + "middle_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_number_verified": { + "type": "boolean" + }, + "picture": { + "type": "string" + }, + "preferred_username": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "website": { + "type": "string" + }, + "zoneinfo": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/internal/swagger/swagger.yaml b/internal/swagger/swagger.yaml new file mode 100644 index 000000000..87e6252b4 --- /dev/null +++ b/internal/swagger/swagger.yaml @@ -0,0 +1,1252 @@ +basePath: / +definitions: + controller.ACRApp: + properties: + appUrl: + type: string + cookieDomain: + type: string + subdomainsEnabled: + type: boolean + type: object + controller.ACRAuth: + properties: + providers: + items: + $ref: '#/definitions/model.Provider' + type: array + type: object + controller.ACROAuth: + properties: + autoRedirect: + type: string + type: object + controller.ACRUI: + properties: + backgroundImage: + type: string + forgotPasswordMessage: + type: string + title: + type: string + warningsEnabled: + type: boolean + type: object + controller.AppContextResponse: + properties: + app: + $ref: '#/definitions/controller.ACRApp' + auth: + $ref: '#/definitions/controller.ACRAuth' + message: + type: string + oauth: + $ref: '#/definitions/controller.ACROAuth' + status: + type: integer + ui: + $ref: '#/definitions/controller.ACRUI' + type: object + controller.AuthorizeCompleteResponse: + properties: + message: + type: string + redirect_uri: + type: string + status: + type: integer + type: object + controller.OAuthURLSuccessResponse: + properties: + message: + type: string + status: + type: integer + url: + type: string + type: object + controller.OIDCErrorResponse: + properties: + error: + type: string + type: object + controller.OpenIDConnectConfiguration: + properties: + authorization_endpoint: + type: string + claims_supported: + items: + type: string + type: array + grant_types_supported: + items: + type: string + type: array + id_token_signing_alg_values_supported: + items: + type: string + type: array + issuer: + type: string + jwks_uri: + type: string + request_object_signing_alg_values_supported: + items: + type: string + type: array + request_parameter_supported: + type: boolean + response_types_supported: + items: + type: string + type: array + scopes_supported: + items: + type: string + type: array + service_documentation: + type: string + subject_types_supported: + items: + type: string + type: array + token_endpoint: + type: string + token_endpoint_auth_methods_supported: + items: + type: string + type: array + userinfo_endpoint: + type: string + type: object + controller.SimpleResponse: + properties: + message: + type: string + status: + type: integer + type: object + controller.TotpPendingResponse: + properties: + message: + type: string + status: + type: integer + totpPending: + type: boolean + type: object + controller.UCRAuth: + properties: + authenticated: + type: boolean + email: + type: string + name: + type: string + providerId: + type: string + username: + type: string + type: object + controller.UCROAuth: + properties: + active: + type: boolean + displayName: + type: string + type: object + controller.UCRTOTP: + properties: + pending: + type: boolean + type: object + controller.UCRTailscale: + properties: + nodeName: + type: string + type: object + controller.UserContextResponse: + properties: + auth: + $ref: '#/definitions/controller.UCRAuth' + message: + type: string + oauth: + $ref: '#/definitions/controller.UCROAuth' + status: + type: integer + tailscale: + $ref: '#/definitions/controller.UCRTailscale' + totp: + $ref: '#/definitions/controller.UCRTOTP' + type: object + controller.WebfingerResponse: + properties: + links: + items: + $ref: '#/definitions/controller.WebfingerResponseLink' + type: array + subject: + type: string + type: object + controller.WebfingerResponseLink: + properties: + href: + type: string + rel: + type: string + type: object + model.AddressClaim: + properties: + country: + type: string + formatted: + type: string + locality: + type: string + postal_code: + type: string + region: + type: string + street_address: + type: string + type: object + model.Provider: + properties: + id: + type: string + name: + type: string + oauth: + type: boolean + type: object + service.TokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + id_token: + type: string + refresh_token: + type: string + scope: + type: string + token_type: + type: string + type: object + service.UserinfoResponse: + properties: + address: + $ref: '#/definitions/model.AddressClaim' + birthdate: + type: string + email: + type: string + email_verified: + type: boolean + family_name: + type: string + gender: + type: string + given_name: + type: string + groups: + items: + type: string + type: array + locale: + type: string + middle_name: + type: string + name: + type: string + nickname: + type: string + phone_number: + type: string + phone_number_verified: + type: boolean + picture: + type: string + preferred_username: + type: string + profile: + type: string + sub: + type: string + updated_at: + type: integer + website: + type: string + zoneinfo: + type: string + type: object +info: + contact: {} + description: Swagger documentation for Tinyauth's API. + license: + name: AGPL-3.0 + url: https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE + title: Tinyauth API + version: development +paths: + /.well-known/jwks.json: + get: + description: JWKS Endpoint + produces: + - application/json + responses: + "200": + description: OK + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: JWKS + tags: + - well-known + /.well-known/openid-configuration: + get: + description: OpenID Connect Configuration Discovery Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.OpenIDConnectConfiguration' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: OpenID Connect Configuration + tags: + - well-known + /.well-known/webfinger: + get: + description: WebFinger Endpoint + parameters: + - description: Resource + in: query + name: resource + required: true + type: string + - description: Rel + in: query + name: rel + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.WebfingerResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: WebFinger + tags: + - well-known + /api/auth/caddy: + get: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + /api/auth/envoy: + delete: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + get: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + head: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + options: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + patch: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + post: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + put: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + /api/auth/nginx: + get: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + /api/auth/traefik: + get: + description: Forward-Auth Proxy Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "302": + description: Found + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Proxy + tags: + - forward-auth + /api/context/app: + get: + description: Get the app context + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.AppContextResponse' + summary: App context + tags: + - context + /api/context/user: + get: + description: Get the user context + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.UserContextResponse' + summary: User context + tags: + - context + /api/healthz: + get: + description: Check if the server is up and running + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Healthcheck + tags: + - health + head: + description: Check if the server is up and running + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Healthcheck + tags: + - health + /api/oauth/callback/{id}: + get: + description: Callback URL for OAuth providers + parameters: + - description: Provider ID + in: path + name: id + required: true + type: string + - description: State + in: query + name: code + required: true + type: string + - description: Code + in: query + name: state + required: true + type: string + responses: + "302": + description: Found + summary: OAuth Callback + tags: + - oauth + /api/oauth/url/{id}: + get: + description: Get an OAuth URL for the specified provider + parameters: + - description: Provider ID + in: path + name: id + required: true + type: string + - description: Login for + in: query + name: login_for + type: string + - description: OpenID Connect Ticket + in: query + name: oidc_ticket + type: string + - description: OpenID Connect Scope + in: query + name: oidc_scope + type: string + - description: OpenID Connect Name + in: query + name: oidc_name + type: string + - description: Redirect URI + in: query + name: redirect_uri + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.OAuthURLSuccessResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: OAuth URL + tags: + - oauth + /api/oidc/authorize-complete: + post: + consumes: + - application/json + description: Internal endpoint for the completion of the OpenID Connect authorization + flow + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.AuthorizeCompleteResponse' + "500": + description: Internal Server Error + summary: Authorize Complete + tags: + - oidc + /api/user/login: + post: + consumes: + - application/json + description: Login Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.TotpPendingResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Login + tags: + - accounts + /api/user/logout: + post: + description: Logout Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Logout + tags: + - accounts + /api/user/tailscale: + post: + consumes: + - application/json + description: Tailscale Auth Endpoint (Experimental) + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Tailscale + tags: + - accounts + /api/user/totp: + post: + consumes: + - application/json + description: TOTP Endpoint + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.SimpleResponse' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: TOTP + tags: + - accounts + /authorize: + get: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Authorize Endpoint + parameters: + - description: OAuth scopes (space separated, must include openid) + in: query + name: scope + type: string + - description: Response type (e.g. code) + in: query + name: response_type + type: string + - description: Client ID + in: query + name: client_id + type: string + - description: Redirect URI + in: query + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: query + name: state + type: string + - description: Nonce for ID token replay protection + in: query + name: nonce + type: string + - description: PKCE code challenge + in: query + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: query + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: query + name: prompt + type: string + - description: Max authentication age in seconds + in: query + name: max_age + type: string + - description: OAuth scopes (space separated, must include openid) + in: formData + name: scope + type: string + - description: Response type (e.g. code) + in: formData + name: response_type + type: string + - description: Client ID + in: formData + name: client_id + type: string + - description: Redirect URI + in: formData + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: formData + name: state + type: string + - description: Nonce for ID token replay protection + in: formData + name: nonce + type: string + - description: PKCE code challenge + in: formData + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: formData + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: formData + name: prompt + type: string + - description: Max authentication age in seconds + in: formData + name: max_age + type: string + responses: + "302": + description: Found + summary: Authorize + tags: + - oidc + post: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Authorize Endpoint + parameters: + - description: OAuth scopes (space separated, must include openid) + in: query + name: scope + type: string + - description: Response type (e.g. code) + in: query + name: response_type + type: string + - description: Client ID + in: query + name: client_id + type: string + - description: Redirect URI + in: query + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: query + name: state + type: string + - description: Nonce for ID token replay protection + in: query + name: nonce + type: string + - description: PKCE code challenge + in: query + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: query + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: query + name: prompt + type: string + - description: Max authentication age in seconds + in: query + name: max_age + type: string + - description: OAuth scopes (space separated, must include openid) + in: formData + name: scope + type: string + - description: Response type (e.g. code) + in: formData + name: response_type + type: string + - description: Client ID + in: formData + name: client_id + type: string + - description: Redirect URI + in: formData + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: formData + name: state + type: string + - description: Nonce for ID token replay protection + in: formData + name: nonce + type: string + - description: PKCE code challenge + in: formData + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: formData + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: formData + name: prompt + type: string + - description: Max authentication age in seconds + in: formData + name: max_age + type: string + responses: + "302": + description: Found + summary: Authorize + tags: + - oidc + /oidc/token: + post: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Token Endpoint + parameters: + - description: Grant type (authorization_code or refresh_token) + in: query + name: grant_type + required: true + type: string + - description: Authorization code (required for authorization_code grant) + in: query + name: code + type: string + - description: Redirect URI (must match the one from the authorize request) + in: query + name: redirect_uri + type: string + - description: Refresh token (required for refresh_token grant) + in: query + name: refresh_token + type: string + - description: Client ID (required if not using Basic auth) + in: query + name: client_id + type: string + - description: Client secret (required for confidential clients without Basic + auth) + in: query + name: client_secret + type: string + - description: PKCE code verifier (required if code_challenge was sent) + in: query + name: code_verifier + type: string + - description: Grant type (authorization_code or refresh_token) + in: formData + name: grant_type + type: string + - description: Authorization code (required for authorization_code grant) + in: formData + name: code + type: string + - description: Redirect URI (must match the one from the authorize request) + in: formData + name: redirect_uri + type: string + - description: Refresh token (required for refresh_token grant) + in: formData + name: refresh_token + type: string + - description: Client ID (required if not using Basic auth) + in: formData + name: client_id + type: string + - description: Client secret (required for confidential clients without Basic + auth) + in: formData + name: client_secret + type: string + - description: PKCE code verifier (required if code_challenge was sent) + in: formData + name: code_verifier + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.TokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + summary: Token + tags: + - oidc + /oidc/userinfo: + get: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Userinfo Endpoint + parameters: + - description: OpenID Connect Access Token + in: formData + name: access_token + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.UserinfoResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + summary: Userinfo + tags: + - oidc + post: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Userinfo Endpoint + parameters: + - description: OpenID Connect Access Token + in: formData + name: access_token + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.UserinfoResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + summary: Userinfo + tags: + - oidc + /resources/{resource}: + get: + description: Get a resource by file name + parameters: + - description: Resource Name + in: path + name: resource + required: true + type: string + responses: + "200": + description: OK + "403": + description: Forbidden + schema: + $ref: '#/definitions/controller.SimpleResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Resources Endpoint + tags: + - resources +swagger: "2.0"