From 6391a99bb93e3c992074e2be0569b8d5033672b1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 24 Mar 2025 23:02:09 +0100 Subject: [PATCH] Switch to jwks instead of custom /info --- api/src/auth.ts | 20 +++++++------------- api/src/index.ts | 6 ------ auth/.env.example | 3 +++ auth/config.go | 4 ++-- auth/go.mod | 13 ++++++++++++- auth/go.sum | 19 +++++++++++++++++++ auth/jwt.go | 42 ++++++++++++++++++------------------------ auth/main.go | 2 +- 8 files changed, 62 insertions(+), 47 deletions(-) diff --git a/api/src/auth.ts b/api/src/auth.ts index ea4c8ef9..1f41b888 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -1,20 +1,14 @@ import jwt from "@elysiajs/jwt"; import Elysia, { t } from "elysia"; +import { createRemoteJWKSet } from "jose"; -export let jwtSecret = process.env.JWT_SECRET!; -if (!jwtSecret) { - const auth = process.env.AUTH_SERVER ?? "http://auth:4568/auth"; - try { - const ret = await fetch(`${auth}/info`); - const info = await ret.json(); - jwtSecret = info.publicKey; - } catch (error) { - console.error(`Can't access auth server at ${auth}:\n${error}`); - } -} +const jwtSecret = process.env.JWT_SECRET; +const jwks = createRemoteJWKSet( + new URL(process.env.AUTH_SERVER ?? "http://auth:4568"), +); export const auth = new Elysia({ name: "auth" }) - .use(jwt({ secret: jwtSecret })) + .use(jwt({ secret: jwtSecret ?? jwks })) .guard({ headers: t.Object({ authorization: t.String({ pattern: "^Bearer .+$" }), @@ -25,7 +19,7 @@ export const auth = new Elysia({ name: "auth" }) return { beforeHandle: () => {}, resolve: async ({ headers: { authorization }, jwt }) => { - console.log(authorization.slice(7)); + console.log(authorization?.slice(7)); const user = await jwt.verify(authorization?.slice(7)); console.log("macro", user); return { user }; diff --git a/api/src/index.ts b/api/src/index.ts index 08142f27..623f271c 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,5 +1,4 @@ import { swagger } from "@elysiajs/swagger"; -import { jwtSecret } from "./auth"; import { app } from "./base"; import { processImages } from "./controllers/seed/images"; import { migrate } from "./db"; @@ -7,11 +6,6 @@ import { comment } from "./utils"; await migrate(); -if (!jwtSecret) { - console.error("Missing jwt secret or auth server. exiting"); - process.exit(1); -} - // run image processor task in background processImages(); diff --git a/auth/.env.example b/auth/.env.example index bf462faa..de7c4b08 100644 --- a/auth/.env.example +++ b/auth/.env.example @@ -4,6 +4,9 @@ # http route prefix (will listen to $KEIBI_PREFIX/users for example) KEIBI_PREFIX="" +# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance. +PUBLIC_URL=http://localhost:8901 + # Database things POSTGRES_USER=kyoo POSTGRES_PASSWORD=password diff --git a/auth/config.go b/auth/config.go index f49352db..737226da 100644 --- a/auth/config.go +++ b/auth/config.go @@ -17,13 +17,12 @@ type Configuration struct { Prefix string JwtPrivateKey *rsa.PrivateKey JwtPublicKey *rsa.PublicKey - Issuer string + PublicUrl string DefaultClaims jwt.MapClaims ExpirationDelay time.Duration } var DefaultConfig = Configuration{ - Issuer: "kyoo", DefaultClaims: make(jwt.MapClaims), ExpirationDelay: 30 * 24 * time.Hour, } @@ -54,6 +53,7 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) { } } + ret.PublicUrl = os.Getenv("PUBLIC_URL") ret.Prefix = os.Getenv("KEIBI_PREFIX") if ret.JwtPrivateKey == nil { diff --git a/auth/go.mod b/auth/go.mod index e3290193..a34b9b9d 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -16,7 +16,18 @@ require ( github.com/swaggo/swag v1.16.4 ) -require github.com/otaxhu/type-mismatch-encoding v0.0.0-20241118152201-1861af90dd01 // indirect +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx v1.2.30 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/otaxhu/type-mismatch-encoding v0.0.0-20241118152201-1861af90dd01 // indirect + github.com/pkg/errors v0.9.1 // indirect +) require ( github.com/KyleBanks/depth v1.2.1 // indirect diff --git a/auth/go.sum b/auth/go.sum index cdf8d8a7..e3bd3475 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -9,6 +9,8 @@ github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6Ct github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= @@ -54,6 +56,8 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -99,6 +103,19 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 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/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA= +github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -134,7 +151,9 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= diff --git a/auth/jwt.go b/auth/jwt.go index 07a4b495..7f931554 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -2,8 +2,6 @@ package main import ( "context" - "crypto/x509" - "encoding/pem" "fmt" "maps" "net/http" @@ -12,6 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" + "github.com/lestrrat-go/jwx/jwk" ) type Jwt struct { @@ -19,11 +18,6 @@ type Jwt struct { Token string `json:"token"` } -type Info struct { - // The public key used to sign jwt tokens. It can be used by your services to check if the jwt is valid. - PublicKey string `json:"publicKey"` -} - // @Summary Get JWT // @Description Convert a session token to a short lived JWT. // @Tags jwt @@ -56,13 +50,13 @@ func (h *Handler) CreateJwt(c echo.Context) error { claims := maps.Clone(session.User.Claims) claims["sub"] = session.User.Id.String() claims["sid"] = session.Id.String() - claims["iss"] = h.config.Issuer + claims["iss"] = h.config.PublicUrl + claims["iat"] = &jwt.NumericDate{ + Time: time.Now().UTC(), + } claims["exp"] = &jwt.NumericDate{ Time: time.Now().UTC().Add(time.Hour), } - claims["iss"] = &jwt.NumericDate{ - Time: time.Now().UTC(), - } jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) t, err := jwt.SignedString(h.config.JwtPrivateKey) if err != nil { @@ -74,21 +68,21 @@ func (h *Handler) CreateJwt(c echo.Context) error { }) } -// @Summary Info -// @Description Get info like the public key used to sign the jwts. +// @Summary Jwks +// @Description Get the jwks info, used to validate jwts. // @Tags jwt // @Produce json -// @Success 200 {object} Info -// @Router /info [get] +// @Success 200 {object} jwk.Key +// @Router /.well-known/jwks.json [get] func (h *Handler) GetInfo(c echo.Context) error { - key := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: x509.MarshalPKCS1PublicKey(h.config.JwtPublicKey), - }, - ) + key, err := jwk.New(h.config.JwtPublicKey) + if err != nil { + return err + } - return c.JSON(200, Info{ - PublicKey: string(key), - }) + key.Set("use", "sig") + key.Set("key_ops", "verify") + set := jwk.NewSet() + set.Add(key) + return c.JSON(200, set) } diff --git a/auth/main.go b/auth/main.go index 2744a944..af94837e 100644 --- a/auth/main.go +++ b/auth/main.go @@ -184,7 +184,7 @@ func main() { r.DELETE("/sessions/:id", h.Logout) g.GET("/jwt", h.CreateJwt) - g.GET("/info", h.GetInfo) + e.GET("/.well-known/jwks.json", h.GetInfo) g.GET("/swagger/*", echoSwagger.WrapHandler)