From 03d06cb0a7c0538df92a9dce271ceee0cefa2c90 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 29 Aug 2025 12:35:11 +0300 Subject: [PATCH] feat: add sqlite database for storing sessions (#326) * feat: add sqlite database for storing sessions * refactor: use db instance instead of service in auth service * fix: coderabbit suggestions --- .env.example | 6 +- Dockerfile | 2 + cmd/root.go | 4 +- docker-compose.dev.yml | 1 + docker-compose.example.yml | 3 +- go.mod | 20 ++- go.sum | 74 ++++++-- internal/assets/assets.go | 7 +- .../migrations/000001_init_sqlite.down.sql | 1 + .../migrations/000001_init_sqlite.up.sql | 10 ++ internal/bootstrap/app_bootstrap.go | 33 ++-- internal/config/config.go | 4 +- internal/controller/oauth_controller.go | 14 +- internal/controller/proxy_controller.go | 8 +- internal/middleware/context_middleware.go | 2 +- internal/middleware/zerolog_middleware.go | 22 ++- internal/model/session_model.go | 12 ++ internal/service/auth_service.go | 167 +++++++----------- internal/service/database_service.go | 78 ++++++++ internal/utils/security_utils.go | 22 --- 20 files changed, 310 insertions(+), 180 deletions(-) create mode 100644 internal/assets/migrations/000001_init_sqlite.down.sql create mode 100644 internal/assets/migrations/000001_init_sqlite.up.sql create mode 100644 internal/model/session_model.go create mode 100644 internal/service/database_service.go diff --git a/.env.example b/.env.example index 0f43bf0..63cecec 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,5 @@ PORT=3000 ADDRESS=0.0.0.0 -SECRET=app_secret -SECRET_FILE=app_secret_file APP_URL=http://localhost:3000 USERS=your_user_password_hash USERS_FILE=users_file @@ -30,4 +28,6 @@ APP_TITLE=Tinyauth SSO FORGOT_PASSWORD_MESSAGE=Some message about resetting the password OAUTH_AUTO_REDIRECT=none BACKGROUND_IMAGE=some_image_url -GENERIC_SKIP_SSL=false \ No newline at end of file +GENERIC_SKIP_SSL=false +RESOURCES_DIR=/data/resources +DATABASE_PATH=/data/tinyauth.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 29a68b9..bd518bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,4 +51,6 @@ COPY --from=builder /tinyauth/tinyauth ./ EXPOSE 3000 +VOLUME ["/data"] + ENTRYPOINT ["./tinyauth"] \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index ef5733e..3ae7292 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,7 +28,6 @@ var rootCmd = &cobra.Command{ } // Check if secrets have a file associated with them - conf.Secret = utils.GetSecret(conf.Secret, conf.SecretFile) conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile) conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile) conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile) @@ -77,8 +76,6 @@ func init() { }{ {"port", 3000, "Port to run the server on."}, {"address", "0.0.0.0", "Address to bind the server to."}, - {"secret", "", "Secret to use for the cookie."}, - {"secret-file", "", "Path to a file containing the secret."}, {"app-url", "", "The Tinyauth URL."}, {"users", "", "Comma separated list of users in the format username:hash."}, {"users-file", "", "Path to a file containing users in the format username:hash."}, @@ -115,6 +112,7 @@ func init() { {"ldap-insecure", false, "Skip certificate verification for the LDAP server."}, {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, + {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, } for _, opt := range configOptions { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d85d5e3..3cf837c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -40,6 +40,7 @@ services: - ./cmd:/tinyauth/cmd - ./main.go:/tinyauth/main.go - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data ports: - 3000:3000 - 4000:4000 diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 4b38707..9cec4a5 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -20,9 +20,10 @@ services: container_name: tinyauth image: ghcr.io/steveiliop56/tinyauth:v3 environment: - - SECRET=some-random-32-chars-string - APP_URL=https://tinyauth.example.com - USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password + volumes: + - ./data:/data labels: traefik.enable: true traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) diff --git a/go.mod b/go.mod index 293dbc5..cea16dd 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/gin-gonic/gin v1.10.1 github.com/go-playground/validator/v10 v10.27.0 + github.com/golang-migrate/migrate/v4 v4.18.3 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 github.com/mdp/qrterminal/v3 v3.2.1 @@ -14,6 +15,9 @@ require ( github.com/spf13/viper v1.20.1 github.com/traefik/paerser v0.2.2 golang.org/x/crypto v0.41.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.30.1 + modernc.org/sqlite v1.38.2 ) require ( @@ -23,22 +27,34 @@ require ( github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/term v0.34.0 // indirect gotest.tools/v3 v3.5.2 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect ) require ( - github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/boombuler/barcode v1.0.2 // indirect diff --git a/go.sum b/go.sum index 68edad6..0b0aceb 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -96,6 +96,10 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= @@ -120,6 +124,8 @@ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -128,6 +134,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= @@ -136,6 +144,11 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -152,6 +165,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -160,13 +177,14 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -180,6 +198,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -205,19 +225,22 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -229,7 +252,6 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -245,11 +267,9 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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= @@ -299,10 +319,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -316,10 +338,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -339,6 +359,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -358,8 +380,38 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/internal/assets/assets.go b/internal/assets/assets.go index df6e61f..412403c 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -4,7 +4,12 @@ import ( "embed" ) -// Frontend assets +// Frontend // //go:embed dist var FrontendAssets embed.FS + +// Migrations +// +//go:embed migrations/*.sql +var Migrations embed.FS diff --git a/internal/assets/migrations/000001_init_sqlite.down.sql b/internal/assets/migrations/000001_init_sqlite.down.sql new file mode 100644 index 0000000..9a8955b --- /dev/null +++ b/internal/assets/migrations/000001_init_sqlite.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "sessions"; \ No newline at end of file diff --git a/internal/assets/migrations/000001_init_sqlite.up.sql b/internal/assets/migrations/000001_init_sqlite.up.sql new file mode 100644 index 0000000..4ffa992 --- /dev/null +++ b/internal/assets/migrations/000001_init_sqlite.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS "sessions" ( + "uuid" TEXT NOT NULL PRIMARY KEY UNIQUE, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "totp_pending" BOOLEAN NOT NULL, + "oauth_groups" TEXT NULL, + "expiry" INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 594c575..7df88ef 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -57,19 +57,6 @@ func (app *BootstrapApp) Setup() error { csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) - // Secrets - encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption") - - if err != nil { - return err - } - - hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac") - - if err != nil { - return err - } - // Create configs authConfig := service.AuthServiceConfig{ Users: users, @@ -80,8 +67,6 @@ func (app *BootstrapApp) Setup() error { LoginTimeout: app.Config.LoginTimeout, LoginMaxRetries: app.Config.LoginMaxRetries, SessionCookieName: sessionCookieName, - HMACSecret: hmacSecret, - EncryptionSecret: encryptionSecret, } // Setup services @@ -107,8 +92,24 @@ func (app *BootstrapApp) Setup() error { } } + // Bootstrap database + databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ + DatabasePath: app.Config.DatabasePath, + }) + + log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service") + + err = databaseService.Init() + + if err != nil { + return fmt.Errorf("failed to initialize database service: %w", err) + } + + database := databaseService.GetDatabase() + + // Create services dockerService := service.NewDockerService() - authService := service.NewAuthService(authConfig, dockerService, ldapService) + authService := service.NewAuthService(authConfig, dockerService, ldapService, database) oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig()) // Initialize services diff --git a/internal/config/config.go b/internal/config/config.go index 5d4dba8..e053f65 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,8 +18,6 @@ var RedirectCookieName = "tinyauth-redirect" type Config struct { Port int `mapstructure:"port" validate:"required"` Address string `validate:"required,ip4_addr" mapstructure:"address"` - Secret string `validate:"required,len=32" mapstructure:"secret"` - SecretFile string `mapstructure:"secret-file"` AppURL string `validate:"required,url" mapstructure:"app-url"` Users string `mapstructure:"users"` UsersFile string `mapstructure:"users-file"` @@ -56,6 +54,7 @@ type Config struct { LdapInsecure bool `mapstructure:"ldap-insecure"` LdapSearchFilter string `mapstructure:"ldap-search-filter"` ResourcesDir string `mapstructure:"resources-dir"` + DatabasePath string `mapstructure:"database-path" validate:"required"` } type OAuthLabels struct { @@ -112,6 +111,7 @@ type UserSearch struct { } type SessionCookie struct { + UUID string Username string Name string Email string diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index aa3289b..31b21f0 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -144,7 +144,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - if !controller.Auth.EmailWhitelisted(user.Email) { + if !controller.Auth.IsEmailWhitelisted(user.Email) { queries, err := query.Values(config.UnauthorizedQuery{ Username: user.Email, }) @@ -169,8 +169,18 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) } + var usename string + + if user.PreferredUsername != "" { + log.Debug().Msg("Using preferred username from OAuth provider") + usename = user.PreferredUsername + } else { + log.Debug().Msg("No preferred username from OAuth provider, using pseudo username") + usename = strings.Replace(user.Email, "@", "_", -1) + } + controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ - Username: user.Email, + Username: usename, Name: name, Email: user.Email, Provider: req.Provider, diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 348be65..6e207e8 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -89,7 +89,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { clientIP := c.ClientIP() - if controller.Auth.BypassedIP(labels, clientIP) { + if controller.Auth.IsBypassedIP(labels, clientIP) { c.Header("Authorization", c.Request.Header.Get("Authorization")) headers := utils.ParseHeaders(labels.Headers) @@ -135,7 +135,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.Auth.AuthEnabled(uri, labels) + authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -195,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.Auth.ResourceAllowed(c, userContext, labels) + appAllowed := controller.Auth.IsResourceAllowed(c, userContext, labels) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -229,7 +229,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.Auth.OAuthGroup(c, userContext, labels) + groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 58e53e1..ee8932a 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -83,7 +83,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { goto basic } - if !m.Auth.EmailWhitelisted(cookie.Email) { + if !m.Auth.IsEmailWhitelisted(cookie.Email) { log.Debug().Msg("Email from session cookie not whitelisted") m.Auth.DeleteSessionCookie(c) goto basic diff --git a/internal/middleware/zerolog_middleware.go b/internal/middleware/zerolog_middleware.go index 877ad4c..f3ca485 100644 --- a/internal/middleware/zerolog_middleware.go +++ b/internal/middleware/zerolog_middleware.go @@ -49,18 +49,24 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc { latency := time.Since(tStart).String() - // logPath check if the path should be logged normally or with debug + subLogger := log.With().Str("method", method). + Str("path", path). + Str("address", address). + Str("client_ip", clientIP). + Int("status", code). + Str("latency", latency).Logger() + if m.logPath(method + " " + path) { switch { - case code >= 200 && code < 300: - log.Info().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") - case code >= 300 && code < 400: - log.Warn().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") - case code >= 400: - log.Error().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request") + case code >= 400 && code < 500: + subLogger.Warn().Msg("Client Error") + case code >= 500: + subLogger.Error().Msg("Server Error") + default: + subLogger.Info().Msg("Request") } } else { - log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") + subLogger.Debug().Msg("Request") } } } diff --git a/internal/model/session_model.go b/internal/model/session_model.go new file mode 100644 index 0000000..45e6065 --- /dev/null +++ b/internal/model/session_model.go @@ -0,0 +1,12 @@ +package model + +type Session struct { + UUID string `gorm:"column:uuid;primaryKey"` + Username string `gorm:"column:username"` + Email string `gorm:"column:email"` + Name string `gorm:"column:name"` + Provider string `gorm:"column:provider"` + TOTPPending bool `gorm:"column:totp_pending"` + OAuthGroups string `gorm:"column:oauth_groups"` + Expiry int64 `gorm:"column:expiry"` +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 10d49e7..f55961c 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -7,12 +7,14 @@ import ( "sync" "time" "tinyauth/internal/config" + "tinyauth/internal/model" "tinyauth/internal/utils" "github.com/gin-gonic/gin" - "github.com/gorilla/sessions" + "github.com/google/uuid" "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" ) type LoginAttempt struct { @@ -30,8 +32,6 @@ type AuthServiceConfig struct { LoginTimeout int LoginMaxRetries int SessionCookieName string - HMACSecret string - EncryptionSecret string } type AuthService struct { @@ -39,49 +39,24 @@ type AuthService struct { Docker *DockerService LoginAttempts map[string]*LoginAttempt LoginMutex sync.RWMutex - Store *sessions.CookieStore LDAP *LdapService + Database *gorm.DB } -func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService) *AuthService { +func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService { return &AuthService{ Config: config, Docker: docker, LoginAttempts: make(map[string]*LoginAttempt), LDAP: ldap, + Database: database, } } func (auth *AuthService) Init() error { - store := sessions.NewCookieStore([]byte(auth.Config.HMACSecret), []byte(auth.Config.EncryptionSecret)) - store.Options = &sessions.Options{ - Path: "/", - MaxAge: auth.Config.SessionExpiry, - Secure: auth.Config.SecureCookie, - HttpOnly: true, - Domain: fmt.Sprintf(".%s", auth.Config.Domain), - } - - auth.Store = store return nil } -func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) { - session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName) - - // If there was an error getting the session, it might be invalid so let's clear it and retry - if err != nil { - log.Debug().Err(err).Msg("Error getting session, creating a new one") - c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) - session, err = auth.Store.New(c.Request, auth.Config.SessionCookieName) - if err != nil { - return nil, err - } - } - - return session, nil -} - func (auth *AuthService) SearchUser(username string) config.UserSearch { if auth.GetLocalUser(username).Username != "" { return config.UserSearch{ @@ -158,30 +133,24 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { auth.LoginMutex.RLock() defer auth.LoginMutex.RUnlock() - // Return false if rate limiting is not configured if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { return false, 0 } - // Check if the identifier exists in the map attempt, exists := auth.LoginAttempts[identifier] if !exists { return false, 0 } - // If account is locked, check if lock time has expired if attempt.LockedUntil.After(time.Now()) { - // Calculate remaining lockout time in seconds remaining := int(time.Until(attempt.LockedUntil).Seconds()) return true, remaining } - // Lock has expired return false, 0 } func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { - // Skip if rate limiting is not configured if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { return } @@ -189,133 +158,132 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { auth.LoginMutex.Lock() defer auth.LoginMutex.Unlock() - // Get current attempt record or create a new one attempt, exists := auth.LoginAttempts[identifier] if !exists { attempt = &LoginAttempt{} auth.LoginAttempts[identifier] = attempt } - // Update last attempt time attempt.LastAttempt = time.Now() - // If successful login, reset failed attempts if success { attempt.FailedAttempts = 0 attempt.LockedUntil = time.Time{} // Reset lock time return } - // Increment failed attempts attempt.FailedAttempts++ - // If max retries reached, lock the account if attempt.FailedAttempts >= auth.Config.LoginMaxRetries { attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second) log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts") } } -func (auth *AuthService) EmailWhitelisted(email string) bool { +func (auth *AuthService) IsEmailWhitelisted(email string) bool { return utils.CheckFilter(auth.Config.OauthWhitelist, email) } func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { - session, err := auth.GetSession(c) + uuid, err := uuid.NewRandom() + if err != nil { return err } - var sessionExpiry int + var expiry int if data.TotpPending { - sessionExpiry = 3600 + expiry = 3600 } else { - sessionExpiry = auth.Config.SessionExpiry + expiry = auth.Config.SessionExpiry } - session.Values["username"] = data.Username - session.Values["name"] = data.Name - session.Values["email"] = data.Email - session.Values["provider"] = data.Provider - session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() - session.Values["totpPending"] = data.TotpPending - session.Values["oauthGroups"] = data.OAuthGroups + session := model.Session{ + UUID: uuid.String(), + Username: data.Username, + Email: data.Email, + Name: data.Name, + Provider: data.Provider, + TOTPPending: data.TotpPending, + OAuthGroups: data.OAuthGroups, + Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(), + } + + err = auth.Database.Create(&session).Error - err = session.Save(c.Request, c.Writer) if err != nil { return err } + c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) + return nil } func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { - session, err := auth.GetSession(c) + cookie, err := c.Cookie(auth.Config.SessionCookieName) + if err != nil { return err } - // Delete all values in the session - for key := range session.Values { - delete(session.Values, key) + res := auth.Database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{}) + + if res.Error != nil { + return res.Error } - err = session.Save(c.Request, c.Writer) - if err != nil { - return err - } - - // Clear the cookie in the browser c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) return nil } func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) { - session, err := auth.GetSession(c) + cookie, err := c.Cookie(auth.Config.SessionCookieName) + if err != nil { return config.SessionCookie{}, err } - username, usernameOk := session.Values["username"].(string) - email, emailOk := session.Values["email"].(string) - name, nameOk := session.Values["name"].(string) - provider, providerOK := session.Values["provider"].(string) - expiry, expiryOk := session.Values["expiry"].(int64) - totpPending, totpPendingOk := session.Values["totpPending"].(bool) - oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string) + var session model.Session - // If any data is missing, delete the session cookie - if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { - log.Warn().Msg("Session cookie is invalid") - auth.DeleteSessionCookie(c) - return config.SessionCookie{}, nil + res := auth.Database.Unscoped().Where("uuid = ?", cookie).First(&session) + + if res.Error != nil { + return config.SessionCookie{}, res.Error } - // If the session cookie has expired, delete it - if time.Now().Unix() > expiry { - log.Warn().Msg("Session cookie expired") - auth.DeleteSessionCookie(c) - return config.SessionCookie{}, nil + if res.RowsAffected == 0 { + return config.SessionCookie{}, fmt.Errorf("session not found") + } + + currentTime := time.Now().Unix() + + if currentTime > session.Expiry { + res := auth.Database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{}) + if res.Error != nil { + log.Error().Err(res.Error).Msg("Failed to delete expired session") + } + return config.SessionCookie{}, fmt.Errorf("session expired") } return config.SessionCookie{ - Username: username, - Name: name, - Email: email, - Provider: provider, - TotpPending: totpPending, - OAuthGroups: oauthGroups, + UUID: session.UUID, + Username: session.Username, + Email: session.Email, + Name: session.Name, + Provider: session.Provider, + TotpPending: session.TOTPPending, + OAuthGroups: session.OAuthGroups, }, nil } func (auth *AuthService) UserAuthConfigured() bool { - // If there are users or LDAP is configured, return true return len(auth.Config.Users) > 0 || auth.LDAP != nil } -func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -325,7 +293,7 @@ func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserCont return utils.CheckFilter(labels.Users, context.Username) } -func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool { +func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool { if labels.OAuth.Groups == "" { return true } @@ -335,23 +303,20 @@ func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, return true } - // Split the groups by comma (no need to parse since they are from the API response) + // No need to parse since they are from the API response oauthGroups := strings.Split(context.OAuthGroups, ",") - // For every group check if it is in the required groups for _, group := range oauthGroups { if utils.CheckFilter(labels.OAuth.Groups, group) { return true } } - // No groups matched log.Debug().Msg("No groups matched") return false } -func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, error) { - // If the label is empty, auth is enabled +func (auth *AuthService) IsAuthEnabled(uri string, labels config.Labels) (bool, error) { if labels.Allowed == "" { return true, nil } @@ -362,12 +327,10 @@ func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, er return true, err } - // If the regex matches the URI, auth is not enabled if regex.MatchString(uri) { return false, nil } - // Auth enabled return true, nil } @@ -384,7 +347,6 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { - // Check if the IP is in block list for _, blocked := range labels.IP.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { @@ -397,7 +359,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { } } - // For every IP in the allow list, check if the IP matches for _, allowed := range labels.IP.Allow { res, err := utils.FilterIP(allowed, ip) if err != nil { @@ -410,7 +371,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { } } - // If not in allowed range and allowed range is not empty, deny access if len(labels.IP.Allow) > 0 { log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") return false @@ -420,8 +380,7 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool { return true } -func (auth *AuthService) BypassedIP(labels config.Labels, ip string) bool { - // For every IP in the bypass list, check if the IP matches +func (auth *AuthService) IsBypassedIP(labels config.Labels, ip string) bool { for _, bypassed := range labels.IP.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { diff --git a/internal/service/database_service.go b/internal/service/database_service.go new file mode 100644 index 0000000..858ba4c --- /dev/null +++ b/internal/service/database_service.go @@ -0,0 +1,78 @@ +package service + +import ( + "database/sql" + "tinyauth/internal/assets" + + "github.com/glebarez/sqlite" + "github.com/golang-migrate/migrate/v4" + sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" + "gorm.io/gorm" +) + +type DatabaseServiceConfig struct { + DatabasePath string +} + +type DatabaseService struct { + Config DatabaseServiceConfig + Database *gorm.DB +} + +func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService { + return &DatabaseService{ + Config: config, + } +} + +func (ds *DatabaseService) Init() error { + gormDB, err := gorm.Open(sqlite.Open(ds.Config.DatabasePath), &gorm.Config{}) + + if err != nil { + return err + } + + sqlDB, err := gormDB.DB() + + if err != nil { + return err + } + + sqlDB.SetMaxOpenConns(1) + + err = ds.migrateDatabase(sqlDB) + + if err != nil && err != migrate.ErrNoChange { + return err + } + + ds.Database = gormDB + return nil +} + +func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error { + data, err := iofs.New(assets.Migrations, "migrations") + + if err != nil { + return err + } + + target, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{}) + + if err != nil { + return err + } + + migrator, err := migrate.NewWithInstance("iofs", data, "tinyauth", target) + + if err != nil { + return err + } + + return migrator.Up() +} + +func (ds *DatabaseService) GetDatabase() *gorm.DB { + return ds.Database +} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index a031900..b40c56c 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -1,17 +1,13 @@ package utils import ( - "bytes" - "crypto/sha256" "encoding/base64" "errors" - "io" "net" "regexp" "strings" "github.com/google/uuid" - "golang.org/x/crypto/hkdf" ) func GetSecret(conf string, file string) string { @@ -49,24 +45,6 @@ func GetBasicAuth(username string, password string) string { return base64.StdEncoding.EncodeToString([]byte(auth)) } -func DeriveKey(secret string, info string) (string, error) { - hash := sha256.New - hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice - key := make([]byte, 24) - - _, err := io.ReadFull(hkdf, key) - if err != nil { - return "", err - } - - if bytes.Equal(key, make([]byte, 24)) { - return "", errors.New("derived key is empty") - } - - encodedKey := base64.StdEncoding.EncodeToString(key) - return encodedKey, nil -} - func FilterIP(filter string, ip string) (bool, error) { ipAddr := net.ParseIP(ip)