From 5d25da1a8d43b66f2898c444f899c7bcfd6a407e Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Wed, 29 Jul 2020 07:17:39 -0700 Subject: [PATCH 01/20] go.mod: update stale dependencies Invokes go get -u -v ./... to try to get rid of stale dependencies cloud.google.com/go@v0.34.0 (released in December 2018) and that dependency unfortunately imported a stale version of go.opencensus.io that then transitively imports another that has a vulnerability. Change-Id: I0a520e8f979ff3ddcd6197b234cdc0f4154bb735 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/245517 Run-TryBot: Emmanuel Odeke TryBot-Result: Gobot Gobot Reviewed-by: Bryan C. Mills --- go.mod | 7 +- go.sum | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 357 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index b345781..2b13f0b 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module golang.org/x/oauth2 go 1.11 require ( - cloud.google.com/go v0.34.0 - golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect - google.golang.org/appengine v1.4.0 + cloud.google.com/go v0.65.0 + golang.org/x/net v0.0.0-20200822124328-c89045814202 + google.golang.org/appengine v1.6.6 ) diff --git a/go.sum b/go.sum index 6f0079b..eab5833 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,361 @@ -cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 9fd604954f58d391cab0f7f49affb0a6aa864086 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Tue, 3 Nov 2020 00:39:58 +0000 Subject: [PATCH 02/20] google: add utilities supporting upcoming oauth2 functionality These are used to support some extended utilities to help with STS requests. Change-Id: Iafc145b06ca42374cfc2ac6572762a50bcf560f2 GitHub-Last-Rev: 3085fe570382318e6690304640751bf312e1a0b8 GitHub-Pull-Request: golang/oauth2#439 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/259777 Trust: Cody Oss Run-TryBot: Cody Oss TryBot-Result: Go Bot Reviewed-by: Tyler Bui-Palsulich --- google/internal/externalaccount/clientauth.go | 41 +++++++ .../externalaccount/clientauth_test.go | 113 ++++++++++++++++++ google/internal/externalaccount/err.go | 18 +++ google/internal/externalaccount/err_test.go | 19 +++ 4 files changed, 191 insertions(+) create mode 100644 google/internal/externalaccount/clientauth.go create mode 100644 google/internal/externalaccount/clientauth_test.go create mode 100644 google/internal/externalaccount/err.go create mode 100644 google/internal/externalaccount/err_test.go diff --git a/google/internal/externalaccount/clientauth.go b/google/internal/externalaccount/clientauth.go new file mode 100644 index 0000000..0464724 --- /dev/null +++ b/google/internal/externalaccount/clientauth.go @@ -0,0 +1,41 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "encoding/base64" + "golang.org/x/oauth2" + "net/http" + "net/url" +) + +// ClientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1. +type ClientAuthentication struct { + // AuthStyle can be either basic or request-body + AuthStyle oauth2.AuthStyle + ClientID string + ClientSecret string +} + +func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) { + if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil { + return + } + + switch c.AuthStyle { + case oauth2.AuthStyleInHeader: // AuthStyleInHeader corresponds to basic authentication as defined in rfc7617#2 + plainHeader := c.ClientID + ":" + c.ClientSecret + headers.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader))) + case oauth2.AuthStyleInParams: // AuthStyleInParams corresponds to request-body authentication with ClientID and ClientSecret in the message body. + values.Set("client_id", c.ClientID) + values.Set("client_secret", c.ClientSecret) + case oauth2.AuthStyleAutoDetect: + values.Set("client_id", c.ClientID) + values.Set("client_secret", c.ClientSecret) + default: + values.Set("client_id", c.ClientID) + values.Set("client_secret", c.ClientSecret) + } +} diff --git a/google/internal/externalaccount/clientauth_test.go b/google/internal/externalaccount/clientauth_test.go new file mode 100644 index 0000000..acacbeb --- /dev/null +++ b/google/internal/externalaccount/clientauth_test.go @@ -0,0 +1,113 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "golang.org/x/oauth2" + "net/http" + "net/url" + "reflect" + "testing" +) + +var clientID = "rbrgnognrhongo3bi4gb9ghg9g" +var clientSecret = "notsosecret" + +var audience = []string{"32555940559.apps.googleusercontent.com"} +var grantType = []string{"urn:ietf:params:oauth:grant-type:token-exchange"} +var requestedTokenType = []string{"urn:ietf:params:oauth:token-type:access_token"} +var subjectTokenType = []string{"urn:ietf:params:oauth:token-type:jwt"} +var subjectToken = []string{"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJjNmZhNmY1OTUwYTdjZTQ2NWZjZjI0N2FhMGIwOTQ4MjhhYzk1MmMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIzMjU1NTk0MDU1OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF1ZCI6IjMyNTU1OTQwNTU5LmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEzMzE4NTQxMDA5MDU3Mzc4MzI4IiwiaGQiOiJnb29nbGUuY29tIiwiZW1haWwiOiJpdGh1cmllbEBnb29nbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiI5OVJVYVFrRHJsVDFZOUV0SzdiYXJnIiwiaWF0IjoxNjAxNTgxMzQ5LCJleHAiOjE2MDE1ODQ5NDl9.SZ-4DyDcogDh_CDUKHqPCiT8AKLg4zLMpPhGQzmcmHQ6cJiV0WRVMf5Lq911qsvuekgxfQpIdKNXlD6yk3FqvC2rjBbuEztMF-OD_2B8CEIYFlMLGuTQimJlUQksLKM-3B2ITRDCxnyEdaZik0OVssiy1CBTsllS5MgTFqic7w8w0Cd6diqNkfPFZRWyRYsrRDRlHHbH5_TUnv2wnLVHBHlNvU4wU2yyjDIoqOvTRp8jtXdq7K31CDhXd47-hXsVFQn2ZgzuUEAkH2Q6NIXACcVyZOrjBcZiOQI9IRWz-g03LzbzPSecO7I8dDrhqUSqMrdNUz_f8Kr8JFhuVMfVug"} +var scope = []string{"https://www.googleapis.com/auth/devstorage.full_control"} + +var ContentType = []string{"application/x-www-form-urlencoded"} + +func TestClientAuthentication_InjectHeaderAuthentication(t *testing.T) { + valuesH := url.Values{ + "audience": audience, + "grant_type": grantType, + "requested_token_type": requestedTokenType, + "subject_token_type": subjectTokenType, + "subject_token": subjectToken, + "scope": scope, + } + headerH := http.Header{ + "Content-Type": ContentType, + } + + headerAuthentication := ClientAuthentication{ + AuthStyle: oauth2.AuthStyleInHeader, + ClientID: clientID, + ClientSecret: clientSecret, + } + headerAuthentication.InjectAuthentication(valuesH, headerH) + + if got, want := valuesH["audience"], audience; !reflect.DeepEqual(got, want) { + t.Errorf("audience = %q, want %q", got, want) + } + if got, want := valuesH["grant_type"], grantType; !reflect.DeepEqual(got, want) { + t.Errorf("grant_type = %q, want %q", got, want) + } + if got, want := valuesH["requested_token_type"], requestedTokenType; !reflect.DeepEqual(got, want) { + t.Errorf("requested_token_type = %q, want %q", got, want) + } + if got, want := valuesH["subject_token_type"], subjectTokenType; !reflect.DeepEqual(got, want) { + t.Errorf("subject_token_type = %q, want %q", got, want) + } + if got, want := valuesH["subject_token"], subjectToken; !reflect.DeepEqual(got, want) { + t.Errorf("subject_token = %q, want %q", got, want) + } + if got, want := valuesH["scope"], scope; !reflect.DeepEqual(got, want) { + t.Errorf("scope = %q, want %q", got, want) + } + if got, want := headerH["Authorization"], []string{"Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="}; !reflect.DeepEqual(got, want) { + t.Errorf("Authorization in header = %q, want %q", got, want) + } +} + +func TestClientAuthentication_ParamsAuthentication(t *testing.T) { + valuesP := url.Values{ + "audience": audience, + "grant_type": grantType, + "requested_token_type": requestedTokenType, + "subject_token_type": subjectTokenType, + "subject_token": subjectToken, + "scope": scope, + } + headerP := http.Header{ + "Content-Type": ContentType, + } + paramsAuthentication := ClientAuthentication{ + AuthStyle: oauth2.AuthStyleInParams, + ClientID: clientID, + ClientSecret: clientSecret, + } + paramsAuthentication.InjectAuthentication(valuesP, headerP) + + if got, want := valuesP["audience"], audience; !reflect.DeepEqual(got, want) { + t.Errorf("audience = %q, want %q", got, want) + } + if got, want := valuesP["grant_type"], grantType; !reflect.DeepEqual(got, want) { + t.Errorf("grant_type = %q, want %q", got, want) + } + if got, want := valuesP["requested_token_type"], requestedTokenType; !reflect.DeepEqual(got, want) { + t.Errorf("requested_token_type = %q, want %q", got, want) + } + if got, want := valuesP["subject_token_type"], subjectTokenType; !reflect.DeepEqual(got, want) { + t.Errorf("subject_token_type = %q, want %q", got, want) + } + if got, want := valuesP["subject_token"], subjectToken; !reflect.DeepEqual(got, want) { + t.Errorf("subject_token = %q, want %q", got, want) + } + if got, want := valuesP["scope"], scope; !reflect.DeepEqual(got, want) { + t.Errorf("scope = %q, want %q", got, want) + } + if got, want := valuesP["client_id"], []string{clientID}; !reflect.DeepEqual(got, want) { + t.Errorf("client_id = %q, want %q", got, want) + } + if got, want := valuesP["client_secret"], []string{clientSecret}; !reflect.DeepEqual(got, want) { + t.Errorf("client_secret = %q, want %q", got, want) + } +} diff --git a/google/internal/externalaccount/err.go b/google/internal/externalaccount/err.go new file mode 100644 index 0000000..233a78c --- /dev/null +++ b/google/internal/externalaccount/err.go @@ -0,0 +1,18 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import "fmt" + +// Error for handling OAuth related error responses as stated in rfc6749#5.2. +type Error struct { + Code string + URI string + Description string +} + +func (err *Error) Error() string { + return fmt.Sprintf("got error code %s from %s: %s", err.Code, err.URI, err.Description) +} diff --git a/google/internal/externalaccount/err_test.go b/google/internal/externalaccount/err_test.go new file mode 100644 index 0000000..687380d --- /dev/null +++ b/google/internal/externalaccount/err_test.go @@ -0,0 +1,19 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import "testing" + +func TestError(t *testing.T) { + e := Error{ + "42", + "http:thisIsAPlaceholder", + "The Answer!", + } + want := "got error code 42 from http:thisIsAPlaceholder: The Answer!" + if got := e.Error(); got != want { + t.Errorf("Got error message %q; want %q", got, want) + } +} From 0b49973bad195855a6a62fc57db590cc1bfe0771 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 2 Dec 2020 23:43:55 +0000 Subject: [PATCH 03/20] google: add ExchangeToken() to run STS exchanges. Adds the ExchangeToken() function and support structs, but depends on https://github.com/golang/oauth2/pull/439 Change-Id: Id738a27b0c2ac083409156af1f60283b9140b159 GitHub-Last-Rev: 1aa066dc21e11a278f2698f982b7e7c857114d0a GitHub-Pull-Request: golang/oauth2#444 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/261918 Run-TryBot: Tyler Bui-Palsulich TryBot-Result: Go Bot Trust: Tyler Bui-Palsulich Trust: Cody Oss Reviewed-by: Tyler Bui-Palsulich --- .../internal/externalaccount/sts_exchange.go | 96 +++++++++ .../externalaccount/sts_exchange_test.go | 183 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 google/internal/externalaccount/sts_exchange.go create mode 100644 google/internal/externalaccount/sts_exchange_test.go diff --git a/google/internal/externalaccount/sts_exchange.go b/google/internal/externalaccount/sts_exchange.go new file mode 100644 index 0000000..d7f54e0 --- /dev/null +++ b/google/internal/externalaccount/sts_exchange.go @@ -0,0 +1,96 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +// ExchangeToken performs an oauth2 token exchange with the provided endpoint. +// The first 4 fields are all mandatory. headers can be used to pass additional +// headers beyond the bare minimum required by the token exchange. options can +// be used to pass additional JSON-structured options to the remote server. +func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]interface{}) (*STSTokenExchangeResponse, error) { + + client := oauth2.NewClient(ctx, nil) + + data := url.Values{} + data.Set("audience", request.Audience) + data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + data.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") + data.Set("subject_token_type", request.SubjectTokenType) + data.Set("subject_token", request.SubjectToken) + data.Set("scope", strings.Join(request.Scope, " ")) + opts, err := json.Marshal(options) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err) + } + data.Set("options", string(opts)) + + authentication.InjectAuthentication(data, headers) + encodedData := data.Encode() + + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(encodedData)) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err) + + } + for key, list := range headers { + for _, val := range list { + req.Header.Add(key, val) + } + } + req.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) + + resp, err := client.Do(req) + + if err != nil { + return nil, fmt.Errorf("oauth2/google: invalid response from Secure Token Server: %v", err) + } + defer resp.Body.Close() + + bodyJson := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)) + var stsResp STSTokenExchangeResponse + err = bodyJson.Decode(&stsResp) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err) + + } + + return &stsResp, nil +} + +// STSTokenExchangeRequest contains fields necessary to make an oauth2 token exchange. +type STSTokenExchangeRequest struct { + ActingParty struct { + ActorToken string + ActorTokenType string + } + GrantType string + Resource string + Audience string + Scope []string + RequestedTokenType string + SubjectToken string + SubjectTokenType string +} + +// STSTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange. +type STSTokenExchangeResponse struct { + AccessToken string `json:"access_token"` + IssuedTokenType string `json:"issued_token_type"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + RefreshToken string `json:"refresh_token"` +} diff --git a/google/internal/externalaccount/sts_exchange_test.go b/google/internal/externalaccount/sts_exchange_test.go new file mode 100644 index 0000000..bd4034a --- /dev/null +++ b/google/internal/externalaccount/sts_exchange_test.go @@ -0,0 +1,183 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "golang.org/x/oauth2" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +var auth = ClientAuthentication{ + AuthStyle: oauth2.AuthStyleInHeader, + ClientID: clientID, + ClientSecret: clientSecret, +} + +var tokenRequest = STSTokenExchangeRequest{ + ActingParty: struct { + ActorToken string + ActorTokenType string + }{}, + GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", + Resource: "", + Audience: "32555940559.apps.googleusercontent.com", //TODO: Make sure audience is correct in this test (might be mismatched) + Scope: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token", + SubjectToken: "Sample.Subject.Token", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", +} + +var requestbody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" +var responseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}` +var expectedToken = STSTokenExchangeResponse{ + AccessToken: "Sample.Access.Token", + IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", + TokenType: "Bearer", + ExpiresIn: 3600, + Scope: "https://www.googleapis.com/auth/cloud-platform", + RefreshToken: "", +} + +func TestExchangeToken(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("Unexpected request method, %v is found", r.Method) + } + if r.URL.String() != "/" { + t.Errorf("Unexpected request URL, %v is found", r.URL) + } + if got, want := r.Header.Get("Authorization"), "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want { + t.Errorf("Unexpected authorization header, got %v, want %v", got, want) + } + if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { + t.Errorf("Unexpected Content-Type header, got %v, want %v", got, want) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Failed reading request body: %v.", err) + } + if got, want := string(body), requestbody; got != want { + t.Errorf("Unexpected exchange payload, got %v but want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(responseBody)) + })) + defer ts.Close() + + headers := http.Header{} + headers.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil) + if err != nil { + t.Fatalf("ExchangeToken failed with error: %v", err) + } + + if expectedToken != *resp { + t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedToken, *resp) + } + +} + +func TestExchangeToken_Err(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("what's wrong with this response?")) + })) + defer ts.Close() + + headers := http.Header{} + headers.Add("Content-Type", "application/x-www-form-urlencoded") + _, err := ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil) + if err == nil { + t.Errorf("Expected handled error; instead got nil.") + } +} + +/* Lean test specifically for options, as the other features are tested earlier. */ +type testOpts struct { + First string `json:"first"` + Second string `json:"second"` +} + +var optsValues = [][]string{{"foo", "bar"}, {"cat", "pan"}} + +func TestExchangeToken_Opts(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed reading request body: %v.", err) + } + data, err := url.ParseQuery(string(body)) + if err != nil { + t.Fatalf("Failed to parse request body: %v", err) + } + strOpts, ok := data["options"] + if !ok { + t.Errorf("Server didn't recieve an \"options\" field.") + } else if len(strOpts) < 1 { + t.Errorf("\"options\" field has length 0.") + } + var opts map[string]interface{} + err = json.Unmarshal([]byte(strOpts[0]), &opts) + if len(opts) < 2 { + t.Errorf("Too few options received.") + } + + val, ok := opts["one"] + if !ok { + t.Errorf("Couldn't find first option parameter.") + } else { + tOpts1, ok := val.(map[string]interface{}) + if !ok { + t.Errorf("Failed to assert the first option parameter as type testOpts.") + } else { + if got, want := tOpts1["first"].(string), optsValues[0][0]; got != want { + t.Errorf("First value in first options field is incorrect; got %v but want %v", got, want) + } + if got, want := tOpts1["second"].(string), optsValues[0][1]; got != want { + t.Errorf("Second value in first options field is incorrect; got %v but want %v", got, want) + } + } + } + + val2, ok := opts["two"] + if !ok { + t.Errorf("Couldn't find second option parameter.") + } else { + tOpts2, ok := val2.(map[string]interface{}) + if !ok { + t.Errorf("Failed to assert the second option parameter as type testOpts.") + } else { + if got, want := tOpts2["first"].(string), optsValues[1][0]; got != want { + t.Errorf("First value in second options field is incorrect; got %v but want %v", got, want) + } + if got, want := tOpts2["second"].(string), optsValues[1][1]; got != want { + t.Errorf("Second value in second options field is incorrect; got %v but want %v", got, want) + } + } + } + + // Send a proper reply so that no other errors crop up. + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(responseBody)) + + })) + defer ts.Close() + headers := http.Header{} + headers.Add("Content-Type", "application/x-www-form-urlencoded") + + firstOption := testOpts{optsValues[0][0], optsValues[0][1]} + secondOption := testOpts{optsValues[1][0], optsValues[1][1]} + inputOpts := make(map[string]interface{}) + inputOpts["one"] = firstOption + inputOpts["two"] = secondOption + ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, inputOpts) +} From 931764155e3f087a554e325f3e6a3adc3fdf4a03 Mon Sep 17 00:00:00 2001 From: Julie Qiu Date: Thu, 3 Dec 2020 18:41:36 -0500 Subject: [PATCH 04/20] README.md: add badge to pkg.go.dev Change-Id: I90a3334507f4501ee082afeb878b82f71b3392ae Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/275303 Trust: Julie Qiu Reviewed-by: Andrew Gerrand --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cfd606..58f4246 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OAuth2 for Go [![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) -[![GoDoc](https://godoc.org/golang.org/x/oauth2?status.svg)](https://godoc.org/golang.org/x/oauth2) +[![Go Package Documentation](https://pkg.go.dev/badge/golang.org/x/oauth2.svg)](https://pkg.go.dev/golang.org/x/oauth2) oauth2 package contains a client implementation for OAuth 2.0 spec. From 08078c50e5b5244ec123a6b69facdbc13b21a548 Mon Sep 17 00:00:00 2001 From: Julie Qiu Date: Mon, 7 Dec 2020 22:44:01 -0500 Subject: [PATCH 05/20] README.md: change godoc.org links to pkg.go.dev Links to godoc.org are changed to pkg.go.dev. The README badge alt text is changed to "Go Reference" to match the updated alt text generated by pkg.go.dev/badge. Change-Id: I935cbe03477131a4361a8ac7b5ba9fd3e378cbdd Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/276016 Reviewed-by: Dmitri Shuralyov Reviewed-by: Andrew Gerrand Trust: Julie Qiu --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 58f4246..1473e12 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OAuth2 for Go +[![Go Reference](https://pkg.go.dev/badge/golang.org/x/oauth2.svg)](https://pkg.go.dev/golang.org/x/oauth2) [![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) -[![Go Package Documentation](https://pkg.go.dev/badge/golang.org/x/oauth2.svg)](https://pkg.go.dev/golang.org/x/oauth2) oauth2 package contains a client implementation for OAuth 2.0 spec. @@ -14,17 +14,17 @@ go get golang.org/x/oauth2 Or you can manually git clone the repository to `$(go env GOPATH)/src/golang.org/x/oauth2`. -See godoc for further documentation and examples. +See pkg.go.dev for further documentation and examples. -* [godoc.org/golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2) -* [godoc.org/golang.org/x/oauth2/google](https://godoc.org/golang.org/x/oauth2/google) +* [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) +* [pkg.go.dev/golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google) ## Policy for new packages We no longer accept new provider-specific packages in this repo if all they do is add a single endpoint variable. If you just want to add a single endpoint, add it to the -[godoc.org/golang.org/x/oauth2/endpoints](https://godoc.org/golang.org/x/oauth2/endpoints) +[pkg.go.dev/golang.org/x/oauth2/endpoints](https://pkg.go.dev/golang.org/x/oauth2/endpoints) package. ## Report Issues / Send Patches From 01de73cf58bdca33ccc181d1bd6d63ebcf21ccca Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Tue, 12 Jan 2021 19:45:47 +0000 Subject: [PATCH 06/20] google: base account credentials with file-sourcing Implements the core functionality to allow 3rd party identities access to Google APIs. Specifically, this PR implements the base account credential type and supports file-sourced credentials such as Kubernetes workloads. Later updates will add support for URL-sourced credentials such as Microsoft Azure and support for AWS credentials. Change-Id: I6e09a450f5221a1e06394b51374cff70ab3ab8a7 GitHub-Last-Rev: 3ab51622f8f7c6982a5e78ae9644675659318e7b GitHub-Pull-Request: golang/oauth2#462 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/276312 Reviewed-by: Tyler Bui-Palsulich Trust: Tyler Bui-Palsulich Trust: Cody Oss Run-TryBot: Tyler Bui-Palsulich TryBot-Result: Go Bot --- google/google.go | 26 ++++ .../externalaccount/basecredentials.go | 133 ++++++++++++++++++ .../externalaccount/basecredentials_test.go | 93 ++++++++++++ .../externalaccount/filecredsource.go | 57 ++++++++ .../externalaccount/filecredsource_test.go | 67 +++++++++ .../externalaccount/testdata/3pi_cred.json | 3 + .../externalaccount/testdata/3pi_cred.txt | 1 + 7 files changed, 380 insertions(+) create mode 100644 google/internal/externalaccount/basecredentials.go create mode 100644 google/internal/externalaccount/basecredentials_test.go create mode 100644 google/internal/externalaccount/filecredsource.go create mode 100644 google/internal/externalaccount/filecredsource_test.go create mode 100644 google/internal/externalaccount/testdata/3pi_cred.json create mode 100644 google/internal/externalaccount/testdata/3pi_cred.txt diff --git a/google/google.go b/google/google.go index 81de32b..e247491 100644 --- a/google/google.go +++ b/google/google.go @@ -15,6 +15,7 @@ import ( "cloud.google.com/go/compute/metadata" "golang.org/x/oauth2" + "golang.org/x/oauth2/google/internal/externalaccount" "golang.org/x/oauth2/jwt" ) @@ -93,6 +94,7 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { const ( serviceAccountKey = "service_account" userCredentialsKey = "authorized_user" + externalAccountKey = "external_account" ) // credentialsFile is the unmarshalled representation of a credentials file. @@ -111,6 +113,16 @@ type credentialsFile struct { ClientSecret string `json:"client_secret"` ClientID string `json:"client_id"` RefreshToken string `json:"refresh_token"` + + // External Account fields + Audience string `json:"audience"` + SubjectTokenType string `json:"subject_token_type"` + TokenURLExternal string `json:"token_url"` + TokenInfoURL string `json:"token_info_url"` + ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` + CredentialSource externalaccount.CredentialSource `json:"credential_source"` + QuotaProjectID string `json:"quota_project_id"` + } func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { @@ -141,6 +153,20 @@ func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oau } tok := &oauth2.Token{RefreshToken: f.RefreshToken} return cfg.TokenSource(ctx, tok), nil + case externalAccountKey: + cfg := &externalaccount.Config{ + Audience: f.Audience, + SubjectTokenType: f.SubjectTokenType, + TokenURL: f.TokenURLExternal, + TokenInfoURL: f.TokenInfoURL, + ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, + ClientSecret: f.ClientSecret, + ClientID: f.ClientID, + CredentialSource: f.CredentialSource, + QuotaProjectID: f.QuotaProjectID, + Scopes: scopes, + } + return cfg.TokenSource(ctx), nil case "": return nil, errors.New("missing 'type' field in credentials") default: diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go new file mode 100644 index 0000000..3291d46 --- /dev/null +++ b/google/internal/externalaccount/basecredentials.go @@ -0,0 +1,133 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "fmt" + "golang.org/x/oauth2" + "net/http" + "time" +) + +// now aliases time.Now for testing +var now = time.Now + +// Config stores the configuration for fetching tokens with external credentials. +type Config struct { + Audience string + SubjectTokenType string + TokenURL string + TokenInfoURL string + ServiceAccountImpersonationURL string + ClientSecret string + ClientID string + CredentialSource CredentialSource + QuotaProjectID string + Scopes []string +} + +// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + ts := tokenSource{ + ctx: ctx, + conf: c, + } + return oauth2.ReuseTokenSource(nil, ts) +} + +// Subject token file types. +const ( + fileTypeText = "text" + fileTypeJSON = "json" +) + +type format struct { + // Type is either "text" or "json". When not provided "text" type is assumed. + Type string `json:"type"` + // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure. + SubjectTokenFieldName string `json:"subject_token_field_name"` +} + +// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. +type CredentialSource struct { + File string `json:"file"` + + URL string `json:"url"` + Headers map[string]string `json:"headers"` + + EnvironmentID string `json:"environment_id"` + RegionURL string `json:"region_url"` + RegionalCredVerificationURL string `json:"regional_cred_verification_url"` + CredVerificationURL string `json:"cred_verification_url"` + Format format `json:"format"` +} + +// parse determines the type of CredentialSource needed +func (c *Config) parse() baseCredentialSource { + if c.CredentialSource.File != "" { + return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format} + } + return nil +} + +type baseCredentialSource interface { + subjectToken() (string, error) +} + +// tokenSource is the source that handles external credentials. +type tokenSource struct { + ctx context.Context + conf *Config +} + +// Token allows tokenSource to conform to the oauth2.TokenSource interface. +func (ts tokenSource) Token() (*oauth2.Token, error) { + conf := ts.conf + + credSource := conf.parse() + if credSource == nil { + return nil, fmt.Errorf("oauth2/google: unable to parse credential source") + } + subjectToken, err := credSource.subjectToken() + if err != nil { + return nil, err + } + stsRequest := STSTokenExchangeRequest{ + GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", + Audience: conf.Audience, + Scope: conf.Scopes, + RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token", + SubjectToken: subjectToken, + SubjectTokenType: conf.SubjectTokenType, + } + header := make(http.Header) + header.Add("Content-Type", "application/x-www-form-urlencoded") + clientAuth := ClientAuthentication{ + AuthStyle: oauth2.AuthStyleInHeader, + ClientID: conf.ClientID, + ClientSecret: conf.ClientSecret, + } + stsResp, err := ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil) + if err != nil { + return nil, err + } + + accessToken := &oauth2.Token{ + AccessToken: stsResp.AccessToken, + TokenType: stsResp.TokenType, + } + if stsResp.ExpiresIn < 0 { + return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service") + } else if stsResp.ExpiresIn >= 0 { + accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second) + } + + if stsResp.RefreshToken != "" { + accessToken.RefreshToken = stsResp.RefreshToken + } + + return accessToken, nil +} diff --git a/google/internal/externalaccount/basecredentials_test.go b/google/internal/externalaccount/basecredentials_test.go new file mode 100644 index 0000000..7ec12e4 --- /dev/null +++ b/google/internal/externalaccount/basecredentials_test.go @@ -0,0 +1,93 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +var testBaseCredSource = CredentialSource{ + File: "./testdata/3pi_cred.txt", + Format: format{Type: fileTypeText}, +} + +var testConfig = Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, +} + +var ( + baseCredsRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" + baseCredsResponseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}` + correctAT = "Sample.Access.Token" + expiry int64 = 234852 +) +var ( + testNow = func() time.Time { return time.Unix(expiry, 0) } +) + +func TestToken(t *testing.T) { + + targetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.URL.String(), "/"; got != want { + t.Errorf("URL.String(): got %v but want %v", got, want) + } + headerAuth := r.Header.Get("Authorization") + if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want { + t.Errorf("got %v but want %v", got, want) + } + headerContentType := r.Header.Get("Content-Type") + if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { + t.Errorf("got %v but want %v", got, want) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Failed reading request body: %s.", err) + } + if got, want := string(body), baseCredsRequestBody; got != want { + t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(baseCredsResponseBody)) + })) + defer targetServer.Close() + + testConfig.TokenURL = targetServer.URL + ourTS := tokenSource{ + ctx: context.Background(), + conf: &testConfig, + } + + oldNow := now + defer func() { now = oldNow }() + now = testNow + + tok, err := ourTS.Token() + if err != nil { + t.Fatalf("Unexpected error: %e", err) + } + if got, want := tok.AccessToken, correctAT; got != want { + t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) + } + if got, want := tok.TokenType, "Bearer"; got != want { + t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) + } + + if got, want := tok.Expiry, now().Add(time.Duration(3600)*time.Second); got != want { + t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want) + } + +} diff --git a/google/internal/externalaccount/filecredsource.go b/google/internal/externalaccount/filecredsource.go new file mode 100644 index 0000000..e953ddb --- /dev/null +++ b/google/internal/externalaccount/filecredsource.go @@ -0,0 +1,57 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "os" +) + +type fileCredentialSource struct { + File string + Format format +} + +func (cs fileCredentialSource) subjectToken() (string, error) { + tokenFile, err := os.Open(cs.File) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to open credential file %q", cs.File) + } + defer tokenFile.Close() + tokenBytes, err := ioutil.ReadAll(io.LimitReader(tokenFile, 1<<20)) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to read credential file: %v", err) + } + tokenBytes = bytes.TrimSpace(tokenBytes) + switch cs.Format.Type { + case "json": + jsonData := make(map[string]interface{}) + err = json.Unmarshal(tokenBytes, &jsonData) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + } + val, ok := jsonData[cs.Format.SubjectTokenFieldName] + if !ok { + return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + } + token, ok := val.(string) + if !ok { + return "", errors.New("oauth2/google: improperly formatted subject token") + } + return token, nil + case "text": + return string(tokenBytes), nil + case "": + return string(tokenBytes), nil + default: + return "", errors.New("oauth2/google: invalid credential_source file format type") + } + +} diff --git a/google/internal/externalaccount/filecredsource_test.go b/google/internal/externalaccount/filecredsource_test.go new file mode 100644 index 0000000..0bc8048 --- /dev/null +++ b/google/internal/externalaccount/filecredsource_test.go @@ -0,0 +1,67 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "testing" +) + +var testFileConfig = Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenURL: "http://localhost:8080/v1/token", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", +} + +func TestRetrieveFileSubjectToken(t *testing.T) { + var fileSourceTests = []struct { + name string + cs CredentialSource + want string + }{ + { + name: "UntypedFileSource", + cs: CredentialSource{ + File: "./testdata/3pi_cred.txt", + }, + want: "street123", + }, + { + name: "TextFileSource", + cs: CredentialSource{ + File: "./testdata/3pi_cred.txt", + Format: format{Type: fileTypeText}, + }, + want: "street123", + }, + { + name: "JSONFileSource", + cs: CredentialSource{ + File: "./testdata/3pi_cred.json", + Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, + }, + want: "321road", + }, + } + + for _, test := range fileSourceTests { + test := test + tfc := testFileConfig + tfc.CredentialSource = test.cs + + t.Run(test.name, func(t *testing.T) { + out, err := tfc.parse().subjectToken() + if err != nil { + t.Errorf("Method subjectToken() errored.") + } else if test.want != out { + t.Errorf("got %v but want %v", out, test.want) + } + + }) + } +} diff --git a/google/internal/externalaccount/testdata/3pi_cred.json b/google/internal/externalaccount/testdata/3pi_cred.json new file mode 100644 index 0000000..6a9cf7d --- /dev/null +++ b/google/internal/externalaccount/testdata/3pi_cred.json @@ -0,0 +1,3 @@ +{ + "SubjToken": "321road" +} diff --git a/google/internal/externalaccount/testdata/3pi_cred.txt b/google/internal/externalaccount/testdata/3pi_cred.txt new file mode 100644 index 0000000..4e511cc --- /dev/null +++ b/google/internal/externalaccount/testdata/3pi_cred.txt @@ -0,0 +1 @@ +street123 From 8b1d76fa042330bf6fcfbda4a3da5f69d1b64f5f Mon Sep 17 00:00:00 2001 From: Cody Oss Date: Wed, 13 Jan 2021 08:23:33 -0700 Subject: [PATCH 07/20] google: restore 1.11 compatibility NewRequestWithContext requires 1.13. As this is just a convenience we should try to retatin the 1.11 compatibility by using NewRequest then calling WithContext instead. Change-Id: I6208a92061b208a119fdf04fd561a3e4d22bc547 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/283535 Reviewed-by: Tyler Bui-Palsulich Trust: Tyler Bui-Palsulich Trust: Cody Oss Run-TryBot: Tyler Bui-Palsulich TryBot-Result: Go Bot --- google/internal/externalaccount/sts_exchange.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/google/internal/externalaccount/sts_exchange.go b/google/internal/externalaccount/sts_exchange.go index d7f54e0..c7d85a3 100644 --- a/google/internal/externalaccount/sts_exchange.go +++ b/google/internal/externalaccount/sts_exchange.go @@ -8,12 +8,13 @@ import ( "context" "encoding/json" "fmt" - "golang.org/x/oauth2" "io" "net/http" "net/url" "strconv" "strings" + + "golang.org/x/oauth2" ) // ExchangeToken performs an oauth2 token exchange with the provided endpoint. @@ -40,11 +41,12 @@ func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchan authentication.InjectAuthentication(data, headers) encodedData := data.Encode() - req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(encodedData)) + req, err := http.NewRequest("POST", endpoint, strings.NewReader(encodedData)) if err != nil { return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err) } + req = req.WithContext(ctx) for key, list := range headers { for _, val := range list { req.Header.Add(key, val) From d3ed898aa8a312e7e3290a8ac5a123149de68011 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 13 Jan 2021 20:38:24 +0000 Subject: [PATCH 08/20] google: support url-sourced 3rd party credentials Implements functionality to allow for URL-sourced 3rd party credentials, expanding the functionality added in #462 . Change-Id: Ib7615fb618486612960d60bee6b9a1ecf5de1404 GitHub-Last-Rev: 95713928e495d51d2209bb81cbf2c16185441145 GitHub-Pull-Request: golang/oauth2#466 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/283372 Run-TryBot: Cody Oss TryBot-Result: Go Bot Reviewed-by: Cody Oss Trust: Tyler Bui-Palsulich Trust: Cody Oss --- google/google.go | 33 ++++--- .../externalaccount/basecredentials.go | 6 +- .../externalaccount/filecredsource_test.go | 3 +- .../internal/externalaccount/urlcredsource.go | 71 ++++++++++++++ .../externalaccount/urlcredsource_test.go | 92 +++++++++++++++++++ 5 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 google/internal/externalaccount/urlcredsource.go create mode 100644 google/internal/externalaccount/urlcredsource_test.go diff --git a/google/google.go b/google/google.go index e247491..2c8f1bd 100644 --- a/google/google.go +++ b/google/google.go @@ -115,14 +115,13 @@ type credentialsFile struct { RefreshToken string `json:"refresh_token"` // External Account fields - Audience string `json:"audience"` - SubjectTokenType string `json:"subject_token_type"` - TokenURLExternal string `json:"token_url"` - TokenInfoURL string `json:"token_info_url"` - ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` - CredentialSource externalaccount.CredentialSource `json:"credential_source"` - QuotaProjectID string `json:"quota_project_id"` - + Audience string `json:"audience"` + SubjectTokenType string `json:"subject_token_type"` + TokenURLExternal string `json:"token_url"` + TokenInfoURL string `json:"token_info_url"` + ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` + CredentialSource externalaccount.CredentialSource `json:"credential_source"` + QuotaProjectID string `json:"quota_project_id"` } func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { @@ -155,16 +154,16 @@ func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oau return cfg.TokenSource(ctx, tok), nil case externalAccountKey: cfg := &externalaccount.Config{ - Audience: f.Audience, - SubjectTokenType: f.SubjectTokenType, - TokenURL: f.TokenURLExternal, - TokenInfoURL: f.TokenInfoURL, + Audience: f.Audience, + SubjectTokenType: f.SubjectTokenType, + TokenURL: f.TokenURLExternal, + TokenInfoURL: f.TokenInfoURL, ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, - ClientSecret: f.ClientSecret, - ClientID: f.ClientID, - CredentialSource: f.CredentialSource, - QuotaProjectID: f.QuotaProjectID, - Scopes: scopes, + ClientSecret: f.ClientSecret, + ClientID: f.ClientID, + CredentialSource: f.CredentialSource, + QuotaProjectID: f.QuotaProjectID, + Scopes: scopes, } return cfg.TokenSource(ctx), nil case "": diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 3291d46..dff0881 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -66,9 +66,11 @@ type CredentialSource struct { } // parse determines the type of CredentialSource needed -func (c *Config) parse() baseCredentialSource { +func (c *Config) parse(ctx context.Context) baseCredentialSource { if c.CredentialSource.File != "" { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format} + } else if c.CredentialSource.URL != "" { + return urlCredentialSource{URL: c.CredentialSource.URL, Format: c.CredentialSource.Format, ctx: ctx} } return nil } @@ -87,7 +89,7 @@ type tokenSource struct { func (ts tokenSource) Token() (*oauth2.Token, error) { conf := ts.conf - credSource := conf.parse() + credSource := conf.parse(ts.ctx) if credSource == nil { return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } diff --git a/google/internal/externalaccount/filecredsource_test.go b/google/internal/externalaccount/filecredsource_test.go index 0bc8048..56dd71e 100644 --- a/google/internal/externalaccount/filecredsource_test.go +++ b/google/internal/externalaccount/filecredsource_test.go @@ -5,6 +5,7 @@ package externalaccount import ( + "context" "testing" ) @@ -55,7 +56,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) { tfc.CredentialSource = test.cs t.Run(test.name, func(t *testing.T) { - out, err := tfc.parse().subjectToken() + out, err := tfc.parse(context.Background()).subjectToken() if err != nil { t.Errorf("Method subjectToken() errored.") } else if test.want != out { diff --git a/google/internal/externalaccount/urlcredsource.go b/google/internal/externalaccount/urlcredsource.go new file mode 100644 index 0000000..b0d5d35 --- /dev/null +++ b/google/internal/externalaccount/urlcredsource.go @@ -0,0 +1,71 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "net/http" +) + +type urlCredentialSource struct { + URL string + Headers map[string]string + Format format + ctx context.Context +} + +func (cs urlCredentialSource) subjectToken() (string, error) { + client := oauth2.NewClient(cs.ctx, nil) + req, err := http.NewRequest("GET", cs.URL, nil) + if err != nil { + return "", fmt.Errorf("oauth2/google: HTTP request for URL-sourced credential failed: %v", err) + } + req = req.WithContext(cs.ctx) + + for key, val := range cs.Headers { + req.Header.Add(key, val) + } + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("oauth2/google: invalid response when retrieving subject token: %v", err) + } + defer resp.Body.Close() + + tokenBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err) + } + + switch cs.Format.Type { + case "json": + jsonData := make(map[string]interface{}) + err = json.Unmarshal(tokenBytes, &jsonData) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + } + val, ok := jsonData[cs.Format.SubjectTokenFieldName] + if !ok { + return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + } + token, ok := val.(string) + if !ok { + return "", errors.New("oauth2/google: improperly formatted subject token") + } + return token, nil + case "text": + return string(tokenBytes), nil + case "": + return string(tokenBytes), nil + default: + return "", errors.New("oauth2/google: invalid credential_source file format type") + } + +} diff --git a/google/internal/externalaccount/urlcredsource_test.go b/google/internal/externalaccount/urlcredsource_test.go new file mode 100644 index 0000000..592610f --- /dev/null +++ b/google/internal/externalaccount/urlcredsource_test.go @@ -0,0 +1,92 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +var myURLToken = "testTokenValue" + +func TestRetrieveURLSubjectToken_Text(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Unexpected request method, %v is found", r.Method) + } + w.Write([]byte("testTokenValue")) + })) + cs := CredentialSource{ + URL: ts.URL, + Format: format{Type: fileTypeText}, + } + tfc := testFileConfig + tfc.CredentialSource = cs + + out, err := tfc.parse(context.Background()).subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + if out != myURLToken { + t.Errorf("got %v but want %v", out, myURLToken) + } +} + +// Checking that retrieveSubjectToken properly defaults to type text +func TestRetrieveURLSubjectToken_Untyped(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Unexpected request method, %v is found", r.Method) + } + w.Write([]byte("testTokenValue")) + })) + cs := CredentialSource{ + URL: ts.URL, + } + tfc := testFileConfig + tfc.CredentialSource = cs + + out, err := tfc.parse(context.Background()).subjectToken() + if err != nil { + t.Fatalf("Failed to retrieve URL subject token: %v", err) + } + if out != myURLToken { + t.Errorf("got %v but want %v", out, myURLToken) + } +} + +func TestRetrieveURLSubjectToken_JSON(t *testing.T) { + type tokenResponse struct { + TestToken string `json:"SubjToken"` + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Method, "GET"; got != want { + t.Errorf("got %v, but want %v", r.Method, want) + } + resp := tokenResponse{TestToken: "testTokenValue"} + jsonResp, err := json.Marshal(resp) + if err != nil { + t.Errorf("Failed to marshal values: %v", err) + } + w.Write(jsonResp) + })) + cs := CredentialSource{ + URL: ts.URL, + Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, + } + tfc := testFileConfig + tfc.CredentialSource = cs + + out, err := tfc.parse(context.Background()).subjectToken() + if err != nil { + t.Fatalf("%v", err) + } + if out != myURLToken { + t.Errorf("got %v but want %v", out, myURLToken) + } +} From af13f521f1965f34d5ede60ad1b69b5c6292783e Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Mon, 25 Jan 2021 20:07:25 +0000 Subject: [PATCH 09/20] google: Create AWS V4 Signing Utility Change-Id: I59b4a13ed0433de7dfaa064a0f7dc1f3dd724518 GitHub-Last-Rev: 8cdc6a9ad02da61234e7d18db77294cef31367da GitHub-Pull-Request: golang/oauth2#467 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/284632 Run-TryBot: Cody Oss TryBot-Result: Go Bot Trust: Cody Oss Trust: Tyler Bui-Palsulich Reviewed-by: Cody Oss --- google/internal/externalaccount/aws.go | 230 ++++++++++++ google/internal/externalaccount/aws_test.go | 396 ++++++++++++++++++++ 2 files changed, 626 insertions(+) create mode 100644 google/internal/externalaccount/aws.go create mode 100644 google/internal/externalaccount/aws_test.go diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go new file mode 100644 index 0000000..906d1fe --- /dev/null +++ b/google/internal/externalaccount/aws.go @@ -0,0 +1,230 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "path" + "sort" + "strings" + "time" +) + +// RequestSigner is a utility class to sign http requests using a AWS V4 signature. +type awsRequestSigner struct { + RegionName string + AwsSecurityCredentials map[string]string +} + +const ( +// AWS Signature Version 4 signing algorithm identifier. + awsAlgorithm = "AWS4-HMAC-SHA256" + +// The termination string for the AWS credential scope value as defined in +// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + awsRequestType = "aws4_request" + +// The AWS authorization header name for the security session token if available. + awsSecurityTokenHeader = "x-amz-security-token" + +// The AWS authorization header name for the auto-generated date. + awsDateHeader = "x-amz-date" + + awsTimeFormatLong = "20060102T150405Z" + awsTimeFormatShort = "20060102" +) + +func getSha256(input []byte) (string, error) { + hash := sha256.New() + if _, err := hash.Write(input); err != nil { + return "", err + } + return hex.EncodeToString(hash.Sum(nil)), nil +} + +func getHmacSha256(key, input []byte) ([]byte, error) { + hash := hmac.New(sha256.New, key) + if _, err := hash.Write(input); err != nil { + return nil, err + } + return hash.Sum(nil), nil +} + +func cloneRequest(r *http.Request) *http.Request { + r2 := new(http.Request) + *r2 = *r + if r.Header != nil { + r2.Header = make(http.Header, len(r.Header)) + + // Find total number of values. + headerCount := 0 + for _, headerValues := range r.Header { + headerCount += len(headerValues) + } + copiedHeaders := make([]string, headerCount) // shared backing array for headers' values + + for headerKey, headerValues := range r.Header { + headerCount = copy(copiedHeaders, headerValues) + r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount] + copiedHeaders = copiedHeaders[headerCount:] + } + } + return r2 +} + +func canonicalPath(req *http.Request) string { + result := req.URL.EscapedPath() + if result == "" { + return "/" + } + return path.Clean(result) +} + +func canonicalQuery(req *http.Request) string { + queryValues := req.URL.Query() + for queryKey := range queryValues { + sort.Strings(queryValues[queryKey]) + } + return queryValues.Encode() +} + +func canonicalHeaders(req *http.Request) (string, string) { + // Header keys need to be sorted alphabetically. + var headers []string + lowerCaseHeaders := make(http.Header) + for k, v := range req.Header { + k := strings.ToLower(k) + if _, ok := lowerCaseHeaders[k]; ok { + // include additional values + lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...) + } else { + headers = append(headers, k) + lowerCaseHeaders[k] = v + } + } + sort.Strings(headers) + + var fullHeaders strings.Builder + for _, header := range headers { + headerValue := strings.Join(lowerCaseHeaders[header], ",") + fullHeaders.WriteString(header) + fullHeaders.WriteRune(':') + fullHeaders.WriteString(headerValue) + fullHeaders.WriteRune('\n') + } + + return strings.Join(headers, ";"), fullHeaders.String() +} + +func requestDataHash(req *http.Request) (string, error) { + var requestData []byte + if req.Body != nil { + requestBody, err := req.GetBody() + if err != nil { + return "", err + } + defer requestBody.Close() + + requestData, err = ioutil.ReadAll(io.LimitReader(requestBody, 1<<20)) + if err != nil { + return "", err + } + } + + return getSha256(requestData) +} + +func requestHost(req *http.Request) string { + if req.Host != "" { + return req.Host + } + return req.URL.Host +} + +func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) { + dataHash, err := requestDataHash(req) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil +} + +// SignRequest adds the appropriate headers to an http.Request +// or returns an error if something prevented this. +func (rs *awsRequestSigner) SignRequest(req *http.Request) error { + signedRequest := cloneRequest(req) + timestamp := now() + + signedRequest.Header.Add("host", requestHost(req)) + + if securityToken, ok := rs.AwsSecurityCredentials["security_token"]; ok { + signedRequest.Header.Add(awsSecurityTokenHeader, securityToken) + } + + if signedRequest.Header.Get("date") == "" { + signedRequest.Header.Add(awsDateHeader, timestamp.Format(awsTimeFormatLong)) + } + + authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp) + if err != nil { + return err + } + signedRequest.Header.Set("Authorization", authorizationCode) + + req.Header = signedRequest.Header + return nil +} + +func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) { + secretAccessKey, ok := rs.AwsSecurityCredentials["secret_access_key"] + if !ok { + return "", errors.New("oauth2/google: missing secret_access_key header") + } + accessKeyId, ok := rs.AwsSecurityCredentials["access_key_id"] + if !ok { + return "", errors.New("oauth2/google: missing access_key_id header") + } + + canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req) + + dateStamp := timestamp.Format(awsTimeFormatShort) + serviceName := "" + if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 { + serviceName = splitHost[0] + } + + credentialScope := fmt.Sprintf("%s/%s/%s/%s",dateStamp, rs.RegionName, serviceName, awsRequestType) + + requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData) + if err != nil { + return "", err + } + requestHash, err := getSha256([]byte(requestString)) + if err != nil{ + return "", err + } + + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash) + + signingKey := []byte("AWS4" + secretAccessKey) + for _, signingInput := range []string{ + dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign, + } { + signingKey, err = getHmacSha256(signingKey, []byte(signingInput)) + if err != nil{ + return "", err + } + } + + return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, accessKeyId, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil +} diff --git a/google/internal/externalaccount/aws_test.go b/google/internal/externalaccount/aws_test.go new file mode 100644 index 0000000..206c3a1 --- /dev/null +++ b/google/internal/externalaccount/aws_test.go @@ -0,0 +1,396 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "net/http" + "reflect" + "strings" + "testing" + "time" +) + +var defaultTime = time.Date(2011, 9, 9, 23, 36, 0, 0, time.UTC) +var secondDefaultTime = time.Date(2020, 8, 11, 6, 55, 22, 0, time.UTC) + +func setTime(testTime time.Time) func() time.Time { + return func() time.Time { + return testTime + } +} + +var defaultRequestSigner = &awsRequestSigner{ + RegionName: "us-east-1", + AwsSecurityCredentials: map[string]string{ + "access_key_id": "AKIDEXAMPLE", + "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + }, +} + +const accessKeyId = "ASIARD4OQDT6A77FR3CL" +const secretAccessKey = "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx" +const securityToken = "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA==" + +var requestSignerWithToken = &awsRequestSigner{ + RegionName: "us-east-2", + AwsSecurityCredentials: map[string]string{ + "access_key_id": accessKeyId, + "secret_access_key": secretAccessKey, + "security_token": securityToken, + }, +} + +func setDefaultTime(req *http.Request) { + // Don't use time.Format for this + // Our output signature expects this to be a Monday, even though Sept 9, 2011 is a Friday + req.Header.Add("date", "Mon, 09 Sep 2011 23:36:00 GMT") +} + +func testRequestSigner(t *testing.T, rs *awsRequestSigner, input, expectedOutput *http.Request) { + t.Helper() + + err := rs.SignRequest(input) + if err != nil { + t.Errorf("unexpected error: %q", err.Error()) + } + + if got, want := input.URL.String(), expectedOutput.URL.String(); !reflect.DeepEqual(got, want) { + t.Errorf("url = %q, want %q", got, want) + } + if got, want := input.Method, expectedOutput.Method; !reflect.DeepEqual(got, want) { + t.Errorf("method = %q, want %q", got, want) + } + for header := range expectedOutput.Header { + if got, want := input.Header[header], expectedOutput.Header[header]; !reflect.DeepEqual(got, want) { + t.Errorf("header[%q] = %q, want %q", header, got, want) + } + } +} + +func TestAwsV4Signature_GetRequest(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithRelativePath(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/foo/bar/../..", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/foo/bar/../..", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithDotPath(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/./", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/./", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithPointlessDotPath(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/./foo", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/./foo", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithUtf8Path(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/%E1%88%B4", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/%E1%88%B4", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithDuplicateQuery(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/?foo=Zoo&foo=aha", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/?foo=Zoo&foo=aha", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithMisorderedQuery(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/?foo=b&foo=a", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/?foo=b&foo=a", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=feb926e49e382bec75c9d7dcb2a1b6dc8aa50ca43c25d2bc51143768c0875acc"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithUtf8Query(t *testing.T) { + input, _ := http.NewRequest("GET", "https://host.foo.com/?ሴ=bar", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("GET", "https://host.foo.com/?ሴ=bar", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=6fb359e9a05394cc7074e0feb42573a2601abc0c869a953e8c5c12e4e01f1a8c"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_PostRequest(t *testing.T) { + input, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + setDefaultTime(input) + input.Header.Add("ZOO", "zoobar") + + output, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a"}, + "Zoo": []string{"zoobar"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_PostRequestWithCapitalizedHeaderValue(t *testing.T) { + input, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + setDefaultTime(input) + input.Header.Add("zoo", "ZOOBAR") + + output, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7"}, + "Zoo": []string{"ZOOBAR"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_PostRequestPhfft(t *testing.T) { + input, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + setDefaultTime(input) + input.Header.Add("p", "phfft") + + output, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592"}, + "P": []string{"phfft"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_PostRequestWithBody(t *testing.T) { + input, _ := http.NewRequest("POST", "https://host.foo.com/", strings.NewReader("foo=bar")) + setDefaultTime(input) + input.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + output, _ := http.NewRequest("POST", "https://host.foo.com/", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Content-Type": []string{"application/x-www-form-urlencoded"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=content-type;date;host, Signature=5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_PostRequestWithQueryString(t *testing.T) { + input, _ := http.NewRequest("POST", "https://host.foo.com/?foo=bar", nil) + setDefaultTime(input) + + output, _ := http.NewRequest("POST", "https://host.foo.com/?foo=bar", nil) + output.Header = http.Header{ + "Host": []string{"host.foo.com"}, + "Date": []string{"Mon, 09 Sep 2011 23:36:00 GMT"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(defaultTime) + + testRequestSigner(t, defaultRequestSigner, input, output) +} + +func TestAwsV4Signature_GetRequestWithSecurityToken(t *testing.T) { + input, _ := http.NewRequest("GET", "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15", nil) + + output, _ := http.NewRequest("GET", "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15", nil) + output.Header = http.Header{ + "Host": []string{"ec2.us-east-2.amazonaws.com"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855"}, + "X-Amz-Date": []string{"20200811T065522Z"}, + "X-Amz-Security-Token": []string{securityToken}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(secondDefaultTime) + + testRequestSigner(t, requestSignerWithToken, input, output) +} + +func TestAwsV4Signature_PostRequestWithSecurityToken(t *testing.T) { + input, _ := http.NewRequest("POST", "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", nil) + + output, _ := http.NewRequest("POST", "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", nil) + output.Header = http.Header{ + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a"}, + "Host": []string{"sts.us-east-2.amazonaws.com"}, + "X-Amz-Date": []string{"20200811T065522Z"}, + "X-Amz-Security-Token": []string{securityToken}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(secondDefaultTime) + + testRequestSigner(t, requestSignerWithToken, input, output) +} + +func TestAwsV4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *testing.T) { + requestParams := "{\"KeySchema\":[{\"KeyType\":\"HASH\",\"AttributeName\":\"Id\"}],\"TableName\":\"TestTable\",\"AttributeDefinitions\":[{\"AttributeName\":\"Id\",\"AttributeType\":\"S\"}],\"ProvisionedThroughput\":{\"WriteCapacityUnits\":5,\"ReadCapacityUnits\":5}}" + input, _ := http.NewRequest("POST", "https://dynamodb.us-east-2.amazonaws.com/", strings.NewReader(requestParams)) + input.Header.Add("Content-Type", "application/x-amz-json-1.0") + input.Header.Add("x-amz-target", "DynamoDB_20120810.CreateTable") + + output, _ := http.NewRequest("POST", "https://dynamodb.us-east-2.amazonaws.com/", strings.NewReader(requestParams)) + output.Header = http.Header{ + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385"}, + "Host": []string{"dynamodb.us-east-2.amazonaws.com"}, + "X-Amz-Date": []string{"20200811T065522Z"}, + "Content-Type": []string{"application/x-amz-json-1.0"}, + "X-Amz-Target": []string{"DynamoDB_20120810.CreateTable"}, + "X-Amz-Security-Token": []string{securityToken}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(secondDefaultTime) + + testRequestSigner(t, requestSignerWithToken, input, output) +} + +func TestAwsV4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) { + var requestSigner = &awsRequestSigner{ + RegionName: "us-east-2", + AwsSecurityCredentials: map[string]string{ + "access_key_id": accessKeyId, + "secret_access_key": secretAccessKey, + }, + } + + input, _ := http.NewRequest("POST", "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", nil) + + output, _ := http.NewRequest("POST", "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", nil) + output.Header = http.Header{ + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=d095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56"}, + "Host": []string{"sts.us-east-2.amazonaws.com"}, + "X-Amz-Date": []string{"20200811T065522Z"}, + } + + oldNow := now + defer func() { now = oldNow }() + now = setTime(secondDefaultTime) + + testRequestSigner(t, requestSigner, input, output) +} From f9ce19ea30139fe25e55e62b15cfa969d4267230 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Tue, 26 Jan 2021 19:21:15 +0000 Subject: [PATCH 10/20] google: support service account impersonation Adds support for service account impersonation when a URL for service account impersonation is provided. Change-Id: I9f3bbd6926212cecb13938fc5dac358ba56855b8 GitHub-Last-Rev: 9c218789db45e9b80bb8bec5c69539dd386d9668 GitHub-Pull-Request: golang/oauth2#468 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/285012 Run-TryBot: Cody Oss TryBot-Result: Go Bot Trust: Cody Oss Trust: Tyler Bui-Palsulich Reviewed-by: Cody Oss --- .../externalaccount/basecredentials.go | 14 ++- .../externalaccount/basecredentials_test.go | 17 ++-- .../internal/externalaccount/impersonate.go | 83 ++++++++++++++++ .../externalaccount/impersonate_test.go | 95 +++++++++++++++++++ 4 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 google/internal/externalaccount/impersonate.go create mode 100644 google/internal/externalaccount/impersonate_test.go diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index dff0881..deb9deb 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -35,7 +35,18 @@ func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { ctx: ctx, conf: c, } - return oauth2.ReuseTokenSource(nil, ts) + if c.ServiceAccountImpersonationURL == "" { + return oauth2.ReuseTokenSource(nil, ts) + } + scopes := c.Scopes + ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} + imp := impersonateTokenSource{ + ctx: ctx, + url: c.ServiceAccountImpersonationURL, + scopes: scopes, + ts: oauth2.ReuseTokenSource(nil, ts), + } + return oauth2.ReuseTokenSource(nil, imp) } // Subject token file types. @@ -130,6 +141,5 @@ func (ts tokenSource) Token() (*oauth2.Token, error) { if stsResp.RefreshToken != "" { accessToken.RefreshToken = stsResp.RefreshToken } - return accessToken, nil } diff --git a/google/internal/externalaccount/basecredentials_test.go b/google/internal/externalaccount/basecredentials_test.go index 7ec12e4..eb60899 100644 --- a/google/internal/externalaccount/basecredentials_test.go +++ b/google/internal/externalaccount/basecredentials_test.go @@ -19,14 +19,13 @@ var testBaseCredSource = CredentialSource{ } var testConfig = Config{ - Audience: "32555940559.apps.googleusercontent.com", - SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - TokenInfoURL: "http://localhost:8080/v1/tokeninfo", - ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken", - ClientSecret: "notsosecret", - ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, - Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, } var ( @@ -55,7 +54,7 @@ func TestToken(t *testing.T) { } body, err := ioutil.ReadAll(r.Body) if err != nil { - t.Errorf("Failed reading request body: %s.", err) + t.Fatalf("Failed reading request body: %s.", err) } if got, want := string(body), baseCredsRequestBody; got != want { t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go new file mode 100644 index 0000000..1d29c46 --- /dev/null +++ b/google/internal/externalaccount/impersonate.go @@ -0,0 +1,83 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "net/http" + "time" +) + +// generateAccesstokenReq is used for service account impersonation +type generateAccessTokenReq struct { + Delegates []string `json:"delegates,omitempty"` + Lifetime string `json:"lifetime,omitempty"` + Scope []string `json:"scope,omitempty"` +} + +type impersonateTokenResponse struct { + AccessToken string `json:"accessToken"` + ExpireTime string `json:"expireTime"` +} + +type impersonateTokenSource struct { + ctx context.Context + ts oauth2.TokenSource + + url string + scopes []string +} + +// Token performs the exchange to get a temporary service account +func (its impersonateTokenSource) Token() (*oauth2.Token, error) { + reqBody := generateAccessTokenReq{ + Lifetime: "3600s", + Scope: its.scopes, + } + b, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err) + } + client := oauth2.NewClient(its.ctx, its.ts) + req, err := http.NewRequest("POST", its.url, bytes.NewReader(b)) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) + } + req = req.WithContext(its.ctx) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err) + } + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) + } + + var accessTokenResp impersonateTokenResponse + if err := json.Unmarshal(body, &accessTokenResp); err != nil { + return nil, fmt.Errorf("oauth2/google: unable to parse response: %v", err) + } + expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to parse expiry: %v", err) + } + return &oauth2.Token{ + AccessToken: accessTokenResp.AccessToken, + Expiry: expiry, + TokenType: "Bearer", + }, nil +} diff --git a/google/internal/externalaccount/impersonate_test.go b/google/internal/externalaccount/impersonate_test.go new file mode 100644 index 0000000..a2d8978 --- /dev/null +++ b/google/internal/externalaccount/impersonate_test.go @@ -0,0 +1,95 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +var testImpersonateConfig = Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, +} + +var ( + baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" + baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}` +) + +func TestImpersonation(t *testing.T) { + impersonateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.URL.String(), "/"; got != want { + t.Errorf("URL.String(): got %v but want %v", got, want) + } + headerAuth := r.Header.Get("Authorization") + if got, want := headerAuth, "Bearer Sample.Access.Token"; got != want { + t.Errorf("got %v but want %v", got, want) + } + headerContentType := r.Header.Get("Content-Type") + if got, want := headerContentType, "application/json"; got != want { + t.Errorf("got %v but want %v", got, want) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed reading request body: %v.", err) + } + if got, want := string(body), "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}"; got != want { + t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(baseImpersonateCredsRespBody)) + })) + testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL + targetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.URL.String(), "/"; got != want { + t.Errorf("URL.String(): got %v but want %v", got, want) + } + headerAuth := r.Header.Get("Authorization") + if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want { + t.Errorf("got %v but want %v", got, want) + } + headerContentType := r.Header.Get("Content-Type") + if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { + t.Errorf("got %v but want %v", got, want) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed reading request body: %v.", err) + } + if got, want := string(body), baseImpersonateCredsReqBody; got != want { + t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(baseCredsResponseBody)) + })) + defer targetServer.Close() + + testImpersonateConfig.TokenURL = targetServer.URL + ourTS := testImpersonateConfig.TokenSource(context.Background()) + + oldNow := now + defer func() { now = oldNow }() + now = testNow + + tok, err := ourTS.Token() + if err != nil { + t.Fatalf("Unexpected error: %e", err) + } + if got, want := tok.AccessToken, "Second.Access.Token"; got != want { + t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) + } + if got, want := tok.TokenType, "Bearer"; got != want { + t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) + } +} From 010130855d6ca777e79c9e50ae4f39373ea0ef74 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Mon, 1 Feb 2021 16:26:06 +0000 Subject: [PATCH 11/20] google: support AWS 3rd party credentials Change-Id: I655b38f7fb8023866bb284c7ce80ab9888682e73 GitHub-Last-Rev: 648f0b3d45d94760bb29e6bfe4680351d8e0364d GitHub-Pull-Request: golang/oauth2#471 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/287752 Reviewed-by: Cody Oss Run-TryBot: Cody Oss TryBot-Result: Go Bot Trust: Tyler Bui-Palsulich Trust: Cody Oss --- google/internal/externalaccount/aws.go | 286 +++++++++- google/internal/externalaccount/aws_test.go | 490 +++++++++++++++++- .../externalaccount/basecredentials.go | 32 +- .../externalaccount/filecredsource_test.go | 7 +- .../externalaccount/urlcredsource_test.go | 21 +- 5 files changed, 781 insertions(+), 55 deletions(-) diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go index 906d1fe..3725a0f 100644 --- a/google/internal/externalaccount/aws.go +++ b/google/internal/externalaccount/aws.go @@ -5,41 +5,54 @@ package externalaccount import ( + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" + "encoding/json" "errors" "fmt" + "golang.org/x/oauth2" "io" "io/ioutil" "net/http" + "os" "path" "sort" "strings" "time" ) -// RequestSigner is a utility class to sign http requests using a AWS V4 signature. -type awsRequestSigner struct { - RegionName string - AwsSecurityCredentials map[string]string +type awsSecurityCredentials struct { + AccessKeyID string `json:"AccessKeyID"` + SecretAccessKey string `json:"SecretAccessKey"` + SecurityToken string `json:"Token"` } +// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature. +type awsRequestSigner struct { + RegionName string + AwsSecurityCredentials awsSecurityCredentials +} + +// getenv aliases os.Getenv for testing +var getenv = os.Getenv + const ( -// AWS Signature Version 4 signing algorithm identifier. + // AWS Signature Version 4 signing algorithm identifier. awsAlgorithm = "AWS4-HMAC-SHA256" -// The termination string for the AWS credential scope value as defined in -// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + // The termination string for the AWS credential scope value as defined in + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html awsRequestType = "aws4_request" -// The AWS authorization header name for the security session token if available. + // The AWS authorization header name for the security session token if available. awsSecurityTokenHeader = "x-amz-security-token" -// The AWS authorization header name for the auto-generated date. + // The AWS authorization header name for the auto-generated date. awsDateHeader = "x-amz-date" - awsTimeFormatLong = "20060102T150405Z" + awsTimeFormatLong = "20060102T150405Z" awsTimeFormatShort = "20060102" ) @@ -167,8 +180,8 @@ func (rs *awsRequestSigner) SignRequest(req *http.Request) error { signedRequest.Header.Add("host", requestHost(req)) - if securityToken, ok := rs.AwsSecurityCredentials["security_token"]; ok { - signedRequest.Header.Add(awsSecurityTokenHeader, securityToken) + if rs.AwsSecurityCredentials.SecurityToken != "" { + signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SecurityToken) } if signedRequest.Header.Get("date") == "" { @@ -186,15 +199,6 @@ func (rs *awsRequestSigner) SignRequest(req *http.Request) error { } func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) { - secretAccessKey, ok := rs.AwsSecurityCredentials["secret_access_key"] - if !ok { - return "", errors.New("oauth2/google: missing secret_access_key header") - } - accessKeyId, ok := rs.AwsSecurityCredentials["access_key_id"] - if !ok { - return "", errors.New("oauth2/google: missing access_key_id header") - } - canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req) dateStamp := timestamp.Format(awsTimeFormatShort) @@ -203,28 +207,258 @@ func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp serviceName = splitHost[0] } - credentialScope := fmt.Sprintf("%s/%s/%s/%s",dateStamp, rs.RegionName, serviceName, awsRequestType) + credentialScope := fmt.Sprintf("%s/%s/%s/%s", dateStamp, rs.RegionName, serviceName, awsRequestType) requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData) if err != nil { return "", err } requestHash, err := getSha256([]byte(requestString)) - if err != nil{ + if err != nil { return "", err } stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash) - signingKey := []byte("AWS4" + secretAccessKey) + signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey) for _, signingInput := range []string{ dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign, } { signingKey, err = getHmacSha256(signingKey, []byte(signingInput)) - if err != nil{ + if err != nil { return "", err } } - return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, accessKeyId, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil + return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil +} + +type awsCredentialSource struct { + EnvironmentID string + RegionURL string + RegionalCredVerificationURL string + CredVerificationURL string + TargetResource string + requestSigner *awsRequestSigner + region string + ctx context.Context + client *http.Client +} + +type awsRequestHeader struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type awsRequest struct { + URL string `json:"url"` + Method string `json:"method"` + Headers []awsRequestHeader `json:"headers"` +} + +func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, error) { + if cs.client == nil { + cs.client = oauth2.NewClient(cs.ctx, nil) + } + return cs.client.Do(req.WithContext(cs.ctx)) +} + +func (cs awsCredentialSource) subjectToken() (string, error) { + if cs.requestSigner == nil { + awsSecurityCredentials, err := cs.getSecurityCredentials() + if err != nil { + return "", err + } + + if cs.region, err = cs.getRegion(); err != nil { + return "", err + } + + cs.requestSigner = &awsRequestSigner{ + RegionName: cs.region, + AwsSecurityCredentials: awsSecurityCredentials, + } + } + + // Generate the signed request to AWS STS GetCallerIdentity API. + // Use the required regional endpoint. Otherwise, the request will fail. + req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil) + if err != nil { + return "", err + } + // The full, canonical resource name of the workload identity pool + // provider, with or without the HTTPS prefix. + // Including this header as part of the signature is recommended to + // ensure data integrity. + if cs.TargetResource != "" { + req.Header.Add("x-goog-cloud-target-resource", cs.TargetResource) + } + cs.requestSigner.SignRequest(req) + + /* + The GCP STS endpoint expects the headers to be formatted as: + # [ + # {key: 'x-amz-date', value: '...'}, + # {key: 'Authorization', value: '...'}, + # ... + # ] + # And then serialized as: + # quote(json.dumps({ + # url: '...', + # method: 'POST', + # headers: [{key: 'x-amz-date', value: '...'}, ...] + # })) + */ + + awsSignedReq := awsRequest{ + URL: req.URL.String(), + Method: "POST", + } + for headerKey, headerList := range req.Header { + for _, headerValue := range headerList { + awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{ + Key: headerKey, + Value: headerValue, + }) + } + } + sort.Slice(awsSignedReq.Headers, func(i, j int) bool { + headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key) + if headerCompare == 0 { + return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0 + } + return headerCompare < 0 + }) + + result, err := json.Marshal(awsSignedReq) + if err != nil { + return "", err + } + return string(result), nil +} + +func (cs *awsCredentialSource) getRegion() (string, error) { + if envAwsRegion := getenv("AWS_REGION"); envAwsRegion != "" { + return envAwsRegion, nil + } + + if cs.RegionURL == "" { + return "", errors.New("oauth2/google: unable to determine AWS region") + } + + req, err := http.NewRequest("GET", cs.RegionURL, nil) + if err != nil { + return "", err + } + + resp, err := cs.doRequest(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("oauth2/google: unable to retrieve AWS region - %s", string(respBody)) + } + + // This endpoint will return the region in format: us-east-2b. + // Only the us-east-2 part should be used. + respBodyEnd := 0 + if len(respBody) > 1 { + respBodyEnd = len(respBody) - 1 + } + return string(respBody[:respBodyEnd]), nil +} + +func (cs *awsCredentialSource) getSecurityCredentials() (result awsSecurityCredentials, err error) { + if accessKeyID := getenv("AWS_ACCESS_KEY_ID"); accessKeyID != "" { + if secretAccessKey := getenv("AWS_SECRET_ACCESS_KEY"); secretAccessKey != "" { + return awsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SecurityToken: getenv("AWS_SESSION_TOKEN"), + }, nil + } + } + + roleName, err := cs.getMetadataRoleName() + if err != nil { + return + } + + credentials, err := cs.getMetadataSecurityCredentials(roleName) + if err != nil { + return + } + + if credentials.AccessKeyID == "" { + return result, errors.New("oauth2/google: missing AccessKeyId credential") + } + + if credentials.SecretAccessKey == "" { + return result, errors.New("oauth2/google: missing SecretAccessKey credential") + } + + return credentials, nil +} + +func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string) (awsSecurityCredentials, error) { + var result awsSecurityCredentials + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.CredVerificationURL, roleName), nil) + if err != nil { + return result, err + } + req.Header.Add("Content-Type", "application/json") + + resp, err := cs.doRequest(req) + if err != nil { + return result, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return result, err + } + + if resp.StatusCode != 200 { + return result, fmt.Errorf("oauth2/google: unable to retrieve AWS security credentials - %s", string(respBody)) + } + + err = json.Unmarshal(respBody, &result) + return result, err +} + +func (cs *awsCredentialSource) getMetadataRoleName() (string, error) { + if cs.CredVerificationURL == "" { + return "", errors.New("oauth2/google: unable to determine the AWS metadata server security credentials endpoint") + } + + req, err := http.NewRequest("GET", cs.CredVerificationURL, nil) + if err != nil { + return "", err + } + + resp, err := cs.doRequest(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("oauth2/google: unable to retrieve AWS role name - %s", string(respBody)) + } + + return string(respBody), nil } diff --git a/google/internal/externalaccount/aws_test.go b/google/internal/externalaccount/aws_test.go index 206c3a1..1a83a7b 100644 --- a/google/internal/externalaccount/aws_test.go +++ b/google/internal/externalaccount/aws_test.go @@ -5,7 +5,11 @@ package externalaccount import ( + "context" + "encoding/json" + "fmt" "net/http" + "net/http/httptest" "reflect" "strings" "testing" @@ -21,24 +25,33 @@ func setTime(testTime time.Time) func() time.Time { } } +func setEnvironment(env map[string]string) func(string) string { + return func(key string) string { + value, _ := env[key] + return value + } +} + var defaultRequestSigner = &awsRequestSigner{ RegionName: "us-east-1", - AwsSecurityCredentials: map[string]string{ - "access_key_id": "AKIDEXAMPLE", - "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + AwsSecurityCredentials: awsSecurityCredentials{ + AccessKeyID: "AKIDEXAMPLE", + SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, } -const accessKeyId = "ASIARD4OQDT6A77FR3CL" -const secretAccessKey = "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx" -const securityToken = "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA==" +const ( + accessKeyID = "ASIARD4OQDT6A77FR3CL" + secretAccessKey = "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx" + securityToken = "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA==" +) var requestSignerWithToken = &awsRequestSigner{ RegionName: "us-east-2", - AwsSecurityCredentials: map[string]string{ - "access_key_id": accessKeyId, - "secret_access_key": secretAccessKey, - "security_token": securityToken, + AwsSecurityCredentials: awsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SecurityToken: securityToken, }, } @@ -317,7 +330,7 @@ func TestAwsV4Signature_GetRequestWithSecurityToken(t *testing.T) { output, _ := http.NewRequest("GET", "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15", nil) output.Header = http.Header{ "Host": []string{"ec2.us-east-2.amazonaws.com"}, - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyID + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855"}, "X-Amz-Date": []string{"20200811T065522Z"}, "X-Amz-Security-Token": []string{securityToken}, } @@ -334,7 +347,7 @@ func TestAwsV4Signature_PostRequestWithSecurityToken(t *testing.T) { output, _ := http.NewRequest("POST", "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", nil) output.Header = http.Header{ - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyID + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a"}, "Host": []string{"sts.us-east-2.amazonaws.com"}, "X-Amz-Date": []string{"20200811T065522Z"}, "X-Amz-Security-Token": []string{securityToken}, @@ -355,7 +368,7 @@ func TestAwsV4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *test output, _ := http.NewRequest("POST", "https://dynamodb.us-east-2.amazonaws.com/", strings.NewReader(requestParams)) output.Header = http.Header{ - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyID + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385"}, "Host": []string{"dynamodb.us-east-2.amazonaws.com"}, "X-Amz-Date": []string{"20200811T065522Z"}, "Content-Type": []string{"application/x-amz-json-1.0"}, @@ -373,9 +386,9 @@ func TestAwsV4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *test func TestAwsV4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) { var requestSigner = &awsRequestSigner{ RegionName: "us-east-2", - AwsSecurityCredentials: map[string]string{ - "access_key_id": accessKeyId, - "secret_access_key": secretAccessKey, + AwsSecurityCredentials: awsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, }, } @@ -383,7 +396,7 @@ func TestAwsV4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) { output, _ := http.NewRequest("POST", "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", nil) output.Header = http.Header{ - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyId + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=d095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + accessKeyID + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=d095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56"}, "Host": []string{"sts.us-east-2.amazonaws.com"}, "X-Amz-Date": []string{"20200811T065522Z"}, } @@ -394,3 +407,446 @@ func TestAwsV4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) { testRequestSigner(t, requestSigner, input, output) } + +type testAwsServer struct { + url string + securityCredentialURL string + regionURL string + regionalCredVerificationURL string + + Credentials map[string]string + + WriteRolename func(http.ResponseWriter) + WriteSecurityCredentials func(http.ResponseWriter) + WriteRegion func(http.ResponseWriter) +} + +func createAwsTestServer(url, regionURL, regionalCredVerificationURL, rolename, region string, credentials map[string]string) *testAwsServer { + server := &testAwsServer{ + url: url, + securityCredentialURL: fmt.Sprintf("%s/%s", url, rolename), + regionURL: regionURL, + regionalCredVerificationURL: regionalCredVerificationURL, + Credentials: credentials, + WriteRolename: func(w http.ResponseWriter) { + w.Write([]byte(rolename)) + }, + WriteRegion: func(w http.ResponseWriter) { + w.Write([]byte(region)) + }, + } + + server.WriteSecurityCredentials = func(w http.ResponseWriter) { + jsonCredentials, _ := json.Marshal(server.Credentials) + w.Write(jsonCredentials) + } + + return server +} + +func createDefaultAwsTestServer() *testAwsServer { + return createAwsTestServer( + "/latest/meta-data/iam/security-credentials", + "/latest/meta-data/placement/availability-zone", + "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", + "gcp-aws-role", + "us-east-2b", + map[string]string{ + "SecretAccessKey": secretAccessKey, + "AccessKeyId": accessKeyID, + "Token": securityToken, + }, + ) +} + +func (server *testAwsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch p := r.URL.Path; p { + case server.url: + server.WriteRolename(w) + case server.securityCredentialURL: + server.WriteSecurityCredentials(w) + case server.regionURL: + server.WriteRegion(w) + } +} + +func notFound(w http.ResponseWriter) { + w.WriteHeader(404) + w.Write([]byte("Not Found")) +} + +func (server *testAwsServer) getCredentialSource(url string) CredentialSource { + return CredentialSource{ + EnvironmentID: "aws1", + URL: url + server.url, + RegionURL: url + server.regionURL, + RegionalCredVerificationURL: server.regionalCredVerificationURL, + } +} + +func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, securityToken string) string { + req, _ := http.NewRequest("POST", url, nil) + req.Header.Add("x-goog-cloud-target-resource", testFileConfig.Audience) + signer := &awsRequestSigner{ + RegionName: region, + AwsSecurityCredentials: awsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SecurityToken: securityToken, + }, + } + signer.SignRequest(req) + + result := awsRequest{ + URL: url, + Method: "POST", + Headers: []awsRequestHeader{ + { + Key: "Authorization", + Value: req.Header.Get("Authorization"), + }, { + Key: "Host", + Value: req.Header.Get("Host"), + }, { + Key: "X-Amz-Date", + Value: req.Header.Get("X-Amz-Date"), + }, + }, + } + + if securityToken != "" { + result.Headers = append(result.Headers, awsRequestHeader{ + Key: "X-Amz-Security-Token", + Value: securityToken, + }) + } + + result.Headers = append(result.Headers, awsRequestHeader{ + Key: "X-Goog-Cloud-Target-Resource", + Value: testFileConfig.Audience, + }) + + str, _ := json.Marshal(result) + return string(str) +} + +func TestAwsCredential_BasicRequest(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + expected := getExpectedSubjectToken( + "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", + "us-east-2", + accessKeyID, + secretAccessKey, + securityToken, + ) + + if got, want := out, expected; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_BasicRequestWithoutSecurityToken(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + delete(server.Credentials, "Token") + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + expected := getExpectedSubjectToken( + "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", + "us-east-2", + accessKeyID, + secretAccessKey, + "", + ) + + if got, want := out, expected; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_BasicRequestWithEnv(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{ + "AWS_ACCESS_KEY_ID": "AKIDEXAMPLE", + "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + "AWS_REGION": "us-west-1", + }) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + expected := getExpectedSubjectToken( + "https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", + "us-west-1", + "AKIDEXAMPLE", + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + "", + ) + + if got, want := out, expected; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithBadVersion(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + tfc.CredentialSource.EnvironmentID = "aws3" + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + _, err := tfc.parse(context.Background()) + if err == nil { + t.Fatalf("parse() should have failed") + } + if got, want := err.Error(), "oauth2/google: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithNoRegionURL(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + tfc.CredentialSource.RegionURL = "" + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: unable to determine AWS region"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithBadRegionURL(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + server.WriteRegion = notFound + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithMissingCredential(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + server.WriteSecurityCredentials = func(w http.ResponseWriter) { + w.Write([]byte("{}")) + } + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithIncompleteCredential(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + server.WriteSecurityCredentials = func(w http.ResponseWriter) { + w.Write([]byte(`{"AccessKeyId":"FOOBARBAS"}`)) + } + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithNoCredentialURL(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + tfc.CredentialSource.URL = "" + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithBadCredentialURL(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + server.WriteRolename = notFound + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + +func TestAwsCredential_RequestWithBadFinalCredentialURL(t *testing.T) { + server := createDefaultAwsTestServer() + ts := httptest.NewServer(server) + server.WriteSecurityCredentials = notFound + + tfc := testFileConfig + tfc.CredentialSource = server.getCredentialSource(ts.URL) + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("retrieveSubjectToken() should have failed") + } + + if got, want := err.Error(), "oauth2/google: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index deb9deb..57a5870 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -9,6 +9,7 @@ import ( "fmt" "golang.org/x/oauth2" "net/http" + "strconv" "time" ) @@ -77,13 +78,27 @@ type CredentialSource struct { } // parse determines the type of CredentialSource needed -func (c *Config) parse(ctx context.Context) baseCredentialSource { - if c.CredentialSource.File != "" { - return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format} +func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { + if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { + if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { + if awsVersion != 1 { + return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion) + } + return awsCredentialSource{ + EnvironmentID: c.CredentialSource.EnvironmentID, + RegionURL: c.CredentialSource.RegionURL, + RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, + CredVerificationURL: c.CredentialSource.URL, + TargetResource: c.Audience, + ctx: ctx, + }, nil + } + } else if c.CredentialSource.File != "" { + return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.URL != "" { - return urlCredentialSource{URL: c.CredentialSource.URL, Format: c.CredentialSource.Format, ctx: ctx} + return urlCredentialSource{URL: c.CredentialSource.URL, Format: c.CredentialSource.Format, ctx: ctx}, nil } - return nil + return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } type baseCredentialSource interface { @@ -100,11 +115,12 @@ type tokenSource struct { func (ts tokenSource) Token() (*oauth2.Token, error) { conf := ts.conf - credSource := conf.parse(ts.ctx) - if credSource == nil { - return nil, fmt.Errorf("oauth2/google: unable to parse credential source") + credSource, err := conf.parse(ts.ctx) + if err != nil { + return nil, err } subjectToken, err := credSource.subjectToken() + if err != nil { return nil, err } diff --git a/google/internal/externalaccount/filecredsource_test.go b/google/internal/externalaccount/filecredsource_test.go index 56dd71e..ebd2bb7 100644 --- a/google/internal/externalaccount/filecredsource_test.go +++ b/google/internal/externalaccount/filecredsource_test.go @@ -56,7 +56,12 @@ func TestRetrieveFileSubjectToken(t *testing.T) { tfc.CredentialSource = test.cs t.Run(test.name, func(t *testing.T) { - out, err := tfc.parse(context.Background()).subjectToken() + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() if err != nil { t.Errorf("Method subjectToken() errored.") } else if test.want != out { diff --git a/google/internal/externalaccount/urlcredsource_test.go b/google/internal/externalaccount/urlcredsource_test.go index 592610f..1b78e68 100644 --- a/google/internal/externalaccount/urlcredsource_test.go +++ b/google/internal/externalaccount/urlcredsource_test.go @@ -28,7 +28,12 @@ func TestRetrieveURLSubjectToken_Text(t *testing.T) { tfc := testFileConfig tfc.CredentialSource = cs - out, err := tfc.parse(context.Background()).subjectToken() + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() if err != nil { t.Fatalf("retrieveSubjectToken() failed: %v", err) } @@ -51,7 +56,12 @@ func TestRetrieveURLSubjectToken_Untyped(t *testing.T) { tfc := testFileConfig tfc.CredentialSource = cs - out, err := tfc.parse(context.Background()).subjectToken() + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() if err != nil { t.Fatalf("Failed to retrieve URL subject token: %v", err) } @@ -82,7 +92,12 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) { tfc := testFileConfig tfc.CredentialSource = cs - out, err := tfc.parse(context.Background()).subjectToken() + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() if err != nil { t.Fatalf("%v", err) } From 66670185b0cdf83286f736c2e4cdced4d9cb6170 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Tue, 9 Feb 2021 17:43:57 +0000 Subject: [PATCH 12/20] google: Changes required to get AWS working in manual testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Subject Token needs to be query escaped • Null options need to be omitted (like they are in other languages) Change-Id: I67d1ed3ba96a35283a8928f196bc7e912084d1ab GitHub-Last-Rev: 1aae076a907551ef0625e2859c6a31a4193a9fa0 GitHub-Pull-Request: golang/oauth2#474 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/290513 Reviewed-by: Cody Oss Trust: Cody Oss Trust: Tyler Bui-Palsulich Run-TryBot: Cody Oss TryBot-Result: Go Bot --- google/internal/externalaccount/aws.go | 3 ++- google/internal/externalaccount/aws_test.go | 3 ++- .../internal/externalaccount/basecredentials_test.go | 2 +- google/internal/externalaccount/impersonate_test.go | 2 +- google/internal/externalaccount/sts_exchange.go | 10 ++++++---- google/internal/externalaccount/sts_exchange_test.go | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go index 3725a0f..2f078f7 100644 --- a/google/internal/externalaccount/aws.go +++ b/google/internal/externalaccount/aws.go @@ -16,6 +16,7 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "path" "sort" @@ -334,7 +335,7 @@ func (cs awsCredentialSource) subjectToken() (string, error) { if err != nil { return "", err } - return string(result), nil + return url.QueryEscape(string(result)), nil } func (cs *awsCredentialSource) getRegion() (string, error) { diff --git a/google/internal/externalaccount/aws_test.go b/google/internal/externalaccount/aws_test.go index 1a83a7b..95ff9ce 100644 --- a/google/internal/externalaccount/aws_test.go +++ b/google/internal/externalaccount/aws_test.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "net/http/httptest" + neturl "net/url" "reflect" "strings" "testing" @@ -527,7 +528,7 @@ func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, security }) str, _ := json.Marshal(result) - return string(str) + return neturl.QueryEscape(string(str)) } func TestAwsCredential_BasicRequest(t *testing.T) { diff --git a/google/internal/externalaccount/basecredentials_test.go b/google/internal/externalaccount/basecredentials_test.go index eb60899..78a1137 100644 --- a/google/internal/externalaccount/basecredentials_test.go +++ b/google/internal/externalaccount/basecredentials_test.go @@ -29,7 +29,7 @@ var testConfig = Config{ } var ( - baseCredsRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" + baseCredsRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" baseCredsResponseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}` correctAT = "Sample.Access.Token" expiry int64 = 234852 diff --git a/google/internal/externalaccount/impersonate_test.go b/google/internal/externalaccount/impersonate_test.go index a2d8978..197fe3c 100644 --- a/google/internal/externalaccount/impersonate_test.go +++ b/google/internal/externalaccount/impersonate_test.go @@ -23,7 +23,7 @@ var testImpersonateConfig = Config{ } var ( - baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" + baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}` ) diff --git a/google/internal/externalaccount/sts_exchange.go b/google/internal/externalaccount/sts_exchange.go index c7d85a3..1a1c9b4 100644 --- a/google/internal/externalaccount/sts_exchange.go +++ b/google/internal/externalaccount/sts_exchange.go @@ -32,11 +32,13 @@ func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchan data.Set("subject_token_type", request.SubjectTokenType) data.Set("subject_token", request.SubjectToken) data.Set("scope", strings.Join(request.Scope, " ")) - opts, err := json.Marshal(options) - if err != nil { - return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err) + if options != nil { + opts, err := json.Marshal(options) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err) + } + data.Set("options", string(opts)) } - data.Set("options", string(opts)) authentication.InjectAuthentication(data, headers) encodedData := data.Encode() diff --git a/google/internal/externalaccount/sts_exchange_test.go b/google/internal/externalaccount/sts_exchange_test.go index bd4034a..16c86a3 100644 --- a/google/internal/externalaccount/sts_exchange_test.go +++ b/google/internal/externalaccount/sts_exchange_test.go @@ -35,7 +35,7 @@ var tokenRequest = STSTokenExchangeRequest{ SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", } -var requestbody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" +var requestbody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" var responseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}` var expectedToken = STSTokenExchangeResponse{ AccessToken: "Sample.Access.Token", From 16ff1888fd2ef5c4116f728723d814a74b0f6ac0 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Fri, 12 Feb 2021 20:04:54 +0000 Subject: [PATCH 13/20] google: manual testing fixes I found some errors while manually testing service account impersonation on Azure. This PR includes the fixes that I made. Change-Id: Ia2b194be6c9a7c843e615f9789c8f8203bcbc151 GitHub-Last-Rev: 56907163634bb65457c12320b4081e919cfbbcb2 GitHub-Pull-Request: golang/oauth2#475 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/291209 Reviewed-by: Cody Oss Trust: Cody Oss Trust: Tyler Bui-Palsulich Run-TryBot: Cody Oss TryBot-Result: Go Bot --- google/internal/externalaccount/basecredentials.go | 2 +- google/internal/externalaccount/sts_exchange.go | 8 ++++++-- google/internal/externalaccount/urlcredsource.go | 11 +++++++---- google/internal/externalaccount/urlcredsource_test.go | 8 ++++++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 57a5870..2eb5c8e 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -96,7 +96,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { } else if c.CredentialSource.File != "" { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.URL != "" { - return urlCredentialSource{URL: c.CredentialSource.URL, Format: c.CredentialSource.Format, ctx: ctx}, nil + return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil } return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } diff --git a/google/internal/externalaccount/sts_exchange.go b/google/internal/externalaccount/sts_exchange.go index 1a1c9b4..fbb477d 100644 --- a/google/internal/externalaccount/sts_exchange.go +++ b/google/internal/externalaccount/sts_exchange.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "net/url" "strconv" @@ -63,9 +64,12 @@ func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchan } defer resp.Body.Close() - bodyJson := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)) + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) + } var stsResp STSTokenExchangeResponse - err = bodyJson.Decode(&stsResp) + err = json.Unmarshal(body, &stsResp) if err != nil { return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err) diff --git a/google/internal/externalaccount/urlcredsource.go b/google/internal/externalaccount/urlcredsource.go index b0d5d35..91b8f20 100644 --- a/google/internal/externalaccount/urlcredsource.go +++ b/google/internal/externalaccount/urlcredsource.go @@ -39,15 +39,18 @@ func (cs urlCredentialSource) subjectToken() (string, error) { } defer resp.Body.Close() - tokenBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err) } + if c := resp.StatusCode; c < 200 || c > 299 { + return "", fmt.Errorf("oauth2/google: status code %d: %s", c, respBody) + } switch cs.Format.Type { case "json": jsonData := make(map[string]interface{}) - err = json.Unmarshal(tokenBytes, &jsonData) + err = json.Unmarshal(respBody, &jsonData) if err != nil { return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) } @@ -61,9 +64,9 @@ func (cs urlCredentialSource) subjectToken() (string, error) { } return token, nil case "text": - return string(tokenBytes), nil + return string(respBody), nil case "": - return string(tokenBytes), nil + return string(respBody), nil default: return "", errors.New("oauth2/google: invalid credential_source file format type") } diff --git a/google/internal/externalaccount/urlcredsource_test.go b/google/internal/externalaccount/urlcredsource_test.go index 1b78e68..6874f11 100644 --- a/google/internal/externalaccount/urlcredsource_test.go +++ b/google/internal/externalaccount/urlcredsource_test.go @@ -7,6 +7,7 @@ package externalaccount import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" @@ -19,11 +20,18 @@ func TestRetrieveURLSubjectToken_Text(t *testing.T) { if r.Method != "GET" { t.Errorf("Unexpected request method, %v is found", r.Method) } + fmt.Println(r.Header) + if r.Header.Get("Metadata") != "True" { + t.Errorf("Metadata header not properly included.") + } w.Write([]byte("testTokenValue")) })) + heads := make(map[string]string) + heads["Metadata"] = "True" cs := CredentialSource{ URL: ts.URL, Format: format{Type: fileTypeText}, + Headers: heads, } tfc := testFileConfig tfc.CredentialSource = cs From f1459370232a5048486186a51ddd920910da9d54 Mon Sep 17 00:00:00 2001 From: Bassam Ojeil Date: Thu, 18 Feb 2021 19:07:24 +0000 Subject: [PATCH 14/20] google: update documentation for workload identity federation Document using workload identity federation from non-Google Cloud platforms to access Google Cloud resources. This covers federation from AWS, Azure and OIDC providers via Application Default Credentials. Change-Id: I77ee7f6aac5a75d095304f07f3004ec3fb7b9613 GitHub-Last-Rev: 07c9dd02716519190b63275be2fb3cf1c4bb6b1d GitHub-Pull-Request: golang/oauth2#478 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/293751 Reviewed-by: Cody Oss Trust: Cody Oss Trust: Tyler Bui-Palsulich Run-TryBot: Cody Oss TryBot-Result: Go Bot --- google/default.go | 14 ++++++++++++-- google/doc.go | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/google/default.go b/google/default.go index ad2c092..ae39131 100644 --- a/google/default.go +++ b/google/default.go @@ -21,6 +21,10 @@ import ( // Credentials holds Google credentials, including "Application Default Credentials". // For more details, see: // https://developers.google.com/accounts/docs/application-default-credentials +// Credentials from external accounts (workload identity federation) are used to +// identify a particular application from an on-prem or non-Google Cloud platform +// including Amazon Web Services (AWS), Microsoft Azure or any identity provider +// that supports OpenID Connect (OIDC). type Credentials struct { ProjectID string // may be empty TokenSource oauth2.TokenSource @@ -65,6 +69,10 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc // // 1. A JSON file whose path is specified by the // GOOGLE_APPLICATION_CREDENTIALS environment variable. +// For workload identity federation, refer to +// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation on +// how to generate the JSON configuration file for on-prem/non-Google cloud +// platforms. // 2. A JSON file in a location known to the gcloud command-line tool. // On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. // On other systems, $HOME/.config/gcloud/application_default_credentials.json. @@ -119,8 +127,10 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials // CredentialsFromJSON obtains Google credentials from a JSON value. The JSON can // represent either a Google Developers Console client_credentials.json file (as in -// ConfigFromJSON) or a Google Developers service account key file (as in -// JWTConfigFromJSON). +// ConfigFromJSON), a Google Developers service account key file (as in +// JWTConfigFromJSON) or the JSON configuration file for workload identity federation +// in non-Google cloud platforms (see +// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation). func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { var f credentialsFile if err := json.Unmarshal(jsonData, &f); err != nil { diff --git a/google/doc.go b/google/doc.go index 73be629..b241c72 100644 --- a/google/doc.go +++ b/google/doc.go @@ -4,13 +4,16 @@ // Package google provides support for making OAuth2 authorized and authenticated // HTTP requests to Google APIs. It supports the Web server flow, client-side -// credentials, service accounts, Google Compute Engine service accounts, and Google -// App Engine service accounts. +// credentials, service accounts, Google Compute Engine service accounts, Google +// App Engine service accounts and workload identity federation from non-Google +// cloud platforms. // // A brief overview of the package follows. For more information, please read // https://developers.google.com/accounts/docs/OAuth2 // and // https://developers.google.com/accounts/docs/application-default-credentials. +// For more information on using workload identity federation, refer to +// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation. // // OAuth2 Configs // @@ -19,6 +22,35 @@ // the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or // create an http.Client. // +// Workload Identity Federation +// +// Using workload identity federation, your application can access Google Cloud +// resources from Amazon Web Services (AWS), Microsoft Azure or any identity +// provider that supports OpenID Connect (OIDC). +// Traditionally, applications running outside Google Cloud have used service +// account keys to access Google Cloud resources. Using identity federation, +// you can allow your workload to impersonate a service account. +// This lets you access Google Cloud resources directly, eliminating the +// maintenance and security burden associated with service account keys. +// +// Follow the detailed instructions on how to configure Workload Identity Federation +// in various platforms: +// +// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/access-resources-aws +// Microsoft Azure: https://cloud.google.com/iam/docs/access-resources-azure +// OIDC identity provider: https://cloud.google.com/iam/docs/access-resources-oidc +// +// For OIDC providers, the library can retrieve OIDC tokens either from a +// local file location (file-sourced credentials) or from a local server +// (URL-sourced credentials). +// For file-sourced credentials, a background process needs to be continuously +// refreshing the file location with a new OIDC token prior to expiration. +// For tokens with one hour lifetimes, the token needs to be updated in the file +// every hour. The token can be stored directly as plain text or in JSON format. +// For URL-sourced credentials, a local server needs to host a GET endpoint to +// return the OIDC token. The response can be in plain text or JSON. +// Additional required request headers can also be specified. +// // // Credentials // @@ -29,6 +61,13 @@ // FindDefaultCredentials looks in some well-known places for a credentials file, and // will call AppEngineTokenSource or ComputeTokenSource as needed. // +// Application Default Credentials also support workload identity federation to +// access Google Cloud resources from non-Google Cloud platforms including Amazon +// Web Services (AWS), Microsoft Azure or any identity provider that supports +// OpenID Connect (OIDC). Workload identity federation is recommended for +// non-Google Cloud environments as it avoids the need to download, manage and +// store service account private keys locally. +// // DefaultClient and DefaultTokenSource are convenience methods. They first call FindDefaultCredentials, // then use the credentials to construct an http.Client or an oauth2.TokenSource. // From ba52d332ba9961c5b5c1e8fee42614c0f5c2c076 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Thu, 18 Feb 2021 20:18:54 +0000 Subject: [PATCH 15/20] google: unexport private structs and funcs These structs and funcs cannot be used by the end consumer. Unexporting them helps cleans up our documentation Change-Id: I2eadb69e87de912ac39f53e83cd9bdfe76a15e3e GitHub-Last-Rev: 60b58eef7558b7e1ccf7a07794668b0b25e99d9e GitHub-Pull-Request: golang/oauth2#479 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/293752 Reviewed-by: Cody Oss Trust: Cody Oss Trust: Tyler Bui-Palsulich Run-TryBot: Cody Oss TryBot-Result: Go Bot --- google/internal/externalaccount/basecredentials.go | 6 +++--- google/internal/externalaccount/clientauth.go | 6 +++--- google/internal/externalaccount/clientauth_test.go | 4 ++-- google/internal/externalaccount/sts_exchange.go | 14 +++++++------- .../internal/externalaccount/sts_exchange_test.go | 14 +++++++------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 2eb5c8e..d0ca23c 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -124,7 +124,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) { if err != nil { return nil, err } - stsRequest := STSTokenExchangeRequest{ + stsRequest := stsTokenExchangeRequest{ GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", Audience: conf.Audience, Scope: conf.Scopes, @@ -134,12 +134,12 @@ func (ts tokenSource) Token() (*oauth2.Token, error) { } header := make(http.Header) header.Add("Content-Type", "application/x-www-form-urlencoded") - clientAuth := ClientAuthentication{ + clientAuth := clientAuthentication{ AuthStyle: oauth2.AuthStyleInHeader, ClientID: conf.ClientID, ClientSecret: conf.ClientSecret, } - stsResp, err := ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil) + stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil) if err != nil { return nil, err } diff --git a/google/internal/externalaccount/clientauth.go b/google/internal/externalaccount/clientauth.go index 0464724..feccf8b 100644 --- a/google/internal/externalaccount/clientauth.go +++ b/google/internal/externalaccount/clientauth.go @@ -11,15 +11,15 @@ import ( "net/url" ) -// ClientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1. -type ClientAuthentication struct { +// clientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1. +type clientAuthentication struct { // AuthStyle can be either basic or request-body AuthStyle oauth2.AuthStyle ClientID string ClientSecret string } -func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) { +func (c *clientAuthentication) InjectAuthentication(values url.Values, headers http.Header) { if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil { return } diff --git a/google/internal/externalaccount/clientauth_test.go b/google/internal/externalaccount/clientauth_test.go index acacbeb..38633e3 100644 --- a/google/internal/externalaccount/clientauth_test.go +++ b/google/internal/externalaccount/clientauth_test.go @@ -37,7 +37,7 @@ func TestClientAuthentication_InjectHeaderAuthentication(t *testing.T) { "Content-Type": ContentType, } - headerAuthentication := ClientAuthentication{ + headerAuthentication := clientAuthentication{ AuthStyle: oauth2.AuthStyleInHeader, ClientID: clientID, ClientSecret: clientSecret, @@ -79,7 +79,7 @@ func TestClientAuthentication_ParamsAuthentication(t *testing.T) { headerP := http.Header{ "Content-Type": ContentType, } - paramsAuthentication := ClientAuthentication{ + paramsAuthentication := clientAuthentication{ AuthStyle: oauth2.AuthStyleInParams, ClientID: clientID, ClientSecret: clientSecret, diff --git a/google/internal/externalaccount/sts_exchange.go b/google/internal/externalaccount/sts_exchange.go index fbb477d..a8a704b 100644 --- a/google/internal/externalaccount/sts_exchange.go +++ b/google/internal/externalaccount/sts_exchange.go @@ -18,11 +18,11 @@ import ( "golang.org/x/oauth2" ) -// ExchangeToken performs an oauth2 token exchange with the provided endpoint. +// exchangeToken performs an oauth2 token exchange with the provided endpoint. // The first 4 fields are all mandatory. headers can be used to pass additional // headers beyond the bare minimum required by the token exchange. options can // be used to pass additional JSON-structured options to the remote server. -func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]interface{}) (*STSTokenExchangeResponse, error) { +func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchangeRequest, authentication clientAuthentication, headers http.Header, options map[string]interface{}) (*stsTokenExchangeResponse, error) { client := oauth2.NewClient(ctx, nil) @@ -68,7 +68,7 @@ func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchan if c := resp.StatusCode; c < 200 || c > 299 { return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) } - var stsResp STSTokenExchangeResponse + var stsResp stsTokenExchangeResponse err = json.Unmarshal(body, &stsResp) if err != nil { return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err) @@ -78,8 +78,8 @@ func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchan return &stsResp, nil } -// STSTokenExchangeRequest contains fields necessary to make an oauth2 token exchange. -type STSTokenExchangeRequest struct { +// stsTokenExchangeRequest contains fields necessary to make an oauth2 token exchange. +type stsTokenExchangeRequest struct { ActingParty struct { ActorToken string ActorTokenType string @@ -93,8 +93,8 @@ type STSTokenExchangeRequest struct { SubjectTokenType string } -// STSTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange. -type STSTokenExchangeResponse struct { +// stsTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange. +type stsTokenExchangeResponse struct { AccessToken string `json:"access_token"` IssuedTokenType string `json:"issued_token_type"` TokenType string `json:"token_type"` diff --git a/google/internal/externalaccount/sts_exchange_test.go b/google/internal/externalaccount/sts_exchange_test.go index 16c86a3..3d498c6 100644 --- a/google/internal/externalaccount/sts_exchange_test.go +++ b/google/internal/externalaccount/sts_exchange_test.go @@ -15,13 +15,13 @@ import ( "testing" ) -var auth = ClientAuthentication{ +var auth = clientAuthentication{ AuthStyle: oauth2.AuthStyleInHeader, ClientID: clientID, ClientSecret: clientSecret, } -var tokenRequest = STSTokenExchangeRequest{ +var tokenRequest = stsTokenExchangeRequest{ ActingParty: struct { ActorToken string ActorTokenType string @@ -37,7 +37,7 @@ var tokenRequest = STSTokenExchangeRequest{ var requestbody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" var responseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}` -var expectedToken = STSTokenExchangeResponse{ +var expectedToken = stsTokenExchangeResponse{ AccessToken: "Sample.Access.Token", IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", TokenType: "Bearer", @@ -75,9 +75,9 @@ func TestExchangeToken(t *testing.T) { headers := http.Header{} headers.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil) + resp, err := exchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil) if err != nil { - t.Fatalf("ExchangeToken failed with error: %v", err) + t.Fatalf("exchangeToken failed with error: %v", err) } if expectedToken != *resp { @@ -95,7 +95,7 @@ func TestExchangeToken_Err(t *testing.T) { headers := http.Header{} headers.Add("Content-Type", "application/x-www-form-urlencoded") - _, err := ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil) + _, err := exchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil) if err == nil { t.Errorf("Expected handled error; instead got nil.") } @@ -179,5 +179,5 @@ func TestExchangeToken_Opts(t *testing.T) { inputOpts := make(map[string]interface{}) inputOpts["one"] = firstOption inputOpts["two"] = secondOption - ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, inputOpts) + exchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, inputOpts) } From 9bb904979d93d76cf7000f77c4a1d7bc06c7a708 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 19 Feb 2021 18:54:49 -0500 Subject: [PATCH 16/20] all: go fmt ./... Make all our package sources use Go 1.17 gofmt format (adding //go:build lines). Not strictly necessary but will avoid spurious changes as files are edited. Part of //go:build change (#41184). See https://golang.org/design/draft-gobuild Change-Id: I22803ea9e936fbb08984a64155302f47e181de27 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/294420 Trust: Russ Cox Trust: Cody Oss Run-TryBot: Russ Cox TryBot-Result: Go Bot Reviewed-by: Cody Oss --- google/appengine_gen1.go | 1 + google/appengine_gen2_flex.go | 1 + google/internal/externalaccount/basecredentials.go | 2 +- google/internal/externalaccount/urlcredsource_test.go | 4 ++-- internal/client_appengine.go | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/google/appengine_gen1.go b/google/appengine_gen1.go index 83dacac..16c6c6b 100644 --- a/google/appengine_gen1.go +++ b/google/appengine_gen1.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build appengine // +build appengine // This file applies to App Engine first generation runtimes (<= Go 1.9). diff --git a/google/appengine_gen2_flex.go b/google/appengine_gen2_flex.go index 04c2c22..a7e27b3 100644 --- a/google/appengine_gen2_flex.go +++ b/google/appengine_gen2_flex.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !appengine // +build !appengine // This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible. diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index d0ca23c..1b87c09 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -45,7 +45,7 @@ func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { ctx: ctx, url: c.ServiceAccountImpersonationURL, scopes: scopes, - ts: oauth2.ReuseTokenSource(nil, ts), + ts: oauth2.ReuseTokenSource(nil, ts), } return oauth2.ReuseTokenSource(nil, imp) } diff --git a/google/internal/externalaccount/urlcredsource_test.go b/google/internal/externalaccount/urlcredsource_test.go index 6874f11..8ade2a2 100644 --- a/google/internal/externalaccount/urlcredsource_test.go +++ b/google/internal/externalaccount/urlcredsource_test.go @@ -29,8 +29,8 @@ func TestRetrieveURLSubjectToken_Text(t *testing.T) { heads := make(map[string]string) heads["Metadata"] = "True" cs := CredentialSource{ - URL: ts.URL, - Format: format{Type: fileTypeText}, + URL: ts.URL, + Format: format{Type: fileTypeText}, Headers: heads, } tfc := testFileConfig diff --git a/internal/client_appengine.go b/internal/client_appengine.go index 7434871..e1755d1 100644 --- a/internal/client_appengine.go +++ b/internal/client_appengine.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build appengine // +build appengine package internal From 5366d9dc1934b408dc3682b22f69674507481b04 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Thu, 11 Mar 2021 15:33:11 +0000 Subject: [PATCH 17/20] google: Make sure time is always in UTC If times are stored in different time zones, then we occasionally get heisenbugs about expired tokens Change-Id: I0c117977688d8d6c7b12b211092e5040a41a1f46 GitHub-Last-Rev: 3ff51b34f58e0bde7946af59e1215b7d3db65a95 GitHub-Pull-Request: golang/oauth2#482 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/300929 Reviewed-by: Tyler Bui-Palsulich Trust: Tyler Bui-Palsulich Trust: Cody Oss Run-TryBot: Tyler Bui-Palsulich TryBot-Result: Go Bot --- google/internal/externalaccount/basecredentials.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 1b87c09..1a6e93c 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -14,7 +14,9 @@ import ( ) // now aliases time.Now for testing -var now = time.Now +var now = func() time.Time { + return time.Now().UTC() +} // Config stores the configuration for fetching tokens with external credentials. type Config struct { From cd4f82c27b84ec2516d3c38a5b5acaf8823e2702 Mon Sep 17 00:00:00 2001 From: Shaba Abhiram Date: Fri, 20 Nov 2020 05:37:50 +0000 Subject: [PATCH 18/20] endpoints: add Zoom endpoint See #424 Change-Id: I1fa6230082261f60a8e41387ad2bf481df0a1fce GitHub-Last-Rev: 6fba3dd9d1d5e052911338058ea908b8238e8f02 GitHub-Pull-Request: golang/oauth2#453 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/271826 Reviewed-by: Brad Fitzpatrick Trust: Brad Fitzpatrick Trust: Tobias Klauser Run-TryBot: Brad Fitzpatrick TryBot-Result: Go Bot --- endpoints/endpoints.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/endpoints/endpoints.go b/endpoints/endpoints.go index 811e101..3b7f32a 100644 --- a/endpoints/endpoints.go +++ b/endpoints/endpoints.go @@ -197,6 +197,12 @@ var Yandex = oauth2.Endpoint{ TokenURL: "https://oauth.yandex.com/token", } +// Zoom is the endpoint for Zoom. +var Zoom = oauth2.Endpoint{ + AuthURL: "https://zoom.us/oauth/authorize", + TokenURL: "https://zoom.us/oauth/token", +} + // AzureAD returns a new oauth2.Endpoint for the given tenant at Azure Active Directory. // If tenant is empty, it uses the tenant called `common`. // From 22b0adad7558c54bf49787666d8773cae1dd3e77 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Fri, 19 Mar 2021 19:10:01 +0000 Subject: [PATCH 19/20] authhandler: Add support for 3-legged-OAuth Added authhandler.go, which implements a TokenSource to support "three-legged OAuth 2.0" via a custom AuthorizationHandler. Added example_test.go with a sample command line implementation for AuthorizationHandler. This patch adds support for 3-legged-OAuth flow using an OAuth Client ID file downloaded from Google Cloud Console. Change-Id: Iefe54494d6f3ee326a6b1b2a81a7d5d1a7ba3331 GitHub-Last-Rev: 48fc0367c2092baf97b8e09f03a94e7fe1ecd890 GitHub-Pull-Request: golang/oauth2#419 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/232238 Reviewed-by: Tyler Bui-Palsulich Reviewed-by: Shin Fan Reviewed-by: Cody Oss Trust: Shin Fan Trust: Cody Oss --- authhandler/authhandler.go | 56 +++++++++++++++++++ authhandler/authhandler_test.go | 99 +++++++++++++++++++++++++++++++++ authhandler/example_test.go | 79 ++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 authhandler/authhandler.go create mode 100644 authhandler/authhandler_test.go create mode 100644 authhandler/example_test.go diff --git a/authhandler/authhandler.go b/authhandler/authhandler.go new file mode 100644 index 0000000..69967cf --- /dev/null +++ b/authhandler/authhandler.go @@ -0,0 +1,56 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package authhandler implements a TokenSource to support +// "three-legged OAuth 2.0" via a custom AuthorizationHandler. +package authhandler + +import ( + "context" + "errors" + + "golang.org/x/oauth2" +) + +// AuthorizationHandler is a 3-legged-OAuth helper that prompts +// the user for OAuth consent at the specified auth code URL +// and returns an auth code and state upon approval. +type AuthorizationHandler func(authCodeURL string) (code string, state string, err error) + +// TokenSource returns an oauth2.TokenSource that fetches access tokens +// using 3-legged-OAuth flow. +// +// The provided context.Context is used for oauth2 Exchange operation. +// +// The provided oauth2.Config should be a full configuration containing AuthURL, +// TokenURL, and Scope. +// +// An environment-specific AuthorizationHandler is used to obtain user consent. +// +// Per the OAuth protocol, a unique "state" string should be specified here. +// This token source will verify that the "state" is identical in the request +// and response before exchanging the auth code for OAuth token to prevent CSRF +// attacks. +func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) +} + +type authHandlerSource struct { + ctx context.Context + config *oauth2.Config + authHandler AuthorizationHandler + state string +} + +func (source authHandlerSource) Token() (*oauth2.Token, error) { + url := source.config.AuthCodeURL(source.state) + code, state, err := source.authHandler(url) + if err != nil { + return nil, err + } + if state != source.state { + return nil, errors.New("state mismatch in 3-legged-OAuth flow") + } + return source.config.Exchange(source.ctx, code) +} diff --git a/authhandler/authhandler_test.go b/authhandler/authhandler_test.go new file mode 100644 index 0000000..084198f --- /dev/null +++ b/authhandler/authhandler_test.go @@ -0,0 +1,99 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package authhandler + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "golang.org/x/oauth2" +) + +func TestTokenExchange_Success(t *testing.T) { + authhandler := func(authCodeURL string) (string, string, error) { + if authCodeURL == "testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=testState" { + return "testCode", "testState", nil + } + return "", "", fmt.Errorf("invalid authCodeURL: %q", authCodeURL) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + if r.Form.Get("code") == "testCode" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "pubsub", + "token_type": "bearer", + "expires_in": 3600 + }`)) + } + })) + defer ts.Close() + + conf := &oauth2.Config{ + ClientID: "testClientID", + Scopes: []string{"pubsub"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "testAuthCodeURL", + TokenURL: ts.URL, + }, + } + + tok, err := TokenSource(context.Background(), conf, "testState", authhandler).Token() + if err != nil { + t.Fatal(err) + } + if !tok.Valid() { + t.Errorf("got invalid token: %v", tok) + } + if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want { + t.Errorf("access token = %q; want %q", got, want) + } + if got, want := tok.TokenType, "bearer"; got != want { + t.Errorf("token type = %q; want %q", got, want) + } + if got := tok.Expiry.IsZero(); got { + t.Errorf("token expiry is zero = %v, want false", got) + } + scope := tok.Extra("scope") + if got, want := scope, "pubsub"; got != want { + t.Errorf("scope = %q; want %q", got, want) + } +} + +func TestTokenExchange_StateMismatch(t *testing.T) { + authhandler := func(authCodeURL string) (string, string, error) { + return "testCode", "testStateMismatch", nil + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "pubsub", + "token_type": "bearer", + "expires_in": 3600 + }`)) + })) + defer ts.Close() + + conf := &oauth2.Config{ + ClientID: "testClientID", + Scopes: []string{"pubsub"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "testAuthCodeURL", + TokenURL: ts.URL, + }, + } + + _, err := TokenSource(context.Background(), conf, "testState", authhandler).Token() + if want_err := "state mismatch in 3-legged-OAuth flow"; err == nil || err.Error() != want_err { + t.Errorf("err = %q; want %q", err, want_err) + } +} diff --git a/authhandler/example_test.go b/authhandler/example_test.go new file mode 100644 index 0000000..a62b4e1 --- /dev/null +++ b/authhandler/example_test.go @@ -0,0 +1,79 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package authhandler_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/authhandler" +) + +// CmdAuthorizationHandler returns a command line auth handler that prints +// the auth URL to the console and prompts the user to authorize in the +// browser and paste the auth code back via stdin. +// +// Per the OAuth protocol, a unique "state" string should be specified here. +// The authhandler token source will verify that the "state" is identical in +// the request and response before exchanging the auth code for OAuth token to +// prevent CSRF attacks. +// +// For convenience, this handler returns a pre-configured state instead of +// asking the user to additionally paste the state from the auth response. +// In order for this to work, the state configured here must match the state +// used in authCodeURL. +func CmdAuthorizationHandler(state string) authhandler.AuthorizationHandler { + return func(authCodeURL string) (string, string, error) { + fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) + fmt.Println("Enter authorization code:") + var code string + fmt.Scanln(&code) + return code, state, nil + } +} + +func Example() { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "pubsub", + "token_type": "bearer", + "expires_in": 3600 + }`)) + })) + defer ts.Close() + + ctx := context.Background() + conf := &oauth2.Config{ + ClientID: "testClientID", + Scopes: []string{"pubsub"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "testAuthCodeURL", + TokenURL: ts.URL, + }, + } + state := "unique_state" + + token, err := authhandler.TokenSource(ctx, conf, state, CmdAuthorizationHandler(state)).Token() + + if err != nil { + fmt.Println(err) + } + + fmt.Printf("AccessToken: %s", token.AccessToken) + + // Output: + // Go to the following link in your browser: + // + // testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=unique_state + // + // Enter authorization code: + // AccessToken: 90d64460d14870c08c81352a05dedd3465940a7c +} From 2e8d9340160224d36fd555eaf8837240a7e239a7 Mon Sep 17 00:00:00 2001 From: Joel Ferrier Date: Thu, 1 Apr 2021 16:22:52 -0700 Subject: [PATCH 20/20] google: Use bytes.Buffer for go 1.9 compatability Update test cases to use const test data file references. Change-Id: Ic08b6de5a84db7b2ae2c649ee676c5a2c13f4227 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/306749 Reviewed-by: Cody Oss Trust: Cody Oss Trust: Tyler Bui-Palsulich Run-TryBot: Cody Oss TryBot-Result: Go Bot --- google/internal/externalaccount/aws.go | 3 ++- google/internal/externalaccount/basecredentials_test.go | 7 ++++++- google/internal/externalaccount/filecredsource_test.go | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go index 2f078f7..fbcefb4 100644 --- a/google/internal/externalaccount/aws.go +++ b/google/internal/externalaccount/aws.go @@ -5,6 +5,7 @@ package externalaccount import ( + "bytes" "context" "crypto/hmac" "crypto/sha256" @@ -127,7 +128,7 @@ func canonicalHeaders(req *http.Request) (string, string) { } sort.Strings(headers) - var fullHeaders strings.Builder + var fullHeaders bytes.Buffer for _, header := range headers { headerValue := strings.Join(lowerCaseHeaders[header], ",") fullHeaders.WriteString(header) diff --git a/google/internal/externalaccount/basecredentials_test.go b/google/internal/externalaccount/basecredentials_test.go index 78a1137..1ebb227 100644 --- a/google/internal/externalaccount/basecredentials_test.go +++ b/google/internal/externalaccount/basecredentials_test.go @@ -13,8 +13,13 @@ import ( "time" ) +const ( + textBaseCredPath = "testdata/3pi_cred.txt" + jsonBaseCredPath = "testdata/3pi_cred.json" +) + var testBaseCredSource = CredentialSource{ - File: "./testdata/3pi_cred.txt", + File: textBaseCredPath, Format: format{Type: fileTypeText}, } diff --git a/google/internal/externalaccount/filecredsource_test.go b/google/internal/externalaccount/filecredsource_test.go index ebd2bb7..553830d 100644 --- a/google/internal/externalaccount/filecredsource_test.go +++ b/google/internal/externalaccount/filecredsource_test.go @@ -28,14 +28,14 @@ func TestRetrieveFileSubjectToken(t *testing.T) { { name: "UntypedFileSource", cs: CredentialSource{ - File: "./testdata/3pi_cred.txt", + File: textBaseCredPath, }, want: "street123", }, { name: "TextFileSource", cs: CredentialSource{ - File: "./testdata/3pi_cred.txt", + File: textBaseCredPath, Format: format{Type: fileTypeText}, }, want: "street123", @@ -43,7 +43,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) { { name: "JSONFileSource", cs: CredentialSource{ - File: "./testdata/3pi_cred.json", + File: jsonBaseCredPath, Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, }, want: "321road",