From e2c1803683f4e30b1b0bc21c3d8e9a01e8154ac8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 19 Jan 2026 10:14:02 +0000 Subject: [PATCH 1/2] fix(deps): update module github.com/auth0/go-jwt-middleware/v2 to v3 --- go.mod | 13 ++++++++++++- go.sum | 29 +++++++++++++++++++++++++---- middleware/auth0.go | 2 +- middleware/auth_test.go | 2 +- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index f3965c9..e2fbdb9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Khan/genqlient v0.8.1 github.com/alecthomas/kong v1.13.0 github.com/apex/log v1.9.0 - github.com/auth0/go-jwt-middleware/v2 v2.3.1 + github.com/auth0/go-jwt-middleware/v3 v3.0.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 @@ -42,17 +42,27 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -61,6 +71,7 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/urfave/cli/v3 v3.6.1 // indirect + github.com/valyala/fastjson v1.6.7 // indirect github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect diff --git a/go.sum b/go.sum index 1e9e17e..89d7256 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw= -github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc= +github.com/auth0/go-jwt-middleware/v3 v3.0.0 h1:+rvUPCT+VbAuK4tpS13fWfZrMyqTwLopt3VoY0Y7kvA= +github.com/auth0/go-jwt-middleware/v3 v3.0.0/go.mod h1:iU42jqjRyeKbf9YYSnRnolr836gk6Ty/jnUNuVq2b0o= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -40,6 +40,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL 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.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -57,6 +59,8 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -94,6 +98,20 @@ 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/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +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/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs= +github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= +github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -131,6 +149,8 @@ github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rY github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E= github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -148,6 +168,7 @@ 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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -169,6 +190,8 @@ github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPf github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= +github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= @@ -259,8 +282,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= -gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/middleware/auth0.go b/middleware/auth0.go index 0ff03e8..b35a58c 100644 --- a/middleware/auth0.go +++ b/middleware/auth0.go @@ -10,7 +10,7 @@ import ( "sync" "time" - mw "github.com/auth0/go-jwt-middleware/v2" + mw "github.com/auth0/go-jwt-middleware/v3" "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" ) diff --git a/middleware/auth_test.go b/middleware/auth_test.go index 124459f..01b0f99 100644 --- a/middleware/auth_test.go +++ b/middleware/auth_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - mw "github.com/auth0/go-jwt-middleware/v2" + mw "github.com/auth0/go-jwt-middleware/v3" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/stretchr/testify/assert" From 817927cb7da9bba911e4e0f6f89a1407e2763599 Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Mon, 19 Jan 2026 20:31:45 +0100 Subject: [PATCH 2/2] fix: migrate to go-jwt-middleware v3 API - Use validator and jwks packages for JWT validation - Replace manual JWKS caching with jwks.NewCachingProvider - Add CustomClaims struct for https://unbound.se/roles claim - Rename TokenFromContext to ClaimsFromContext - Update middleware/auth.go to use new claims structure - Update tests to use core.SetClaims and validator.ValidatedClaims Co-Authored-By: Claude Opus 4.5 --- go.mod | 3 +- go.sum | 2 - middleware/auth.go | 35 ++------ middleware/auth0.go | 191 +++++++++++----------------------------- middleware/auth_test.go | 144 +++++++++++++++--------------- 5 files changed, 133 insertions(+), 242 deletions(-) diff --git a/go.mod b/go.mod index e2fbdb9..c49ea62 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,8 @@ require ( github.com/alecthomas/kong v1.13.0 github.com/apex/log v1.9.0 github.com/auth0/go-jwt-middleware/v3 v3.0.0 - github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 - github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.26.0 github.com/rs/cors v1.11.1 github.com/sparetimecoders/goamqp v0.3.3 @@ -60,6 +58,7 @@ require ( github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rabbitmq/amqp091-go v1.10.0 // indirect github.com/segmentio/asm v1.2.1 // indirect diff --git a/go.sum b/go.sum index 89d7256..0182357 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,6 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= diff --git a/middleware/auth.go b/middleware/auth.go index b31dba2..b432600 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -6,7 +6,6 @@ import ( "net/http" "github.com/99designs/gqlgen/graphql" - "github.com/golang-jwt/jwt/v5" "gitea.unbound.se/unboundsoftware/schemas/domain" ) @@ -33,14 +32,9 @@ type AuthMiddleware struct { func (m *AuthMiddleware) Handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - token, err := TokenFromContext(r.Context()) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("Invalid JWT token format")) - return - } - if token != nil { - ctx = context.WithValue(ctx, UserKey, token.Claims.(jwt.MapClaims)["sub"]) + claims := ClaimsFromContext(r.Context()) + if claims != nil { + ctx = context.WithValue(ctx, UserKey, claims.RegisteredClaims.Subject) } apiKey, err := ApiKeyFromContext(r.Context()) if err != nil { @@ -68,29 +62,18 @@ func UserFromContext(ctx context.Context) string { } func UserHasRole(ctx context.Context, role string) bool { - token, err := TokenFromContext(ctx) - if err != nil || token == nil { + claims := ClaimsFromContext(ctx) + if claims == nil { return false } - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { + customClaims, ok := claims.CustomClaims.(*CustomClaims) + if !ok || customClaims == nil { return false } - // Check the custom roles claim - rolesInterface, ok := claims["https://unbound.se/roles"] - if !ok { - return false - } - - roles, ok := rolesInterface.([]interface{}) - if !ok { - return false - } - - for _, r := range roles { - if roleStr, ok := r.(string); ok && roleStr == role { + for _, r := range customClaims.Roles { + if r == role { return true } } diff --git a/middleware/auth0.go b/middleware/auth0.go index b35a58c..77ad02e 100644 --- a/middleware/auth0.go +++ b/middleware/auth0.go @@ -2,39 +2,34 @@ package middleware import ( "context" - "crypto/tls" - "encoding/json" "fmt" - "net/http" - "strings" - "sync" - "time" + "log" + "net/url" - mw "github.com/auth0/go-jwt-middleware/v3" - "github.com/golang-jwt/jwt/v5" - "github.com/pkg/errors" + jwtmiddleware "github.com/auth0/go-jwt-middleware/v3" + "github.com/auth0/go-jwt-middleware/v3/jwks" + "github.com/auth0/go-jwt-middleware/v3/validator" ) +// CustomClaims contains custom claims from the JWT token. +type CustomClaims struct { + Roles []string `json:"https://unbound.se/roles"` +} + +// Validate implements the validator.CustomClaims interface. +func (c CustomClaims) Validate(_ context.Context) error { + return nil +} + type Auth0 struct { domain string audience string - client *http.Client - cache JwksCache } -func NewAuth0(audience, domain string, strictSsl bool) *Auth0 { - customTransport := http.DefaultTransport.(*http.Transport).Clone() - customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: !strictSsl} - client := &http.Client{Transport: customTransport} - +func NewAuth0(audience, domain string, _ bool) *Auth0 { return &Auth0{ domain: domain, audience: audience, - client: client, - cache: JwksCache{ - RWMutex: &sync.RWMutex{}, - cache: make(map[string]cacheItem), - }, } } @@ -42,133 +37,47 @@ type Response struct { Message string `json:"message"` } -type Jwks struct { - Keys []JSONWebKeys `json:"keys"` -} - -type JSONWebKeys struct { - Kty string `json:"kty"` - Kid string `json:"kid"` - Use string `json:"use"` - N string `json:"n"` - E string `json:"e"` - X5c []string `json:"x5c"` -} - -func (a *Auth0) ValidationKeyGetter() func(token *jwt.Token) (interface{}, error) { - return func(token *jwt.Token) (interface{}, error) { - // Verify 'aud' claim - - cert, err := a.getPemCert(token) - if err != nil { - panic(err.Error()) - } - - result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) - return result, nil - } -} - -func (a *Auth0) Middleware() *mw.JWTMiddleware { +func (a *Auth0) Middleware() *jwtmiddleware.JWTMiddleware { issuer := fmt.Sprintf("https://%s/", a.domain) - jwtMiddleware := mw.New(func(ctx context.Context, token string) (interface{}, error) { - jwtToken, err := jwt.Parse(token, a.ValidationKeyGetter(), jwt.WithAudience(a.audience), jwt.WithIssuer(issuer)) - if err != nil { - return nil, err - } - if _, ok := jwtToken.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) - } - return jwtToken, nil - }, - mw.WithTokenExtractor(func(r *http.Request) (string, error) { - token := r.Header.Get("Authorization") - if strings.HasPrefix(token, "Bearer ") { - return token[7:], nil - } - return "", nil + + issuerURL, err := url.Parse(issuer) + if err != nil { + log.Fatalf("failed to parse issuer URL: %v", err) + } + + provider, err := jwks.NewCachingProvider(jwks.WithIssuerURL(issuerURL)) + if err != nil { + log.Fatalf("failed to create JWKS provider: %v", err) + } + + jwtValidator, err := validator.New( + validator.WithKeyFunc(provider.KeyFunc), + validator.WithAlgorithm(validator.RS256), + validator.WithIssuer(issuer), + validator.WithAudience(a.audience), + validator.WithCustomClaims(func() validator.CustomClaims { + return &CustomClaims{} }), - mw.WithCredentialsOptional(true), ) + if err != nil { + log.Fatalf("failed to create JWT validator: %v", err) + } + + jwtMiddleware, err := jwtmiddleware.New( + jwtmiddleware.WithValidator(jwtValidator), + jwtmiddleware.WithCredentialsOptional(true), + ) + if err != nil { + log.Fatalf("failed to create JWT middleware: %v", err) + } return jwtMiddleware } -func TokenFromContext(ctx context.Context) (*jwt.Token, error) { - if value := ctx.Value(mw.ContextKey{}); value != nil { - if u, ok := value.(*jwt.Token); ok { - return u, nil - } - return nil, fmt.Errorf("token is in wrong format") - } - return nil, nil -} - -func (a *Auth0) cacheGetWellknown(url string) (*Jwks, error) { - if value := a.cache.get(url); value != nil { - return value, nil - } - jwks := &Jwks{} - resp, err := a.client.Get(url) +func ClaimsFromContext(ctx context.Context) *validator.ValidatedClaims { + claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](ctx) if err != nil { - return jwks, err - } - defer func() { - _ = resp.Body.Close() - }() - err = json.NewDecoder(resp.Body).Decode(jwks) - if err == nil && jwks != nil { - a.cache.put(url, jwks) - } - return jwks, err -} - -func (a *Auth0) getPemCert(token *jwt.Token) (string, error) { - jwks, err := a.cacheGetWellknown(fmt.Sprintf("https://%s/.well-known/jwks.json", a.domain)) - if err != nil { - return "", err - } - var cert string - for k := range jwks.Keys { - if token.Header["kid"] == jwks.Keys[k].Kid { - cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" - } - } - - if cert == "" { - err := errors.New("Unable to find appropriate key.") - return cert, err - } - - return cert, nil -} - -type JwksCache struct { - *sync.RWMutex - cache map[string]cacheItem -} -type cacheItem struct { - data *Jwks - expiration time.Time -} - -func (c *JwksCache) get(url string) *Jwks { - c.RLock() - defer c.RUnlock() - if value, ok := c.cache[url]; ok { - if time.Now().After(value.expiration) { - return nil - } - return value.data - } - return nil -} - -func (c *JwksCache) put(url string, jwks *Jwks) { - c.Lock() - defer c.Unlock() - c.cache[url] = cacheItem{ - data: jwks, - expiration: time.Now().Add(time.Minute * 60), + return nil } + return claims } diff --git a/middleware/auth_test.go b/middleware/auth_test.go index 01b0f99..3f27520 100644 --- a/middleware/auth_test.go +++ b/middleware/auth_test.go @@ -6,8 +6,8 @@ import ( "net/http/httptest" "testing" - mw "github.com/auth0/go-jwt-middleware/v3" - "github.com/golang-jwt/jwt/v5" + "github.com/auth0/go-jwt-middleware/v3/core" + "github.com/auth0/go-jwt-middleware/v3/validator" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -155,9 +155,11 @@ func TestAuthMiddleware_Handler_WithValidJWT(t *testing.T) { mockCache.On("OrganizationByAPIKey", "").Return(nil) userID := "user-123" - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": userID, - }) + claims := &validator.ValidatedClaims{ + RegisteredClaims: validator.RegisteredClaims{ + Subject: userID, + }, + } // Create a test handler that checks the context var capturedUser string @@ -170,9 +172,9 @@ func TestAuthMiddleware_Handler_WithValidJWT(t *testing.T) { w.WriteHeader(http.StatusOK) }) - // Create request with JWT token in context + // Create request with JWT claims in context req := httptest.NewRequest(http.MethodGet, "/test", nil) - ctx := context.WithValue(req.Context(), mw.ContextKey{}, token) + ctx := core.SetClaims(req.Context(), claims) req = req.WithContext(ctx) rec := httptest.NewRecorder() @@ -209,28 +211,35 @@ func TestAuthMiddleware_Handler_APIKeyErrorHandling(t *testing.T) { assert.Contains(t, rec.Body.String(), "Invalid API Key format") } -func TestAuthMiddleware_Handler_JWTErrorHandling(t *testing.T) { +func TestAuthMiddleware_Handler_JWTMissingClaims(t *testing.T) { // Setup mockCache := new(MockCache) authMiddleware := NewAuth(mockCache) + // The middleware passes the plaintext API key (cache handles hashing) + mockCache.On("OrganizationByAPIKey", "").Return(nil) + + // Create a test handler that checks the context + var capturedUser string testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if user := r.Context().Value(UserKey); user != nil { + if u, ok := user.(string); ok { + capturedUser = u + } + } w.WriteHeader(http.StatusOK) }) - // Create request with invalid JWT token type in context + // Create request without JWT claims - user should not be set req := httptest.NewRequest(http.MethodGet, "/test", nil) - ctx := context.WithValue(req.Context(), mw.ContextKey{}, "not-a-token") // Invalid type - req = req.WithContext(ctx) - rec := httptest.NewRecorder() // Execute authMiddleware.Handler(testHandler).ServeHTTP(rec, req) // Assert - assert.Equal(t, http.StatusInternalServerError, rec.Code) - assert.Contains(t, rec.Body.String(), "Invalid JWT token format") + assert.Equal(t, http.StatusOK, rec.Code) + assert.Empty(t, capturedUser, "User should not be set when no claims in context") } func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) { @@ -249,9 +258,11 @@ func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) { userID := "user-123" apiKey := "test-api-key-123" - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": userID, - }) + claims := &validator.ValidatedClaims{ + RegisteredClaims: validator.RegisteredClaims{ + Subject: userID, + }, + } // Mock expects plaintext key (cache handles hashing internally) mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg) @@ -273,9 +284,9 @@ func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) { w.WriteHeader(http.StatusOK) }) - // Create request with both JWT and API key in context + // Create request with both JWT claims and API key in context req := httptest.NewRequest(http.MethodGet, "/test", nil) - ctx := context.WithValue(req.Context(), mw.ContextKey{}, token) + ctx := core.SetClaims(req.Context(), claims) ctx = context.WithValue(ctx, ApiKey, apiKey) req = req.WithContext(ctx) @@ -475,13 +486,17 @@ func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) { } func TestUserHasRole_WithValidRole(t *testing.T) { - // Create token with roles claim - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": "user-123", - "https://unbound.se/roles": []interface{}{"admin", "user"}, - }) + // Create claims with roles + claims := &validator.ValidatedClaims{ + RegisteredClaims: validator.RegisteredClaims{ + Subject: "user-123", + }, + CustomClaims: &CustomClaims{ + Roles: []string{"admin", "user"}, + }, + } - ctx := context.WithValue(context.Background(), mw.ContextKey{}, token) + ctx := core.SetClaims(context.Background(), claims) // Test for existing role hasRole := UserHasRole(ctx, "admin") @@ -492,13 +507,17 @@ func TestUserHasRole_WithValidRole(t *testing.T) { } func TestUserHasRole_WithoutRole(t *testing.T) { - // Create token with roles claim - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": "user-123", - "https://unbound.se/roles": []interface{}{"user"}, - }) + // Create claims with roles + claims := &validator.ValidatedClaims{ + RegisteredClaims: validator.RegisteredClaims{ + Subject: "user-123", + }, + CustomClaims: &CustomClaims{ + Roles: []string{"user"}, + }, + } - ctx := context.WithValue(context.Background(), mw.ContextKey{}, token) + ctx := core.SetClaims(context.Background(), claims) // Test for non-existing role hasRole := UserHasRole(ctx, "admin") @@ -506,59 +525,42 @@ func TestUserHasRole_WithoutRole(t *testing.T) { } func TestUserHasRole_WithoutRolesClaim(t *testing.T) { - // Create token without roles claim - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": "user-123", - }) + // Create claims without custom claims + claims := &validator.ValidatedClaims{ + RegisteredClaims: validator.RegisteredClaims{ + Subject: "user-123", + }, + } - ctx := context.WithValue(context.Background(), mw.ContextKey{}, token) + ctx := core.SetClaims(context.Background(), claims) - // Test should return false when roles claim is missing + // Test should return false when custom claims is missing hasRole := UserHasRole(ctx, "admin") assert.False(t, hasRole) } -func TestUserHasRole_WithoutToken(t *testing.T) { +func TestUserHasRole_WithoutClaims(t *testing.T) { ctx := context.Background() - // Test should return false when no token in context + // Test should return false when no claims in context hasRole := UserHasRole(ctx, "admin") assert.False(t, hasRole) } -func TestUserHasRole_WithInvalidTokenType(t *testing.T) { - // Put invalid token type in context - ctx := context.WithValue(context.Background(), mw.ContextKey{}, "not-a-token") +func TestUserHasRole_WithEmptyRoles(t *testing.T) { + // Create claims with empty roles + claims := &validator.ValidatedClaims{ + RegisteredClaims: validator.RegisteredClaims{ + Subject: "user-123", + }, + CustomClaims: &CustomClaims{ + Roles: []string{}, + }, + } - // Test should return false when token type is invalid - hasRole := UserHasRole(ctx, "admin") - assert.False(t, hasRole) -} - -func TestUserHasRole_WithInvalidRolesType(t *testing.T) { - // Create token with invalid roles type - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": "user-123", - "https://unbound.se/roles": "not-an-array", - }) - - ctx := context.WithValue(context.Background(), mw.ContextKey{}, token) - - // Test should return false when roles type is invalid - hasRole := UserHasRole(ctx, "admin") - assert.False(t, hasRole) -} - -func TestUserHasRole_WithInvalidRoleElementType(t *testing.T) { - // Create token with invalid role element types - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": "user-123", - "https://unbound.se/roles": []interface{}{123, 456}, // Numbers instead of strings - }) - - ctx := context.WithValue(context.Background(), mw.ContextKey{}, token) - - // Test should return false when role elements are not strings + ctx := core.SetClaims(context.Background(), claims) + + // Test should return false when roles array is empty hasRole := UserHasRole(ctx, "admin") assert.False(t, hasRole) }