diff --git a/go.mod b/go.mod index eb303eac1e..4a762187ce 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,9 @@ require ( github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af github.com/centrifugal/centrifuge v0.14.2 github.com/centrifugal/protocol v0.3.4 - github.com/cristalhq/jwt/v3 v3.0.0 + github.com/cristalhq/jwt/v3 v3.0.7 github.com/gogo/protobuf v1.3.1 + github.com/google/go-cmp v0.5.4 // indirect github.com/google/uuid v1.1.2 github.com/gorilla/securecookie v1.1.1 github.com/mattn/go-isatty v0.0.12 @@ -23,13 +24,16 @@ require ( github.com/prometheus/client_golang v1.7.1 github.com/prometheus/common v0.14.0 // indirect github.com/prometheus/procfs v0.2.0 // indirect - github.com/rs/zerolog v1.18.0 + github.com/rakutentech/jwk-go v1.0.1 + github.com/rs/zerolog v1.20.0 github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v0.0.7 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20200625001655-4c5254603344 + golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect google.golang.org/grpc v1.28.0 google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 2287be7d41..e71e32e2bf 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/cristalhq/jwt/v3 v3.0.0 h1:0PtBLOa6XEZj6powOaneZCxU3+yiFSYwVeL2wmbavnY= -github.com/cristalhq/jwt/v3 v3.0.0/go.mod h1:XOnIXst8ozq/esy5N1XOlSyQqBd+84fxJ99FK+1jgL8= +github.com/cristalhq/jwt/v3 v3.0.7 h1:QPyEkOxV9ly40Mdg9Zt1HbylGPN/cqTOwplO+h1TPEM= +github.com/cristalhq/jwt/v3 v3.0.7/go.mod h1:XOnIXst8ozq/esy5N1XOlSyQqBd+84fxJ99FK+1jgL8= 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= @@ -87,6 +87,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -135,6 +136,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -178,6 +181,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/igm/sockjs-go/v3 v3.0.0 h1:4wLoB9WCnQ8RI87cmqUH778ACDFVmRpkKRCWBeuc+Ww= @@ -261,7 +265,12 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= @@ -328,12 +337,14 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakutentech/jwk-go v1.0.1 h1:yCE+M/FsgPbO94hMIRkcMNFZAlmyFwmrstg717dykFc= +github.com/rakutentech/jwk-go v1.0.1/go.mod h1:LtzSv4/+Iti1nnNeVQiP6l5cI74GBStbhyXCYvgPZFk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8= -github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -364,12 +375,15 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -377,7 +391,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= @@ -435,7 +448,10 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -450,6 +466,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -529,9 +546,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= @@ -542,6 +561,10 @@ gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/jwks/cache.go b/internal/jwks/cache.go new file mode 100644 index 0000000000..54b1e8ace1 --- /dev/null +++ b/internal/jwks/cache.go @@ -0,0 +1,22 @@ +package jwks + +import ( + "context" + "errors" +) + +var ( + // ErrEmptyKeyID raises when input kid is empty. + ErrEmptyKeyID = errors.New("cache: empty kid") + // ErrCacheNotFound raises when cache value not found. + ErrCacheNotFound = errors.New("cache: value not found") + // ErrInvalidValue raises when type conversion to JWK has been failed. + ErrInvalidValue = errors.New("cache: invalid value") +) + +// Cache works with cache layer. +type Cache interface { + Add(ctx context.Context, key *JWK) error + Get(ctx context.Context, kid string) (*JWK, error) + Len(ctx context.Context) (int, error) +} diff --git a/internal/jwks/cache_ttl.go b/internal/jwks/cache_ttl.go new file mode 100644 index 0000000000..937dc28b83 --- /dev/null +++ b/internal/jwks/cache_ttl.go @@ -0,0 +1,139 @@ +package jwks + +import ( + "context" + "sync" + "time" +) + +type item struct { + sync.RWMutex + data *JWK + expiration *time.Time +} + +func (i *item) touch(d time.Duration) { + i.Lock() + exp := time.Now().Add(d) + i.expiration = &exp + i.Unlock() +} + +func (i *item) expired() bool { + i.RLock() + res := true + if i.expiration != nil { + res = i.expiration.Before(time.Now()) + } + i.RUnlock() + return res +} + +// TTLCache is a TTL bases in-memory cache. +type TTLCache struct { + mu sync.RWMutex + ttl time.Duration + stop chan struct{} + items map[string]*item +} + +// NewTTLCache returns a new instance of ttl cache. +func NewTTLCache(ttl time.Duration) *TTLCache { + cache := &TTLCache{ + ttl: ttl, + stop: make(chan struct{}), + items: make(map[string]*item), + } + cache.run() + return cache +} + +func (tc *TTLCache) cleanup() { + tc.mu.Lock() + for key, item := range tc.items { + if item.expired() { + delete(tc.items, key) + } + } + tc.mu.Unlock() +} + +func (tc *TTLCache) run() { + d := tc.ttl + if d < time.Second { + d = time.Second + } + + ticker := time.Tick(d) + go func() { + for { + select { + case <-ticker: + tc.cleanup() + case <-tc.stop: + return + } + } + }() +} + +// Add item into cache. +func (tc *TTLCache) Add(_ context.Context, key *JWK) error { + tc.mu.Lock() + item := &item{data: key} + item.touch(tc.ttl) + tc.items[key.Kid] = item + tc.mu.Unlock() + return nil +} + +// Get item by key. +func (tc *TTLCache) Get(_ context.Context, kid string) (*JWK, error) { + tc.mu.RLock() + item, ok := tc.items[kid] + if !ok || item.expired() { + tc.mu.RUnlock() + return nil, ErrCacheNotFound + } + item.touch(tc.ttl) + tc.mu.RUnlock() + return item.data, nil +} + +// Remove item by key. +func (tc *TTLCache) Remove(_ context.Context, kid string) error { + tc.mu.Lock() + delete(tc.items, kid) + tc.mu.Unlock() + return nil +} + +// Contains checks item on existence. +func (tc *TTLCache) Contains(_ context.Context, kid string) (bool, error) { + tc.mu.RLock() + _, ok := tc.items[kid] + tc.mu.RUnlock() + return ok, nil +} + +// Len returns current size of cache. +func (tc *TTLCache) Len(_ context.Context) (int, error) { + tc.mu.RLock() + n := len(tc.items) + tc.mu.RUnlock() + return n, nil +} + +// Purge deletes all items. +func (tc *TTLCache) Purge(_ context.Context) error { + tc.mu.Lock() + tc.items = map[string]*item{} + tc.mu.Unlock() + return nil +} + +// Stop cleanup process. +func (tc *TTLCache) Stop(_ context.Context) error { + tc.stop <- struct{}{} + return nil +} diff --git a/internal/jwks/cache_ttl_test.go b/internal/jwks/cache_ttl_test.go new file mode 100644 index 0000000000..3c0fcf793a --- /dev/null +++ b/internal/jwks/cache_ttl_test.go @@ -0,0 +1,285 @@ +package jwks + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTTLCacheInit(t *testing.T) { + cache := NewTTLCache(5 * time.Minute) + require.NotNil(t, cache) +} + +func TestTTLCacheAdd(t *testing.T) { + testCases := []struct { + Name string + TTL time.Duration + Ops int + }{ + { + Name: "OK", + TTL: 5 * time.Second, + Ops: 100, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx := context.Background() + + cache := NewTTLCache(tc.TTL) + require.NotNil(t, cache) + + for i := 0; i < tc.Ops; i++ { + require.NoError(t, cache.Add(ctx, &JWK{ + Kid: fmt.Sprintf("key-%d", i+1), + Kty: "RSA", + Alg: "RS256", + Use: "sig", + })) + } + }) + } +} + +func TestTTLCacheGet(t *testing.T) { + testCases := []struct { + Name string + Key *JWK + Kid string + Error error + }{ + { + Name: "OK", + Key: &JWK{ + Kid: "202101", + Kty: "RSA", + Alg: "RS256", + Use: "sig", + }, + Kid: "202101", + }, + { + Name: "NotFound", + Key: &JWK{ + Kid: "202101", + Kty: "RSA", + Alg: "RS256", + Use: "sig", + }, + Kid: "202102", + Error: ErrCacheNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx := context.Background() + + cache := NewTTLCache(5 * time.Minute) + require.NotNil(t, cache) + require.NoError(t, cache.Add(ctx, tc.Key)) + + key, err := cache.Get(ctx, tc.Kid) + if tc.Error != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.Error) + } else { + require.NoError(t, err) + require.EqualValues(t, tc.Key, key) + } + }) + } +} + +func TestTTLCacheRemove(t *testing.T) { + testCases := []struct { + Name string + Adds int + Dels int + Len int + }{ + { + Name: "OK", + Adds: 75, + Dels: 50, + Len: 25, + }, + { + Name: "RemoveUntilEmpty", + Adds: 75, + Dels: 100, + Len: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx := context.Background() + + cache := NewTTLCache(5 * time.Minute) + require.NotNil(t, cache) + + for i := 0; i < tc.Adds; i++ { + require.NoError(t, cache.Add(ctx, &JWK{ + Kid: fmt.Sprintf("key-%d", i+1), + Kty: "RSA", + Alg: "RS256", + Use: "sig", + })) + } + + for i := 0; i < tc.Dels; i++ { + kid := fmt.Sprintf("key-%d", i+1) + require.NoError(t, cache.Remove(ctx, kid)) + } + + n, err := cache.Len(ctx) + require.NoError(t, err) + require.Equal(t, tc.Len, n) + }) + } +} + +func TestTTLCacheContains(t *testing.T) { + testCases := []struct { + Name string + Key *JWK + Kid string + Found bool + }{ + { + Name: "OK", + Key: &JWK{ + Kid: "202101", + Kty: "RSA", + Alg: "RS256", + Use: "sig", + }, + Kid: "202101", + Found: true, + }, + { + Name: "NotFound", + Key: &JWK{ + Kid: "202101", + Kty: "RSA", + Alg: "RS256", + Use: "sig", + }, + Kid: "202102", + Found: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx := context.Background() + + cache := NewTTLCache(5 * time.Minute) + require.NotNil(t, cache) + require.NoError(t, cache.Add(ctx, tc.Key)) + + found, err := cache.Contains(ctx, tc.Kid) + require.NoError(t, err) + + require.Equal(t, tc.Found, found) + }) + } +} + +func TestTTLCacheLen(t *testing.T) { + testCases := []struct { + Name string + Ops int + Len int + }{ + { + Name: "OK", + Ops: 50, + Len: 50, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx := context.Background() + + cache := NewTTLCache(5 * time.Second) + require.NotNil(t, cache) + + for i := 0; i < tc.Ops; i++ { + require.NoError(t, cache.Add(ctx, &JWK{ + Kid: fmt.Sprintf("key-%d", i+1), + Kty: "RSA", + Alg: "RS256", + Use: "sig", + })) + } + + n, err := cache.Len(ctx) + require.NoError(t, err) + require.Equal(t, tc.Len, n) + }) + } +} + +func TestTTLCachePurge(t *testing.T) { + testCases := []struct { + Name string + Ops int + }{ + { + Name: "OK", + Ops: 50, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx := context.Background() + + cache := NewTTLCache(5 * time.Second) + require.NotNil(t, cache) + + for i := 0; i < tc.Ops; i++ { + require.NoError(t, cache.Add(ctx, &JWK{ + Kid: fmt.Sprintf("key-%d", i+1), + Kty: "RSA", + Alg: "RS256", + Use: "sig", + })) + } + + require.NoError(t, cache.Purge(ctx)) + + n, err := cache.Len(ctx) + require.NoError(t, err) + require.Equal(t, 0, n) + }) + } +} + +func TestTTLCacheCleanup(t *testing.T) { + ctx := context.Background() + cache := NewTTLCache(1 * time.Millisecond) + + for i := 0; i < 10; i++ { + require.NoError(t, cache.Add(ctx, &JWK{ + Kid: fmt.Sprintf("key-%d", i+1), + Kty: "RSA", + Alg: "RS256", + Use: "sig", + })) + } + + time.Sleep(2 * time.Second) + + n, err := cache.Len(ctx) + require.NoError(t, err) + require.Equal(t, 0, n) +} diff --git a/internal/jwks/manager.go b/internal/jwks/manager.go new file mode 100644 index 0000000000..9ee7ae78a9 --- /dev/null +++ b/internal/jwks/manager.go @@ -0,0 +1,161 @@ +package jwks + +import ( + "context" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "time" + + "github.com/rakutentech/jwk-go/jwk" + "golang.org/x/sync/singleflight" +) + +const ( + _defaultRetries = 2 + _defaultTimeout = 1 * time.Second + _defaultTTL = 1 * time.Hour +) + +// JWK represents an unparsed JSON Web Key (JWK) in its wire format. +type JWK = jwk.JWK + +var ( + // ErrConnectionFailed raises when JWKS endpoint cannot be reached. + ErrConnectionFailed = errors.New("jwks: connection failed") + // ErrInvalidURL raises when input url has invalid format. + ErrInvalidURL = errors.New("jwks: invalid url value or format") + // ErrKeyIDNotProvided raises when input kid is not present. + ErrKeyIDNotProvided = errors.New("jwks: kid is not provided") + // ErrPublicKeyNotFound raises when no public key is found. + ErrPublicKeyNotFound = errors.New("jwks: public key not found") +) + +// Manager fetches and returns JWK from public source. +type Manager struct { + url *url.URL + cache Cache + client *http.Client + lookup bool + retries int + group singleflight.Group +} + +// NewManager returns a new instance of `Manager`. +func NewManager(rawurl string, opts ...Option) (*Manager, error) { + url, err := url.Parse(rawurl) + if err != nil { + return nil, ErrInvalidURL + } + + mng := &Manager{ + url: url, + cache: NewTTLCache(_defaultTTL), + client: &http.Client{Timeout: _defaultTimeout}, + lookup: true, + retries: _defaultRetries, + } + + for _, opt := range opts { + opt(mng) + } + + return mng, nil +} + +// FetchKey fetches JWKS from public source or cache. +func (m *Manager) FetchKey(ctx context.Context, kid string) (*JWK, error) { + if kid == "" { + return nil, ErrKeyIDNotProvided + } + + // If lookup is true, first try to get key from cache. + if m.lookup { + key, err := m.cache.Get(ctx, kid) + if err == nil { + return key, nil + } + } + + // Otherwise fetch from public JWKS. + v, err, _ := m.group.Do(kid, func() (interface{}, error) { + return m.fetchKey(ctx, kid) + }) + if err != nil { + return nil, err + } + + return v.(*JWK), nil +} + +func (m *Manager) fetchKey(ctx context.Context, kid string) (*JWK, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.url.String(), nil) + if err != nil { + return nil, err + } + + var set jwk.KeySpecSet + + // Make sure that you have exponential back off on this http request with retries. + retries := m.retries + for retries > 0 { + retries-- + + resp, err := m.client.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + continue + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + continue + } + + if err := json.Unmarshal(data, &set); err != nil { + return nil, err + } + + break + } + + if retries == 0 { + return nil, ErrConnectionFailed + } + + if len(set.Keys) == 0 { + return nil, ErrPublicKeyNotFound + } + + var res *JWK + + // Save new set into cache. + for _, spec := range set.Keys { + jwk, err := spec.ToJWK() + if err != nil { + return nil, err + } + + if m.lookup { + if err := m.cache.Add(ctx, jwk); err != nil { + return nil, err + } + } + + if jwk.Kid == kid { + res = jwk + } + } + + if res == nil || res.Kty != "RSA" || res.Use != "sig" { + return nil, ErrPublicKeyNotFound + } + + return res, nil +} diff --git a/internal/jwks/manager_test.go b/internal/jwks/manager_test.go new file mode 100644 index 0000000000..ebc18f6b90 --- /dev/null +++ b/internal/jwks/manager_test.go @@ -0,0 +1,152 @@ +package jwks + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/rakutentech/jwk-go/jwk" + "github.com/stretchr/testify/require" +) + +type testKey struct { + Kid string + Key interface{} +} + +func randomKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) { + priv, err := rsa.GenerateKey(rand.Reader, 512) + if err != nil { + return nil, nil, err + } + + return priv, &priv.PublicKey, nil +} + +func jwksHandler(keys ...testKey) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + specs := jwk.KeySpecSet{} + + for _, key := range keys { + spec := jwk.NewSpecWithID(key.Kid, key.Key) + spec.Use = "sig" + specs.Keys = append(specs.Keys, *spec) + } + + data, err := json.Marshal(specs) + if err != nil { + http.Error(w, "Server Error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) +} + +func TestManagerInit(t *testing.T) { + manager, err := NewManager("https:example.com/.well-known/jwks.json") + require.NoError(t, err) + require.NotNil(t, manager) +} + +func TestManagerFailedFetchKey(t *testing.T) { + manager, err := NewManager("https:example.com/.well-known/jwks.json") + require.NoError(t, err) + + _, err = manager.FetchKey(context.Background(), "202101") + require.ErrorIs(t, err, ErrConnectionFailed) +} + +func TestManagerInitialFetchKey(t *testing.T) { + _, pubKey, err := randomKeys() + require.NoError(t, err) + + testCases := []struct { + Name string + Handler http.Handler + Kid string + Error error + }{ + { + Name: "OK", + Handler: jwksHandler(testKey{"202101", pubKey}), + Kid: "202101", + }, + { + Name: "NotFound", + Handler: jwksHandler(testKey{"202101", pubKey}), + Kid: "202102", + Error: ErrPublicKeyNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + r := require.New(t) + + ts := httptest.NewServer(tc.Handler) + defer ts.Close() + + manager, err := NewManager(ts.URL) + r.NoError(err) + + key, err := manager.FetchKey(context.Background(), tc.Kid) + if tc.Error != nil { + r.Error(err) + r.ErrorIs(err, tc.Error) + } else { + r.NoError(err) + r.Equal(tc.Kid, key.Kid) + } + }) + } +} + +func TestManagerCachedFetchKey(t *testing.T) { + testCases := []struct { + Name string + Options []Option + ExpectedSize int + }{ + { + Name: "Default", + ExpectedSize: 1, + }, + { + Name: "NoLookup", + Options: []Option{WithLookup(false)}, + ExpectedSize: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + r := require.New(t) + + ctx := context.Background() + kid := "202101" + + _, pubKey, err := randomKeys() + r.NoError(err) + + ts := httptest.NewServer(jwksHandler(testKey{kid, pubKey})) + defer ts.Close() + + manager, err := NewManager(ts.URL, tc.Options...) + r.NoError(err) + + key, err := manager.FetchKey(ctx, kid) + r.NoError(err) + r.Equal(kid, key.Kid) + + size, err := manager.cache.Len(ctx) + r.NoError(err) + r.Equal(tc.ExpectedSize, size) + }) + } +} diff --git a/internal/jwks/options.go b/internal/jwks/options.go new file mode 100644 index 0000000000..691fbda8de --- /dev/null +++ b/internal/jwks/options.go @@ -0,0 +1,28 @@ +package jwks + +import ( + "net/http" +) + +// Option is used for configuring key manager. +type Option func(m *Manager) + +// WithCache sets custom cache. Default is `memory cache`. +func WithCache(c Cache) Option { + return func(m *Manager) { m.cache = c } +} + +// WithHTTPClient sets custom http client. +func WithHTTPClient(c *http.Client) Option { + return func(m *Manager) { m.client = c } +} + +// WithLookup defines cache lookup option. Default is `true`. +func WithLookup(flag bool) Option { + return func(m *Manager) { m.lookup = flag } +} + +// WithMaxRetries defines max retries count if request has been failed. Default is `5`. +func WithMaxRetries(n int) Option { + return func(m *Manager) { m.retries = n } +} diff --git a/internal/jwtverify/token_verifier_jwt.go b/internal/jwtverify/token_verifier_jwt.go index a3b25d164d..37eb8c9977 100644 --- a/internal/jwtverify/token_verifier_jwt.go +++ b/internal/jwtverify/token_verifier_jwt.go @@ -1,6 +1,7 @@ package jwtverify import ( + "context" "crypto/rsa" "encoding/base64" "encoding/json" @@ -9,6 +10,7 @@ import ( "sync" "time" + "github.com/centrifugal/centrifugo/internal/jwks" "github.com/cristalhq/jwt/v3" ) @@ -16,28 +18,44 @@ type VerifierConfig struct { // HMACSecretKey is a secret key used to validate connection and subscription // tokens generated using HMAC. Zero value means that HMAC tokens won't be allowed. HMACSecretKey string + // RSAPublicKey is a public key used to validate connection and subscription // tokens generated using RSA. Zero value means that RSA tokens won't be allowed. RSAPublicKey *rsa.PublicKey + + // JWKSPublicEndpoint is a public url used to validate connection and subscription + // tokens generated using rotating RSA public keys. Zero value means that JSON Web Key Sets extension won't be used. + JWKSPublicEndpoint string } func NewTokenVerifierJWT(config VerifierConfig) *VerifierJWT { verifier := &VerifierJWT{} + algorithms, err := newAlgorithms(config.HMACSecretKey, config.RSAPublicKey) if err != nil { panic(err) } verifier.algorithms = algorithms + + if config.JWKSPublicEndpoint != "" { + mng, err := jwks.NewManager(config.JWKSPublicEndpoint) + if err == nil { + verifier.jwksManager = &jwksManager{mng} + } + } + return verifier } type VerifierJWT struct { - mu sync.RWMutex - algorithms *algorithms + mu sync.RWMutex + jwksManager *jwksManager + algorithms *algorithms } var ( ErrTokenExpired = errors.New("token expired") + errPublicKeyInvalid = errors.New("public key is invalid") errUnsupportedAlgorithm = errors.New("unsupported JWT algorithm") errDisabledAlgorithm = errors.New("disabled JWT algorithm") ) @@ -58,6 +76,35 @@ type SubscribeTokenClaims struct { jwt.StandardClaims } +type jwksManager struct{ *jwks.Manager } + +func (j *jwksManager) verify(token *jwt.Token) error { + ctx := context.Background() + kid := token.Header().KeyID + + key, err := j.Manager.FetchKey(ctx, kid) + if err != nil { + return err + } + + spec, err := key.ParseKeySpec() + if err != nil { + return err + } + + pubKey, ok := spec.Key.(*rsa.PublicKey) + if !ok { + return errPublicKeyInvalid + } + + verifier, err := jwt.NewVerifierRS(jwt.Algorithm(spec.Algorithm), pubKey) + if err != nil { + return fmt.Errorf("%w: %s", errUnsupportedAlgorithm, spec.Algorithm) + } + + return verifier.Verify(token.Payload(), token.Signature()) +} + type algorithms struct { HS256 jwt.Verifier HS384 jwt.Verifier @@ -138,23 +185,35 @@ func (s *algorithms) verify(token *jwt.Token) error { func (verifier *VerifierJWT) verifySignature(token *jwt.Token) error { verifier.mu.RLock() defer verifier.mu.RUnlock() + return verifier.algorithms.verify(token) } +func (verifier *VerifierJWT) verifySignatureByJWK(token *jwt.Token) error { + verifier.mu.RLock() + defer verifier.mu.RUnlock() + + return verifier.jwksManager.verify(token) +} + func (verifier *VerifierJWT) VerifyConnectToken(t string) (ConnectToken, error) { token, err := jwt.Parse([]byte(t)) if err != nil { return ConnectToken{}, err } - err = verifier.verifySignature(token) + if verifier.jwksManager != nil { + err = verifier.verifySignatureByJWK(token) + } else { + err = verifier.verifySignature(token) + } + if err != nil { return ConnectToken{}, err } claims := &ConnectTokenClaims{} - err = json.Unmarshal(token.RawClaims(), claims) - if err != nil { + if err := json.Unmarshal(token.RawClaims(), claims); err != nil { return ConnectToken{}, err } @@ -168,9 +227,11 @@ func (verifier *VerifierJWT) VerifyConnectToken(t string) (ConnectToken, error) Info: claims.Info, Channels: claims.Channels, } + if claims.ExpiresAt != nil { ct.ExpireAt = claims.ExpiresAt.Unix() } + if claims.Base64Info != "" { byteInfo, err := base64.StdEncoding.DecodeString(claims.Base64Info) if err != nil { @@ -178,6 +239,7 @@ func (verifier *VerifierJWT) VerifyConnectToken(t string) (ConnectToken, error) } ct.Info = byteInfo } + return ct, nil } diff --git a/internal/jwtverify/token_verifier_jwt_test.go b/internal/jwtverify/token_verifier_jwt_test.go index 1c84f985b2..765da0637c 100644 --- a/internal/jwtverify/token_verifier_jwt_test.go +++ b/internal/jwtverify/token_verifier_jwt_test.go @@ -3,7 +3,12 @@ package jwtverify import ( "crypto/rand" "crypto/rsa" + "encoding/base64" + "encoding/binary" + "encoding/json" "errors" + "net/http" + "net/http/httptest" "reflect" "testing" "time" @@ -23,6 +28,26 @@ const ( jwtStringAud = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyNjk0IiwiaW5mbyI6eyJmaXJzdF9uYW1lIjoiQWxleGFuZGVyIiwibGFzdF9uYW1lIjoiRW1lbGluIn0sImF1ZCI6ImZvbyJ9.jym6CG5haHME3ZQbb9jlnV1E0hSwwEjZycBZSygRzO0" ) +const nullByte = 0x0 + +func encodeToString(src []byte) string { + return base64.RawURLEncoding.EncodeToString(src) +} + +func encodeUint64ToString(v uint64) string { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, v) + + i := 0 + for ; i < len(data); i++ { + if data[i] != nullByte { + break + } + } + + return encodeToString(data[i:]) +} + func generateTestRSAKeys(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) { reader := rand.Reader bitSize := 2048 @@ -31,7 +56,7 @@ func generateTestRSAKeys(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) { return key, &key.PublicKey } -func getTokenBuilder(rsaPrivateKey *rsa.PrivateKey) *jwt.Builder { +func getTokenBuilder(rsaPrivateKey *rsa.PrivateKey, opts ...jwt.BuilderOption) *jwt.Builder { var signer jwt.Signer if rsaPrivateKey != nil { signer, _ = jwt.NewSignerRS(jwt.RS256, rsaPrivateKey) @@ -41,11 +66,11 @@ func getTokenBuilder(rsaPrivateKey *rsa.PrivateKey) *jwt.Builder { signer, _ = jwt.NewSignerHS(jwt.HS256, key) } - return jwt.NewBuilder(signer) + return jwt.NewBuilder(signer, opts...) } -func getConnToken(user string, exp int64, rsaPrivateKey *rsa.PrivateKey) string { - builder := getTokenBuilder(rsaPrivateKey) +func getConnToken(user string, exp int64, rsaPrivateKey *rsa.PrivateKey, opts ...jwt.BuilderOption) string { + builder := getTokenBuilder(rsaPrivateKey, opts...) claims := &ConnectTokenClaims{ Base64Info: "e30=", StandardClaims: jwt.StandardClaims{ @@ -80,6 +105,25 @@ func getSubscribeToken(channel string, client string, exp int64, rsaPrivateKey * return string(token.Raw()) } +func getJWKServer(pubKey *rsa.PublicKey, kty, use, kid string) *httptest.Server { + return httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := map[string]interface{}{ + "keys": []map[string]string{ + { + "alg": "RS256", + "kty": kty, + "use": use, + "kid": kid, + "n": encodeToString(pubKey.N.Bytes()), + "e": encodeUint64ToString(uint64(pubKey.E)), + }, + }} + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) +} + func Test_tokenVerifierJWT_Signer(t *testing.T) { _, pubKey := generateTestRSAKeys(t) signer, err := newAlgorithms("secret", pubKey) @@ -88,7 +132,7 @@ func Test_tokenVerifierJWT_Signer(t *testing.T) { } func Test_tokenVerifierJWT_Valid(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) ct, err := verifier.VerifyConnectToken(jwtValid) require.NoError(t, err) require.Equal(t, "2694", ct.UserID) @@ -97,40 +141,40 @@ func Test_tokenVerifierJWT_Valid(t *testing.T) { } func Test_tokenVerifierJWT_Expired(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) _, err := verifier.VerifyConnectToken(jwtExpired) require.Error(t, err) require.Equal(t, ErrTokenExpired, err) } func Test_tokenVerifierJWT_DisabledAlgorithm(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"", nil, ""}) _, err := verifier.VerifyConnectToken(jwtExpired) require.Error(t, err) require.True(t, errors.Is(err, errDisabledAlgorithm), err.Error()) } func Test_tokenVerifierJWT_InvalidSignature(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) _, err := verifier.VerifyConnectToken(jwtInvalidSignature) require.Error(t, err) } func Test_tokenVerifierJWT_WithNotBefore(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) _, err := verifier.VerifyConnectToken(jwtNotBefore) require.Error(t, err) } func Test_tokenVerifierJWT_StringAudience(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) ct, err := verifier.VerifyConnectToken(jwtStringAud) require.NoError(t, err) require.Equal(t, "2694", ct.UserID) } func Test_tokenVerifierJWT_ArrayAudience(t *testing.T) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) ct, err := verifier.VerifyConnectToken(jwtArrayAud) require.NoError(t, err) require.Equal(t, "2694", ct.UserID) @@ -143,7 +187,7 @@ func Test_tokenVerifierJWT_VerifyConnectToken(t *testing.T) { privateKey, pubKey := generateTestRSAKeys(t) - verifierJWT := NewTokenVerifierJWT(VerifierConfig{"secret", pubKey}) + verifierJWT := NewTokenVerifierJWT(VerifierConfig{"secret", pubKey, ""}) _time := time.Now() tests := []struct { name string @@ -216,6 +260,93 @@ func Test_tokenVerifierJWT_VerifyConnectToken(t *testing.T) { } } +func Test_tokenVerifierJWT_VerifyConnectTokenWithJWK(t *testing.T) { + type token struct { + user string + exp int64 + } + + type jwk struct { + kty string + use string + kid string + } + + now := time.Now() + + testCases := []struct { + name string + token token + jwk jwk + want ConnectToken + wantErr bool + expired bool + }{ + { + name: "OK", + token: token{user: "user1", exp: now.Add(24 * time.Hour).Unix()}, + jwk: jwk{kty: "RSA", use: "sig", kid: "ok"}, + want: ConnectToken{ + UserID: "user1", + ExpireAt: now.Add(24 * time.Hour).Unix(), + Info: []byte("{}"), + }, + wantErr: false, + }, + { + name: "ExpiredToken", + token: token{user: "user2", exp: now.Add(-24 * time.Hour).Unix()}, + jwk: jwk{kty: "RSA", use: "sig", kid: "ok"}, + want: ConnectToken{}, + wantErr: false, + expired: true, + }, + { + name: "InvalidKeyUsage", + token: token{user: "user3", exp: now.Add(24 * time.Hour).Unix()}, + jwk: jwk{kty: "RSA", use: "enc", kid: "invalidkeyusage"}, + want: ConnectToken{}, + wantErr: true, + }, + { + name: "InvalidKeyType", + token: token{user: "user4", exp: now.Add(24 * time.Hour).Unix()}, + jwk: jwk{kty: "HS", use: "sig", kid: "invalidkeytype"}, + want: ConnectToken{}, + wantErr: true, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + privKey, pubKey := generateTestRSAKeys(t) + ts := getJWKServer(pubKey, tt.jwk.kty, tt.jwk.use, tt.jwk.kid) + + ts.Start() + defer ts.Close() + + verifier := NewTokenVerifierJWT(VerifierConfig{"", nil, ts.URL}) + token := getConnToken(tt.token.user, tt.token.exp, privKey, jwt.WithKeyID(tt.jwk.kid)) + + got, err := verifier.VerifyConnectToken(token) + if tt.wantErr { + r.Error(err) + return + } + + if tt.expired { + r.EqualError(err, ErrTokenExpired.Error()) + return + } + + r.NoError(err) + r.EqualValues(got, tt.want) + }) + } +} + func Test_tokenVerifierJWT_VerifySubscribeToken(t *testing.T) { type args struct { token string @@ -223,7 +354,7 @@ func Test_tokenVerifierJWT_VerifySubscribeToken(t *testing.T) { privateKey, pubKey := generateTestRSAKeys(t) - verifierJWT := NewTokenVerifierJWT(VerifierConfig{"secret", pubKey}) + verifierJWT := NewTokenVerifierJWT(VerifierConfig{"secret", pubKey, ""}) _time := time.Now() tests := []struct { name string @@ -306,7 +437,7 @@ func Test_tokenVerifierJWT_VerifySubscribeToken(t *testing.T) { } func BenchmarkConnectTokenVerify_Valid(b *testing.B) { - verifierJWT := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifierJWT := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) b.ResetTimer() for i := 0; i < b.N; i++ { _, err := verifierJWT.VerifyConnectToken(jwtValid) @@ -319,7 +450,7 @@ func BenchmarkConnectTokenVerify_Valid(b *testing.B) { } func BenchmarkConnectTokenVerify_Expired(b *testing.B) { - verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil}) + verifier := NewTokenVerifierJWT(VerifierConfig{"secret", nil, ""}) for i := 0; i < b.N; i++ { _, err := verifier.VerifyConnectToken(jwtExpired) if err != ErrTokenExpired { diff --git a/main.go b/main.go index 5d23b6306f..33cd918933 100644 --- a/main.go +++ b/main.go @@ -61,6 +61,7 @@ var configDefaults = map[string]interface{}{ "name": "", "secret": "", "token_hmac_secret_key": "", + "token_jwks_public_endpoint": "", "token_rsa_public_key": "", "server_side": false, "publish": false, @@ -208,7 +209,7 @@ func main() { "grpc_api_tls_cert", "grpc_api_tls_key", "proxy_connect_endpoint", "proxy_connect_timeout", "proxy_rpc_endpoint", "proxy_rpc_timeout", "proxy_refresh_endpoint", "proxy_refresh_timeout", - "token_rsa_public_key", "token_hmac_secret_key", "redis_sequence_ttl", + "token_jwks_public_endpoint", "token_rsa_public_key", "token_hmac_secret_key", "redis_sequence_ttl", "proxy_extra_http_headers", "server_side", "user_subscribe_to_personal", "user_personal_channel_namespace", "websocket_use_write_buffer_pool", "websocket_disable", "sockjs_disable", "api_disable", "redis_cluster_addrs", @@ -1060,6 +1061,8 @@ func jwtVerifierConfig() jwtverify.VerifierConfig { cfg.RSAPublicKey = pubKey } + cfg.JWKSPublicEndpoint = v.GetString("token_jwks_public_endpoint") + return cfg }