diff --git a/.link-checker-service.toml b/.link-checker-service.toml index de234f8..8eb3827 100644 --- a/.link-checker-service.toml +++ b/.link-checker-service.toml @@ -63,6 +63,9 @@ privKeyFile = "./dummy.priv.cer" pubKeyFile = "./public.cer" signingAlgorithm = "RS384" +# alternatively, via JWKS +# jwksUrl = "http://my-auth-provider/jwks" + # searchForBodyPatterns allows searching for patterns in response bodies # enabling searchForBodyPatterns will impact checker performance # this feature is only configurable via the config file diff --git a/CHANGES.md b/CHANGES.md index 10120e5..41e34e3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,14 @@ Notable changes will be documented here +## 0.9.35 + +- token validation via JWKS + +## 0.9.34 + +- upgraded dependencies + ## 0.9.33 - Go v1.21 diff --git a/cmd/serve.go b/cmd/serve.go index 7b49739..0431c54 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -25,6 +25,7 @@ const useJWTValidationKey = "useJWTValidation" const privKeyFileKey = "privKeyFile" const pubKeyFileKey = "pubKeyFile" const signingAlgorithmKey = "signingAlgorithm" +const jwksUrlKey = "jwksUrl" const disableRequestLoggingKey = "disableRequestLogging" var serveCmd = &cobra.Command{ @@ -73,6 +74,7 @@ func fetchConfig() { func fetchJWTValidationConfig() { jwtValidationOptions = &s.JWTValidationOptions{ + JwksUrl: viper.GetString(jwksUrlKey), PrivKeyFile: viper.GetString(privKeyFileKey), PubKeyFile: viper.GetString(pubKeyFileKey), SigningAlgorithm: viper.GetString(signingAlgorithmKey), @@ -104,6 +106,10 @@ func init() { "Provide a valid public key to validate the JWT tokens against") _ = viper.BindPFlag(signingAlgorithmKey, flags.Lookup(signingAlgorithmKey)) + flags.String(jwksUrlKey, "", + "Provide a JWKS Url for automatic JWT validation automation") + _ = viper.BindPFlag(jwksUrlKey, flags.Lookup(jwksUrlKey)) + flags.StringVar(&IPRateLimit, "IPRateLimit", "", "rate-limit requests from an IP. e.g. 5-S (5 per second), 1000-H (1000 per hour)") serveCmd.PersistentFlags().BoolP(disableRequestLoggingKey, "s", false, "disable request logging") diff --git a/go.mod b/go.mod index 7fd6c00..2492fc4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/siemens/link-checker-service go 1.19 require ( + github.com/MicahParks/keyfunc v1.9.0 github.com/appleboy/gin-jwt/v2 v2.9.1 github.com/darren/gpac v0.0.0-20210609082804-b56d6523a3af github.com/dgraph-io/ristretto v0.1.1 @@ -10,7 +11,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/go-resty/resty/v2 v2.10.0 github.com/gobwas/glob v0.2.3 - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.5.0 github.com/mitchellh/go-homedir v1.1.0 github.com/patrickmn/go-cache v2.1.0+incompatible @@ -39,7 +40,6 @@ require ( github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/glog v1.2.0 // indirect github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -50,7 +50,6 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index ca1a771..5e0516f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go/v5 v5.0.2/go.mod h1:ZI9JFB4ewXbw1sBnF4sxsR2k1H3xjV+PUAOUsHvKpcU= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxGWVZs= @@ -99,8 +101,7 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -169,7 +170,6 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= diff --git a/main_test_support.go b/main_test_support.go index 1945b6a..5c3b105 100644 --- a/main_test_support.go +++ b/main_test_support.go @@ -18,7 +18,7 @@ import ( "os" "time" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" ) // adapted the testing technique from diff --git a/server/server.go b/server/server.go index ff51aa2..5acfc1b 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,8 @@ package server import ( "context" + "github.com/gin-contrib/cors" + "github.com/gobwas/glob" "io" "log" "math" @@ -17,14 +19,13 @@ import ( "sync" "time" - jwt "github.com/appleboy/gin-jwt/v2" - "github.com/gobwas/glob" + "github.com/MicahParks/keyfunc" + ginGwt "github.com/appleboy/gin-jwt/v2" + jwtv4 "github.com/golang-jwt/jwt/v4" "github.com/ulule/limiter/v3" gm "github.com/ulule/limiter/v3/drivers/middleware/gin" "github.com/ulule/limiter/v3/drivers/store/memory" - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" "github.com/siemens/link-checker-service/infrastructure" ) @@ -39,6 +40,7 @@ type JWTValidationOptions struct { PrivKeyFile string PubKeyFile string SigningAlgorithm string + JwksUrl string } // Options configures the web service instance @@ -141,8 +143,7 @@ func (s *Server) setupRoutes() { s.setUpRateLimiting(checkURLsRoutes) if s.options.JWTValidationOptions != nil { - s.setUpJWTValidation(checkURLsRoutes) - s.setUpJWTValidation(statsRoutes) + s.setUpJWTValidation(checkURLsRoutes, statsRoutes) } checkURLsRoutes.POST("", s.checkURLs) @@ -374,7 +375,7 @@ func (s *Server) isBlacklisted(input URLRequest) bool { return false } -func (s *Server) setUpJWTValidation(routerGroup *gin.RouterGroup) { +func (s *Server) setUpJWTValidation(routerGroups ...*gin.RouterGroup) { if s.options.JWTValidationOptions == nil { log.Fatal("JWT Validation not set up correctly") } @@ -385,7 +386,8 @@ func (s *Server) setUpJWTValidation(routerGroup *gin.RouterGroup) { log.Printf(" SigningAlgorithm: %v", s.options.JWTValidationOptions.SigningAlgorithm) // the jwt middleware - middleware, err := jwt.New(&jwt.GinJWTMiddleware{ + middleware, err := ginGwt.New(&ginGwt.GinJWTMiddleware{ + KeyFunc: tryGetJwksKeyFunc(s.options.JWTValidationOptions.JwksUrl), PrivKeyFile: s.options.JWTValidationOptions.PrivKeyFile, PubKeyFile: s.options.JWTValidationOptions.PubKeyFile, SigningAlgorithm: s.options.JWTValidationOptions.SigningAlgorithm, @@ -399,7 +401,21 @@ func (s *Server) setUpJWTValidation(routerGroup *gin.RouterGroup) { log.Fatal("JWT Error:" + err.Error()) } - routerGroup.Use(middleware.MiddlewareFunc()) + for _, routerGroup := range routerGroups { + routerGroup.Use(middleware.MiddlewareFunc()) + } +} + +func tryGetJwksKeyFunc(jwksURL string) func(token *jwtv4.Token) (interface{}, error) { + kf, err := keyfunc.Get(jwksURL, keyfunc.Options{ + // to do: configurable + RefreshInterval: 1 * time.Hour, + }) + if err != nil { + return nil + } + log.Printf("JWKS configured with the url: %s", jwksURL) + return kf.Keyfunc } func (s *Server) setUpRateLimiting(routerGroup *gin.RouterGroup) {