diff --git a/cmd/main.go b/cmd/main.go index a4113e4..7f3f235 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ import ( "github.com/tetratelabs/telemetry" "github.com/tetrateio/authservice-go/internal" + "github.com/tetrateio/authservice-go/internal/oidc" "github.com/tetrateio/authservice-go/internal/server" ) @@ -31,8 +32,9 @@ func main() { var ( configFile = &internal.LocalConfigFile{} logging = internal.NewLogSystem(log.New(), &configFile.Config) - authz = server.NewExtAuthZFilter(&configFile.Config) - authzServer = server.New(&configFile.Config, authz.Register) + jwks = oidc.NewJWKSProvider() + envoyAuthz = server.NewExtAuthZFilter(&configFile.Config, jwks) + authzServer = server.New(&configFile.Config, envoyAuthz.Register) ) configLog := run.NewPreRunner("config-log", func() error { @@ -49,6 +51,7 @@ func main() { configFile, // load the configuration logging, // set up the logging system configLog, // log the configuration + jwks, // start the JWKS provider authzServer, // start the server &signal.Handler{}, // handle graceful termination ) diff --git a/go.mod b/go.mod index d92ea77..0ea7d21 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.6 require ( github.com/envoyproxy/go-control-plane v0.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 + github.com/lestrrat-go/jwx v1.2.28 github.com/stretchr/testify v1.8.4 github.com/tetratelabs/log v0.2.3 github.com/tetratelabs/run v0.2.2 @@ -19,14 +20,23 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/kr/text v0.2.0 // 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/option v1.0.1 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tetratelabs/multierror v1.1.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index dc8bcd6..6a2c8a7 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,18 @@ cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2Aawl github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ= github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +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/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -24,14 +30,35 @@ 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/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.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA= +github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8= +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/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/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/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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tetratelabs/log v0.2.3 h1:a+0omnV1Y/4PyOSbLXHsba1guq8MZmmSvq+n9YdNuLY= @@ -45,34 +72,50 @@ github.com/tetratelabs/telemetry v0.8.2/go.mod h1:jDUcf1A2u4F5V1io5RdipM/bKz/hFC github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= @@ -88,5 +131,6 @@ google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/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= diff --git a/internal/authz/handler.go b/internal/authz/handler.go index e86cc3f..e699bde 100644 --- a/internal/authz/handler.go +++ b/internal/authz/handler.go @@ -20,8 +20,8 @@ import ( envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" ) -// Authz is an interface for handling authorization requests. -type Authz interface { +// Handler is an interface for handling authorization requests. +type Handler interface { // Process a CheckRequest and populate a CheckResponse. Process(ctx context.Context, req *envoy.CheckRequest, resp *envoy.CheckResponse) error } diff --git a/internal/authz/mock.go b/internal/authz/mock.go index 7bb0a96..a1691a6 100644 --- a/internal/authz/mock.go +++ b/internal/authz/mock.go @@ -26,16 +26,16 @@ import ( "github.com/tetrateio/authservice-go/internal" ) -var _ Authz = (*mockHandler)(nil) +var _ Handler = (*mockHandler)(nil) -// mockHandler handler is an implementation of the Authz interface. +// mockHandler handler is an implementation of the Handler interface. type mockHandler struct { log telemetry.Logger config *mockv1.MockConfig } -// NewMockHandler creates a new Mock implementation of the Authz interface. -func NewMockHandler(cfg *mockv1.MockConfig) Authz { +// NewMockHandler creates a new Mock implementation of the Handler interface. +func NewMockHandler(cfg *mockv1.MockConfig) Handler { return &mockHandler{ log: internal.Logger(internal.Authz).With("type", "mockHandler"), config: cfg, diff --git a/internal/authz/oidc.go b/internal/authz/oidc.go index b8974e6..bd9706f 100644 --- a/internal/authz/oidc.go +++ b/internal/authz/oidc.go @@ -16,32 +16,43 @@ package authz import ( "context" + "time" envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" "github.com/tetratelabs/telemetry" oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" "github.com/tetrateio/authservice-go/internal" - "github.com/tetrateio/authservice-go/internal/authz/oidc" + "github.com/tetrateio/authservice-go/internal/oidc" ) -var _ Authz = (*oidcHandler)(nil) +var _ Handler = (*oidcHandler)(nil) -// oidc handler is an implementation of the Authz interface that implements +// oidc handler is an implementation of the Handler interface that implements // the OpenID connect protocol. type oidcHandler struct { log telemetry.Logger config *oidcv1.OIDCConfig store oidc.SessionStore + jwks oidc.JWKSProvider } -// NewOIDCHandler creates a new OIDC implementation of the Authz interface. -func NewOIDCHandler(cfg *oidcv1.OIDCConfig, store oidc.SessionStore) Authz { +// NewOIDCHandler creates a new OIDC implementation of the Handler interface. +func NewOIDCHandler(cfg *oidcv1.OIDCConfig, jwks oidc.JWKSProvider) (Handler, error) { + // TODO(nacx): Read the redis store config to configure the redi store + // TODO(nacx): Properly lifecycle the session store + store := oidc.NewMemoryStore( + oidc.Clock{}, + time.Duration(cfg.AbsoluteSessionTimeout), + time.Duration(cfg.IdleSessionTimeout), + ) + return &oidcHandler{ log: internal.Logger(internal.Authz).With("type", "oidc"), config: cfg, store: store, - } + jwks: jwks, + }, nil } // Process a CheckRequest and populate a CheckResponse according to the mockHandler configuration. diff --git a/internal/logging.go b/internal/logging.go index f98ea8b..186c31f 100644 --- a/internal/logging.go +++ b/internal/logging.go @@ -30,6 +30,7 @@ const ( Authz = "authz" Config = "config" Default = "default" + JWKS = "jwks" Requests = "requests" Server = "server" Session = "session" @@ -40,6 +41,7 @@ var scopes = map[string]string{ Authz: "Envoy ext-authz filter implementation messages", Config: "Configuration messages", Default: "Default", + JWKS: "JWKS update and parse messages", Requests: "Logs all requests and responses received by the server", Server: "Server request handling messages", Session: "Session store messages", diff --git a/internal/oidc/jwks.go b/internal/oidc/jwks.go new file mode 100644 index 0000000..1baea02 --- /dev/null +++ b/internal/oidc/jwks.go @@ -0,0 +1,129 @@ +// Copyright 2024 Tetrate +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oidc + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "time" + + "github.com/lestrrat-go/jwx/jwk" + "github.com/tetratelabs/run" + "github.com/tetratelabs/telemetry" + + oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" + "github.com/tetrateio/authservice-go/internal" +) + +var ( + // ErrJWKSParse is returned when the JWKS document cannot be parsed. + ErrJWKSParse = errors.New("error parsing JWKS document") + // ErrJWKSFetch is returned when the JWKS document cannot be fetched. + ErrJWKSFetch = errors.New("error fetching JWKS document") + + _ run.Service = (*DefaultJWKSProvider)(nil) +) + +// JWKSProvider provides a JWKS set for a given OIDC configuration. +type JWKSProvider interface { + // Get the JWKS for the given OIDC configuration + Get(context.Context, *oidcv1.OIDCConfig) (jwk.Set, error) +} + +// DefaultJWKSProvider provides a JWKS set +type DefaultJWKSProvider struct { + log telemetry.Logger + cache *jwk.AutoRefresh + shutdown context.CancelFunc +} + +// NewJWKSProvider returns a new JWKSProvider. +func NewJWKSProvider() *DefaultJWKSProvider { + return &DefaultJWKSProvider{ + log: internal.Logger(internal.JWKS), + } +} + +// Name of the JWKSProvider run.Unit +func (j *DefaultJWKSProvider) Name() string { return "JWKS" } + +// Serve implements run.Service +func (j *DefaultJWKSProvider) Serve() error { + ctx, cancel := context.WithCancel(context.Background()) + j.shutdown = cancel + + ch := make(chan jwk.AutoRefreshError) + j.cache = jwk.NewAutoRefresh(ctx) + j.cache.ErrorSink(ch) + + for { + select { + case err := <-ch: + j.log.Debug("jwks auto refresh error", "error", err) + case <-ctx.Done(): + return nil + } + } +} + +// GracefulStop implements run.Service +func (j *DefaultJWKSProvider) GracefulStop() { + if j.shutdown != nil { + j.shutdown() + } +} + +// Get the JWKS for the given OIDC configuration +func (j *DefaultJWKSProvider) Get(ctx context.Context, config *oidcv1.OIDCConfig) (jwk.Set, error) { + if config.GetJwksFetcher() != nil { + return j.fetchDynamic(ctx, config.GetJwksFetcher()) + } + return j.fetchStatic(config.GetJwks()) +} + +// fetchDynamic fetches the JWKS from the given URI. If the JWKS URI is already know, the JWKS will be returned from +// the cache. Otherwise, the JWKS will be fetched from the URI and the cache will be configured to periodically +// refresh the JWKS. +func (j *DefaultJWKSProvider) fetchDynamic(ctx context.Context, config *oidcv1.OIDCConfig_JwksFetcherConfig) (jwk.Set, error) { + if !j.cache.IsRegistered(config.JwksUri) { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: config.SkipVerifyPeerCert} + client := &http.Client{Transport: transport} + refreshInterval := time.Duration(config.PeriodicFetchIntervalSec) * time.Second + + j.cache.Configure(config.JwksUri, + jwk.WithHTTPClient(client), + jwk.WithRefreshInterval(refreshInterval), + ) + } + + jwks, err := j.cache.Fetch(ctx, config.JwksUri) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrJWKSFetch, err) + } + return jwks, nil +} + +// fetchStatic parses the given raw JWKS document. +func (*DefaultJWKSProvider) fetchStatic(raw string) (jwk.Set, error) { + jwks, err := jwk.Parse([]byte(raw)) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrJWKSParse, err) + } + return jwks, nil +} diff --git a/internal/oidc/jwks_test.go b/internal/oidc/jwks_test.go new file mode 100644 index 0000000..9cfe67f --- /dev/null +++ b/internal/oidc/jwks_test.go @@ -0,0 +1,289 @@ +// Copyright 2024 Tetrate +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oidc + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/stretchr/testify/require" + + oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" +) + +// nolint: lll +var ( + keys = ` +{ + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47", + "n": + "up97uqrF9MWOPaPkwSaBeuAPLOr9FKcaWGdVEGzQ4f3Zq5WKVZowx9TCBxmImNJ1qmUi13pB8otwM_l5lfY1AFBMxVbQCUXntLovhDaiSvYp4wGDjFzQiYA-pUq8h6MUZBnhleYrkU7XlCBwNVyN8qNMkpLA7KFZYz-486GnV2NIJJx_4BGa3HdKwQGxi2tjuQsQvao5W4xmSVaaEWopBwMy2QmlhSFQuPUpTaywTqUcUq_6SfAHhZ4IDa_FxEd2c2z8gFGtfst9cY3lRYf-c_ZdboY3mqN9Su3-j3z5r2SHWlhB_LNAjyWlBGsvbGPlTqDziYQwZN4aGsqVKQb9Vw", + "e": "AQAB" + }, + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "b3319a147514df7ee5e4bcdee51350cc890cc89e", + "n": + "up97uqrF9MWOPaPkwSaBeuAPLOr9FKcaWGdVEGzQ4f3Zq5WKVZowx9TCBxmImNJ1qmUi13pB8otwM_l5lfY1AFBMxVbQCUXntLovhDaiSvYp4wGDjFzQiYA-pUq8h6MUZBnhleYrkU7XlCBwNVyN8qNMkpLA7KFZYz-486GnV2NIJJx_4BGa3HdKwQGxi2tjuQsQvao5W4xmSVaaEWopBwMy2QmlhSFQuPUpTaywTqUcUq_6SfAHhZ4IDa_FxEd2c2z8gFGtfst9cY3lRYf-c_ZdboY3mqN9Su3-j3z5r2SHWlhB_LNAjyWlBGsvbGPlTqDziYQwZN4aGsqVKQb9Vw", + "e": "AQAB" + } + ] +} +` + + singleKey = ` +{ + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47", + "n": + "up97uqrF9MWOPaPkwSaBeuAPLOr9FKcaWGdVEGzQ4f3Zq5WKVZowx9TCBxmImNJ1qmUi13pB8otwM_l5lfY1AFBMxVbQCUXntLovhDaiSvYp4wGDjFzQiYA-pUq8h6MUZBnhleYrkU7XlCBwNVyN8qNMkpLA7KFZYz-486GnV2NIJJx_4BGa3HdKwQGxi2tjuQsQvao5W4xmSVaaEWopBwMy2QmlhSFQuPUpTaywTqUcUq_6SfAHhZ4IDa_FxEd2c2z8gFGtfst9cY3lRYf-c_ZdboY3mqN9Su3-j3z5r2SHWlhB_LNAjyWlBGsvbGPlTqDziYQwZN4aGsqVKQb9Vw", + "e": "AQAB" +} +` +) + +func TestStaticJWKSProvider(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + cache := NewJWKSProvider() + go func() { require.NoError(t, cache.Serve()) }() + t.Cleanup(cache.GracefulStop) + + _, err := cache.Get(context.Background(), &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_Jwks{ + Jwks: "{aaa}", + }, + }) + + require.ErrorIs(t, err, ErrJWKSParse) + }) + + t.Run("single-key", func(t *testing.T) { + cache := NewJWKSProvider() + go func() { require.NoError(t, cache.Serve()) }() + t.Cleanup(cache.GracefulStop) + + jwks, err := cache.Get(context.Background(), &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_Jwks{ + Jwks: singleKey, + }, + }) + + require.NoError(t, err) + require.Equal(t, 1, jwks.Len()) + + key, ok := jwks.Get(0) + require.True(t, ok) + require.Equal(t, "RS256", key.Algorithm()) + require.Equal(t, jwa.KeyType("RSA"), key.KeyType()) + require.Equal(t, "62a93512c9ee4c7f8067b5a216dade2763d32a47", key.KeyID()) + }) + + t.Run("multiple-keys", func(t *testing.T) { + cache := NewJWKSProvider() + go func() { require.NoError(t, cache.Serve()) }() + t.Cleanup(cache.GracefulStop) + + jwks, err := cache.Get(context.Background(), &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_Jwks{ + Jwks: keys, + }, + }) + + require.NoError(t, err) + require.Equal(t, 2, jwks.Len()) + + key, ok := jwks.Get(0) + require.True(t, ok) + require.Equal(t, "RS256", key.Algorithm()) + require.Equal(t, jwa.KeyType("RSA"), key.KeyType()) + require.Equal(t, "62a93512c9ee4c7f8067b5a216dade2763d32a47", key.KeyID()) + + key, ok = jwks.Get(1) + require.True(t, ok) + require.Equal(t, "RS256", key.Algorithm()) + require.Equal(t, jwa.KeyType("RSA"), key.KeyType()) + require.Equal(t, "b3319a147514df7ee5e4bcdee51350cc890cc89e", key.KeyID()) + }) +} + +func TestDynamicJWKSProvider(t *testing.T) { + var ( + pub = newKey(t) + jwks = newKeySet(pub) + + newCache = func(t *testing.T) JWKSProvider { + cache := NewJWKSProvider() + go func() { require.NoError(t, cache.Serve()) }() + t.Cleanup(cache.GracefulStop) + // Block until the cache is initialized + require.Eventually(t, func() bool { + return cache.cache != nil + }, 10*time.Second, 50*time.Millisecond) + return cache + } + ) + + t.Run("invalid url", func(t *testing.T) { + server := newTestServer(t, jwks) + cache := newCache(t) + + config := &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ + JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ + JwksUri: server.URL + "/not-found", + PeriodicFetchIntervalSec: 1, + }, + }, + } + + _, err := cache.Get(context.Background(), config) + + require.ErrorIs(t, err, ErrJWKSFetch) + require.Equal(t, 1, server.requestCount) // The attempt to load the JWKS is made, but fails + }) + + t.Run("cache load", func(t *testing.T) { + server := newTestServer(t, jwks) + cache := newCache(t) + + config := &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ + JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ + JwksUri: server.URL, + PeriodicFetchIntervalSec: 1, + SkipVerifyPeerCert: true, + }, + }, + } + + keys, err := cache.Get(context.Background(), config) + require.NoError(t, err) + require.Equal(t, jwks, keys) + require.Equal(t, 1, server.requestCount) + }) + + t.Run("cached results", func(t *testing.T) { + server := newTestServer(t, jwks) + cache := newCache(t) + + config := &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ + JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ + JwksUri: server.URL, + PeriodicFetchIntervalSec: 60, + }, + }, + } + + for i := 0; i < 5; i++ { + keys, err := cache.Get(context.Background(), config) + require.NoError(t, err) + require.Equal(t, jwks, keys) + require.Equal(t, 1, server.requestCount) // Cached results after the first request + } + }) + + t.Run("cache refresh", func(t *testing.T) { + server := newTestServer(t, jwks) + cache := newCache(t) + + config := &oidcv1.OIDCConfig{ + JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ + JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ + JwksUri: server.URL, + PeriodicFetchIntervalSec: 1, + }, + }, + } + + // Load the entry in the cache and remove it to let the background refresher refresh it + _, err := cache.Get(context.Background(), config) + require.NoError(t, err) + jwks.Remove(pub) + + // Wait for the refresh period and check that the JWKS has been refreshed + require.Eventually(t, func() bool { + return server.requestCount > 1 + }, 3*time.Second, 1*time.Second) + }) +} + +type server struct { + *httptest.Server + requestCount int +} + +func newTestServer(t *testing.T, jwks jwk.Set) *server { + s := &server{} + s.Server = httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + s.requestCount++ + + if strings.HasSuffix(req.URL.Path, "/not-found") { + res.WriteHeader(404) + return + } + + bytes, err := json.Marshal(jwks) + require.NoError(t, err) + res.WriteHeader(200) + _, _ = res.Write(bytes) + })) + t.Cleanup(func() { s.requestCount = 0 }) + t.Cleanup(s.Close) + return s +} + +const keyID = "test" + +func newKeySet(keys ...jwk.Key) jwk.Set { + jwks := jwk.NewSet() + for _, k := range keys { + jwks.Add(k) + } + return jwks +} + +func newKey(t *testing.T) jwk.Key { + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + pub, err := jwk.New(rsaKey.PublicKey) + require.NoError(t, err) + + err = pub.Set(jwk.KeyIDKey, keyID) + require.NoError(t, err) + err = pub.Set(jwk.AlgorithmKey, jwa.RS256) + require.NoError(t, err) + + return pub +} diff --git a/internal/authz/oidc/memory.go b/internal/oidc/memory.go similarity index 100% rename from internal/authz/oidc/memory.go rename to internal/oidc/memory.go diff --git a/internal/authz/oidc/memory_test.go b/internal/oidc/memory_test.go similarity index 100% rename from internal/authz/oidc/memory_test.go rename to internal/oidc/memory_test.go diff --git a/internal/authz/oidc/session.go b/internal/oidc/session.go similarity index 100% rename from internal/authz/oidc/session.go rename to internal/oidc/session.go diff --git a/internal/authz/oidc/state.go b/internal/oidc/state.go similarity index 100% rename from internal/authz/oidc/state.go rename to internal/oidc/state.go diff --git a/internal/authz/oidc/time.go b/internal/oidc/time.go similarity index 100% rename from internal/authz/oidc/time.go rename to internal/oidc/time.go diff --git a/internal/authz/oidc/time_test.go b/internal/oidc/time_test.go similarity index 100% rename from internal/authz/oidc/time_test.go rename to internal/oidc/time_test.go diff --git a/internal/authz/oidc/token.go b/internal/oidc/token.go similarity index 100% rename from internal/authz/oidc/token.go rename to internal/oidc/token.go diff --git a/internal/server/authz.go b/internal/server/authz.go index 54e5212..88bd85f 100644 --- a/internal/server/authz.go +++ b/internal/server/authz.go @@ -19,7 +19,6 @@ import ( "fmt" "regexp" "strings" - "time" envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" "github.com/tetratelabs/telemetry" @@ -30,7 +29,7 @@ import ( configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" "github.com/tetrateio/authservice-go/internal" "github.com/tetrateio/authservice-go/internal/authz" - "github.com/tetrateio/authservice-go/internal/authz/oidc" + "github.com/tetrateio/authservice-go/internal/oidc" ) // EnvoyXRequestID is the header name for the request id @@ -58,15 +57,17 @@ var ( // ExtAuthZFilter is an implementation of the Envoy AuthZ filter. type ExtAuthZFilter struct { - log telemetry.Logger - cfg *configv1.Config + log telemetry.Logger + cfg *configv1.Config + jwks oidc.JWKSProvider } // NewExtAuthZFilter creates a new ExtAuthZFilter. -func NewExtAuthZFilter(cfg *configv1.Config) *ExtAuthZFilter { +func NewExtAuthZFilter(cfg *configv1.Config, jwks oidc.JWKSProvider) *ExtAuthZFilter { return &ExtAuthZFilter{ - log: internal.Logger(internal.Authz), - cfg: cfg, + log: internal.Logger(internal.Authz), + cfg: cfg, + jwks: jwks, } } @@ -103,7 +104,7 @@ func (e *ExtAuthZFilter) Check(ctx context.Context, req *envoy.CheckRequest) (re // Inside a filter chain, all filters must match for i, f := range c.Filters { var ( - h authz.Authz + h authz.Handler resp = &envoy.CheckResponse{} ) @@ -113,23 +114,15 @@ func (e *ExtAuthZFilter) Check(ctx context.Context, req *envoy.CheckRequest) (re case *configv1.Filter_Mock: h = authz.NewMockHandler(ft.Mock) case *configv1.Filter_Oidc: - // TODO(nacx): Read the redis store config to configure the redi store - store := oidc.NewMemoryStore( - oidc.Clock{}, - time.Duration(ft.Oidc.AbsoluteSessionTimeout), - time.Duration(ft.Oidc.IdleSessionTimeout), - ) // TODO(nacx): Check if the Oidc setting is enough or we have to pull the default Oidc settings - h = authz.NewOIDCHandler(ft.Oidc, store) + h, err = authz.NewOIDCHandler(ft.Oidc, e.jwks) case *configv1.Filter_OidcOverride: - // TODO(nacx): Read the redis store config to configure the redi store - store := oidc.NewMemoryStore( - oidc.Clock{}, - time.Duration(ft.OidcOverride.AbsoluteSessionTimeout), - time.Duration(ft.OidcOverride.IdleSessionTimeout), - ) // TODO(nacx): Check if the OidcOverride is enough or we have to pull the default Oidc settings - h = authz.NewOIDCHandler(ft.OidcOverride, store) + h, err = authz.NewOIDCHandler(ft.OidcOverride, e.jwks) + } + + if err != nil { + return nil, err } if err = h.Process(ctx, req, resp); err != nil { diff --git a/internal/server/authz_test.go b/internal/server/authz_test.go index fb8f325..c6ef672 100644 --- a/internal/server/authz_test.go +++ b/internal/server/authz_test.go @@ -39,7 +39,7 @@ func TestUnmatchedRequests(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := NewExtAuthZFilter(&configv1.Config{AllowUnmatchedRequests: tt.allow}) + e := NewExtAuthZFilter(&configv1.Config{AllowUnmatchedRequests: tt.allow}, nil) got, err := e.Check(context.Background(), &envoy.CheckRequest{}) require.NoError(t, err) require.Equal(t, int32(tt.want), got.Status.Code) @@ -61,7 +61,7 @@ func TestFiltersMatch(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &configv1.Config{Chains: []*configv1.FilterChain{{Filters: tt.filters}}} - e := NewExtAuthZFilter(cfg) + e := NewExtAuthZFilter(cfg, nil) got, err := e.Check(context.Background(), &envoy.CheckRequest{}) require.NoError(t, err) @@ -91,7 +91,7 @@ func TestUseFirstMatchingChain(t *testing.T) { }, } - e := NewExtAuthZFilter(cfg) + e := NewExtAuthZFilter(cfg, nil) got, err := e.Check(context.Background(), header("match")) require.NoError(t, err) @@ -121,7 +121,7 @@ func TestMatch(t *testing.T) { } func TestGrpcNoChainsMatched(t *testing.T) { - e := NewExtAuthZFilter(&configv1.Config{}) + e := NewExtAuthZFilter(&configv1.Config{}, nil) s := NewTestServer(e.Register) go func() { require.NoError(t, s.Start()) }() t.Cleanup(s.Stop) @@ -274,7 +274,7 @@ func TestCheckTriggerRules(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := NewExtAuthZFilter(tt.config) + e := NewExtAuthZFilter(tt.config, nil) req := &envoy.CheckRequest{ Attributes: &envoy.AttributeContext{ Request: &envoy.AttributeContext_Request{