Compare commits

...

162 Commits

Author SHA1 Message Date
renovate[bot]
2e450b6f79 Update golang.org/x/oauth2 commit hash to 5d25da1 (#376)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-09-05 15:28:03 +09:00
renovate[bot]
ec38934b7e Update golang.org/x/crypto commit hash to 5c72a88 (#328)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-09-03 06:54:16 +09:00
Hidetake Iwata
9dfeb2f735 go mod tidy 2020-09-03 06:45:35 +09:00
Hidetake Iwata
c051d4e51a Refactor: close channel in writer goroutine (#375) 2020-09-03 06:44:46 +09:00
Hidetake Iwata
88f03655ea Add Chocolatey installation (#374) 2020-09-02 12:05:56 +09:00
renovate[bot]
fae53fd634 Update module k8s.io/client-go to v0.19.0 (#369)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-09-02 11:16:33 +09:00
renovate[bot]
43f1c44ea4 Update module int128/oauth2cli to v1.13.0 (#372)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-09-01 22:25:40 +09:00
renovate[bot]
23cbc28649 Update module k8s.io/client-go to v0.18.8 (#356)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-08-16 16:26:11 +09:00
Hidetake Iwata
003cb6c77c Bump the version of golangci-lint-action to v2 (#364) 2020-08-16 16:25:36 +09:00
Hidetake Iwata
c039323693 Add golangci-lint workflow (#363) 2020-08-16 16:10:16 +09:00
Hidetake Iwata
5f0e1750bd Run system test when go.mod/sum is changed (#360) 2020-08-15 00:27:21 +09:00
Hidetake Iwata
e0e3287feb Bump the version of golangci-lint to v1.30.0 (#359) 2020-08-15 00:23:12 +09:00
renovate[bot]
6dad6b3751 Update cimg/go Docker tag to v1.14.7 (#353)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-08-14 23:46:14 +09:00
Hidetake Iwata
c095bdabc1 Refactor GitHub actions workflow (#358)
* Use preinstalled kind

* Use the latest Go

* Bump the version of actions

* Run system test only if related source is changed
2020-08-14 23:36:11 +09:00
Hidetake Iwata
daf563ea9a Refactor: extract usage.md from README.md (#357)
* Refactor docs

* Update usage.md
2020-08-14 23:22:38 +09:00
renovate[bot]
cf73e9a495 Update cimg/go Docker tag to v1.14.6 (#340)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-08-02 14:29:59 +09:00
Hidetake Iwata
488e8c62ec Determine go version from .circleci/config.yml (#351) 2020-08-02 12:08:26 +09:00
Hidetake Iwata
58d170fa65 Add --open-url-after-authentication option (#350)
* Add --open-url-after-authentication option

* Add integration test for --open-url-after-authentication
2020-08-01 10:38:33 +09:00
Hidetake Iwata
c488888834 Refactor: pull up packages of domain (#349) 2020-07-30 09:37:10 +09:00
Hidetake Iwata
2cd741735e Refactor: move templates.AuthCodeBrowserSuccessHTML to authcode (#348) 2020-07-30 09:29:49 +09:00
Hidetake Iwata
dbb684f10e Refactor: use oidc.TokenSet in adaptors (#347) 2020-07-30 09:26:21 +09:00
Hidetake Iwata
a0e81e762c Refactor: split authentication package into methods (#346) 2020-07-30 00:31:23 +09:00
Hidetake Iwata
c4ce1629e2 Refactor: regenerate with the latest mockgen (#345) 2020-07-30 00:04:56 +09:00
renovate[bot]
e965114ecb Update module golang/mock to v1.4.4 (#344)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-07-29 23:54:50 +09:00
Hidetake Iwata
804a245fde Refactor: rename to AuthCodeBrowser (#342) 2020-07-26 18:49:22 +09:00
Hidetake Iwata
ffaa26cba8 Merge pull request #341 from int128/refactor-flags
Refactor flags
2020-07-26 18:18:45 +09:00
Hidetake Iwata
923a4251f1 Change messages in standalone mode 2020-07-26 18:11:39 +09:00
Hidetake Iwata
98b84d87e0 Refactor: change options description 2020-07-26 15:39:09 +09:00
Hidetake Iwata
1ae2008e28 Refactor: extract tlsOptions 2020-07-26 15:39:09 +09:00
Hidetake Iwata
8197b5b35a Refactor: extract authentication.go 2020-07-26 12:00:15 +09:00
Hidetake Iwata
7196c64bec Refactor: rename to addFlags() 2020-07-26 12:00:15 +09:00
Hidetake Iwata
d8419f0dd3 go mod tidy 2020-07-26 11:59:57 +09:00
renovate[bot]
5f61d298f5 Update alpine Docker tag to v3.12 (#330)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-07-24 17:02:07 +09:00
renovate[bot]
7310b3c271 Update golang.org/x/sync commit hash to 6e8e738 (#329)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-07-24 16:40:22 +09:00
Hidetake Iwata
ba4d010f86 Enable go mod tidy on Renovate (#339) 2020-07-24 15:00:50 +09:00
Hidetake Iwata
41d8e74ba9 Reduce time of CircleCI macOS executor (#338) 2020-07-23 17:37:55 +09:00
renovate[bot]
23a1efb4f1 Update module k8s.io/client-go to v0.18.6 (#333)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-07-23 08:29:03 +09:00
renovate[bot]
3cfeacc861 Update module google/go-cmp to v0.5.1 (#337)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-07-22 14:59:49 +09:00
renovate[bot]
509f29637b Add renovate.json (#322)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-07-21 10:33:31 +09:00
Hidetake Iwata
4f96435e97 Show debug logs in authentication (#325) 2020-07-14 09:50:02 +09:00
Hidetake Iwata
22005fb715 go mod tidy 2020-07-14 09:14:51 +09:00
dependabot-preview[bot]
8af36b13e4 Build(deps): bump github.com/int128/oauth2cli from 1.11.0 to 1.12.1 (#324)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.11.0 to 1.12.1.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.11.0...v1.12.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-14 07:06:48 +09:00
Hidetake Iwata
f0c399b8fc go mod tidy 2020-07-05 19:50:26 +09:00
dependabot-preview[bot]
17499aac24 Build(deps): bump k8s.io/client-go from 0.18.4 to 0.18.5 (#320)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.18.4 to 0.18.5.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.18.4...v0.18.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-03 09:08:57 +09:00
Hidetake Iwata
d81457995d Add links to use Okta groups claim (#318)
This will close #250.
2020-06-24 10:18:54 +09:00
Hidetake Iwata
2e7b93a31e Add issue templates (#317) 2020-06-24 09:28:52 +09:00
Hidetake Iwata
9aeffbc71e Update README.md 2020-06-24 01:10:25 +09:00
Hidetake Iwata
c2b0c101af Change margin of success page (#316) 2020-06-24 00:53:51 +09:00
Hidetake Iwata
e8161d5a47 go mod tidy 2020-06-23 16:27:05 +09:00
dependabot-preview[bot]
a3946c7f5f Build(deps): bump k8s.io/client-go from 0.18.3 to 0.18.4 (#309)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.18.3 to 0.18.4.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.18.3...v0.18.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-23 16:24:41 +09:00
dependabot-preview[bot]
6b880febdb Build(deps): bump k8s.io/apimachinery from 0.18.3 to 0.18.4 (#310)
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.18.3 to 0.18.4.
- [Release notes](https://github.com/kubernetes/apimachinery/releases)
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.18.3...v0.18.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-23 15:03:51 +09:00
dependabot-preview[bot]
a51c15aec2 Build(deps): bump github.com/google/go-cmp from 0.4.1 to 0.5.0 (#311)
Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.4.1 to 0.5.0.
- [Release notes](https://github.com/google/go-cmp/releases)
- [Commits](https://github.com/google/go-cmp/compare/v0.4.1...v0.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-23 15:01:28 +09:00
Hidetake Iwata
77a6b91be8 Change authentication success page more descriptive (#312)
* Refactor: rename to authcode_browser.go

* Change authentication success page more descriptive
2020-06-23 15:00:58 +09:00
Hidetake Iwata
9e27385c0b Add acceptance test (#315) 2020-06-23 14:47:29 +09:00
Hidetake Iwata
3c50431a09 Refactor: rename to system test (#314) 2020-06-22 16:22:03 +09:00
Hidetake Iwata
e41fdf3dcd Fix error of acceptance test (#313) 2020-06-22 14:33:54 +09:00
Hidetake Iwata
dd93a6537d Update README.md 2020-06-14 16:19:52 +09:00
Hidetake Iwata
822f6c86de Remove redundant information from caveats (#306)
This reflects the change of https://github.com/kubernetes-sigs/krew-index/pull/635
2020-06-12 18:20:01 +09:00
Hidetake Iwata
dd22ccb9c3 Bump version of github.com/spf13/cobra to v1.0.0 (#305) 2020-06-12 14:46:59 +09:00
Hidetake Iwata
f6c4a1257d Update acceptance-test.yaml 2020-06-12 14:35:59 +09:00
Hidetake Iwata
a0c62a9ff1 Move to CircleCI macOS build (#304) 2020-06-12 14:28:32 +09:00
Andreas Lindhé
0aa3e43e62 Remove install instructions for Github releases (#303)
* Remove install instructions for Github releases

Fixes #301

* Update README.md

Co-authored-by: Hidetake Iwata <int128@gmail.com>
2020-06-10 10:37:26 +09:00
Hidetake Iwata
9028199abb Bump kind version to v0.8.1 (#300) 2020-06-09 16:22:42 +09:00
ThomasAlxDmy
c308ccb511 CGO_ENABLE as a make option (#299)
* CGO_ENABLE as a make option

* Update Makefile

* Update Makefile

Co-authored-by: Thomas Dmytryk <tdmytryk@tesla.com>
Co-authored-by: Hidetake Iwata <int128@gmail.com>
2020-05-28 00:50:29 +09:00
Hidetake Iwata
ff1aa97d87 go mod tidy 2020-05-23 00:48:37 +09:00
dependabot-preview[bot]
ca21c6568b Build(deps): bump k8s.io/client-go from 0.18.2 to 0.18.3 (#296)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.18.2 to 0.18.3.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.18.2...v0.18.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-23 00:48:06 +09:00
dependabot-preview[bot]
117a8d35d4 Build(deps): bump github.com/google/go-cmp from 0.4.0 to 0.4.1 (#294)
Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/google/go-cmp/releases)
- [Commits](https://github.com/google/go-cmp/compare/v0.4.0...v0.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-22 21:51:49 +09:00
Hidetake Iwata
5557290105 Bump the version to v1.19.1 2020-05-16 18:25:10 +09:00
Hidetake Iwata
fe85419312 go mod tidy 2020-05-16 18:07:23 +09:00
dependabot-preview[bot]
d8d810bc0d Build(deps): bump k8s.io/client-go from 0.18.1 to 0.18.2 (#277)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.18.1 to 0.18.2.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.18.1...v0.18.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-16 17:49:11 +09:00
dependabot-preview[bot]
170eeb4ed5 Build(deps): bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0 (#288)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.8 to 2.3.0.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.8...v2.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-16 16:50:25 +09:00
Michael Goodwin
01637fbe12 Update brew template to include support for LinuxBrew (#291)
This commit introduces an OS check, and extra variables `baseurl` and `kernel` to determine the right archive to download based on the platform. `baseurl` is used to compose the full URI with `#{version}` and `#{kernel}` ruby variables
2020-05-16 16:28:33 +09:00
Hidetake Iwata
e152e95a9f Refactor: add integration test of PKCE (#293)
* Refactor: add integration test of PKCE

* Refactor: fix name to pkce/Params.IsZero()
2020-05-16 14:29:42 +09:00
Hidetake Iwata
bf8eefd045 Refactor: reduce test cases of integration test (#292) 2020-05-14 16:49:39 +09:00
Hidetake Iwata
e88138c640 Refactor: fix token verifier does not respect clock (#290) 2020-05-13 15:45:14 +09:00
Hidetake Iwata
9ad520ba22 Refactor: inject stdout mock in integration test (#289) 2020-05-13 11:25:59 +09:00
Hidetake Iwata
35e8ecab8d Refactor: replace newCredentialPluginWriterMock (#287)
* Refactor: replace newCredentialPluginWriterMock

* Refactor: remove unused mock and files
2020-05-12 14:36:13 +09:00
Hidetake Iwata
c5621239e8 Refactor: remove unused mock and files 2020-05-12 14:17:19 +09:00
Hidetake Iwata
582ca48092 Refactor: replace newCredentialPluginWriterMock 2020-05-12 14:17:18 +09:00
Hidetake Iwata
da32d2184d Refactor: remove Server.NewTokenResponse() (#286) 2020-05-11 20:41:46 +09:00
Hidetake Iwata
3a9768d6de Refactor: extract httpdriver package (#285) 2020-05-11 12:06:11 +09:00
Hidetake Iwata
16d6fa2fbb Refactor: replace idp mock with oidcserver package (#284) 2020-05-11 11:51:21 +09:00
Hidetake Iwata
175275bf3d Fix to send challenge only if provider supports PKCE (#283) 2020-05-08 22:19:32 +09:00
Hidetake Iwata
4ffc914d0e Bump the version 2020-04-11 17:14:59 +09:00
Hidetake Iwata
fa008bef55 Add linux_arm and linux_arm64 binary to distribution (#275) 2020-04-11 17:14:00 +09:00
Hidetake Iwata
50047417ab Delete snapcraft.yaml 2020-04-11 16:10:48 +09:00
Hidetake Iwata
4c0ebb0284 go mod tidy 2020-04-11 15:57:57 +09:00
dependabot-preview[bot]
c1173accd3 Build(deps): bump k8s.io/client-go from 0.18.0 to 0.18.1 (#273)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.18.0 to 0.18.1.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.18.0...v0.18.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-11 15:57:17 +09:00
Hidetake Iwata
d4addcfc6e Show Go version, GOOS and GOARCH in version command (#272) 2020-04-11 11:39:49 +09:00
Hidetake Iwata
695347e419 Merge pull request #271 from int128/refactor
Refactor integration test
2020-04-10 20:39:18 +09:00
Hidetake Iwata
d5c738697f Refactor: tidy up 2020-04-10 20:32:22 +09:00
Hidetake Iwata
777a60c96c Refactor: rename package 2020-04-10 17:15:02 +09:00
Hidetake Iwata
6f6e0723f1 Refactor: extract integration_test.authCodeFlowConfig 2020-04-10 16:32:02 +09:00
Hidetake Iwata
3fb074a4a8 Refactor: replace ClusterRoleBinding manifest with kubectl command (#270) 2020-04-08 20:38:35 +09:00
Hidetake Iwata
59b5f1bd89 Add --oidc-redirect-url-hostname flag (#269) 2020-04-08 15:25:19 +09:00
Hidetake Iwata
70819843f0 go mod tidy 2020-04-07 10:30:13 +09:00
dependabot-preview[bot]
87c46a24ae Build(deps): bump github.com/int128/oauth2cli from 1.10.0 to 1.11.0 (#268)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.10.0...v1.11.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-07 10:27:30 +09:00
Hidetake Iwata
ac504150b2 Bump k8s.io/apimachinery, k8s.io/client-go to 0.18.0 (#267) 2020-04-06 21:58:53 +09:00
Hidetake Iwata
caeb55f21d Refactor: enable Go module cache (#264) 2020-04-02 20:08:52 +09:00
dependabot-preview[bot]
2ccda6099f Build(deps): bump github.com/spf13/cobra from 0.0.6 to 0.0.7 (#263)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 0.0.6 to 0.0.7.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v0.0.6...0.0.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-31 11:34:19 +09:00
MOZGIII
7f4f72c9e0 Correct the command args at stage 2 instructions (#261) 2020-03-29 18:26:29 +09:00
Hidetake Iwata
bfc1568057 Bump the version 2020-03-26 10:36:06 +09:00
Hidetake Iwata
8758d55bb3 go mod tidy 2020-03-26 10:29:32 +09:00
dependabot-preview[bot]
d9be392f5a Build(deps): bump k8s.io/client-go from 0.17.3 to 0.17.4 (#252)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.17.3 to 0.17.4.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.17.3...v0.17.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-26 10:24:49 +09:00
dependabot-preview[bot]
af840a519c Build(deps): bump github.com/golang/mock from 1.4.0 to 1.4.3 (#253)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.4.0 to 1.4.3.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/v1.4.0...v1.4.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-26 10:24:31 +09:00
Hidetake Iwata
285b3b15a8 Bump to Go 1.14.1 (#258) 2020-03-26 10:13:54 +09:00
Matthew M. Boedicker
123d7c8124 Add --oidc-extra-url-params argument (#255)
* Add --oidc-extra-url-params argument

This accepts a comma-separated list of key-value pairs that will be
added to get token requests as query string parameters.

Closes #254.

* Refactor

- move code setting the extra params to the authorization code flow specific functions (it is not needed in ROPC flow)
- add unit tests
- rename flag to --oidc-auth-request-extra-params
- add description to README.md

* Add integration test for --oidc-auth-request-extra-params

Co-authored-by: Hidetake Iwata <int128@gmail.com>
2020-03-25 11:52:53 +09:00
Hidetake Iwata
e2a6b5d4e2 Update README.md 2020-03-06 10:26:06 +09:00
dependabot-preview[bot]
ce93c739f8 Build(deps): bump github.com/int128/oauth2cli from 1.9.0 to 1.10.0 (#246)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.9.0...v1.10.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-28 22:08:48 +09:00
Hidetake Iwata
dc646c88f9 Bump version to v1.17.1 2020-02-22 16:04:29 +09:00
Hidetake Iwata
07e34d2222 Refactor (#245)
* Refactor: use Command.Context

* Refactor: do not infer command name for help/version
2020-02-22 15:40:43 +09:00
Hidetake Iwata
0e2d402c40 Bump github.com/int128/oauth2cli to v1.9.0 (#244)
* Bump github.com/int128/oauth2cli to v1.9.0

* Generate state parameter and pass to oauth2cli

* Refactor: use base64.NoPadding
2020-02-22 15:26:54 +09:00
Hidetake Iwata
db7c260f9d go mod tidy 2020-02-22 12:41:11 +09:00
dependabot-preview[bot]
a95dc5c794 Build(deps): bump github.com/spf13/cobra from 0.0.5 to 0.0.6 (#239)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/0.0.5...v0.0.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-22 12:39:26 +09:00
dependabot-preview[bot]
846804f611 Build(deps): bump k8s.io/client-go from 0.17.2 to 0.17.3 (#236)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.17.2 to 0.17.3.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.17.2...v0.17.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-22 12:32:56 +09:00
Hidetake Iwata
8b9e31b4c5 Refactor: error messages and testing/logger (#243)
* Refactor: respect -v option in testing/logger

* Refactor: revise error messages
2020-02-22 12:31:00 +09:00
Hidetake Iwata
bc99af6eab Merge pull request #242 from int128/refactor
Refactor
2020-02-21 23:37:32 +09:00
Hidetake Iwata
e3bec130f4 Refactor: fix mock package path 2020-02-21 22:59:59 +09:00
Hidetake Iwata
d59e3355fe Refactor: rename to adaptor/reader 2020-02-21 22:56:43 +09:00
Hidetake Iwata
9d2d0109d5 Refactor: extract adaptor/clock and testing/clock 2020-02-21 22:49:48 +09:00
Hidetake Iwata
aac8780caf Refactor: move to testing/logger 2020-02-21 22:39:27 +09:00
Hidetake Iwata
f89525b184 Refactor: extract domain/jwt and testing/jwt (#241)
* Refactor: extract domain/jwt and testing/jwt

* Refactor: remove jwt-go dep from product code
2020-02-21 22:33:08 +09:00
Hidetake Iwata
a46dab3dfd Fix error if multiple aud claim is given (#240) 2020-02-21 09:58:01 +09:00
Hidetake Iwata
42879dc915 Revise setup instruction (#235) 2020-02-12 21:27:08 +09:00
Hidetake Iwata
7ce98c7119 Add --certificate-authority-data option (#233) 2020-02-12 10:15:12 +09:00
dependabot-preview[bot]
8b5e87de75 Build(deps): bump github.com/coreos/go-oidc (#221)
Bumps [github.com/coreos/go-oidc](https://github.com/coreos/go-oidc) from 2.1.0+incompatible to 2.2.1+incompatible.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v2.1.0...v2.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-11 17:06:10 +09:00
Hidetake Iwata
1b545e1c58 Refactor: Use BROWSER env var to run chrome-login script (#232)
* Use BROWSER env var to run chrome-login script

* Update acceptance-test-diagram.svg

* Added acceptance-test-diagram.svg
2020-02-11 16:06:04 +09:00
Hidetake Iwata
2fa306c348 Improve error message if cannot open browser (#230) 2020-02-11 15:50:10 +09:00
Hidetake Iwata
9018cd65c5 Refactor: rename to integration_test (#228) 2020-02-07 20:58:13 +09:00
Hidetake Iwata
9fe6a09943 Fix flaky acceptance test (#229) 2020-02-07 20:45:51 +09:00
Hidetake Iwata
c53d415255 Refactor test and interfaces (#227)
* Refactor: extract adaptors.browser package

* Refactor: rename to idp.Provider

* Refactor: rename to adaptors.credentialpluginwriter
2020-02-07 11:56:31 +09:00
Hidetake Iwata
aa0718df16 Refactor: Makefile of e2e_test/keys/testdata (#226) 2020-02-06 10:44:55 +09:00
Hidetake Iwata
40698536b0 Delete Dockerfile
Fix up #224
2020-02-05 00:31:35 +09:00
Hidetake Iwata
7ec53a5dd1 Reduce time of acceptance test (#224)
* Reduce time of acceptance test

* Update README.md
2020-02-05 00:30:13 +09:00
Hidetake Iwata
3a28e44556 Add Docker usage (#223) 2020-02-02 10:55:47 +09:00
Hidetake Iwata
f8cca818af Add acceptance test (#222)
* Add acceptance test

* Update README.md

* Update README.md

* Added acceptance-test-diagram.svg

* Update README.md

* Add files via upload

* Added acceptance-test-diagram.svg

* Added acceptance-test-diagram.svg

* Update README.md

* Update README.md
2020-01-31 21:45:23 +09:00
Hidetake Iwata
0c6ca03eb9 Fix docker build error (#220)
upstream: https://github.com/int128/kubelogin-docker/pull/1
2020-01-24 11:14:11 +09:00
Hidetake Iwata
fe2fbcbc53 Refactor: use ghcp for release assets and PR (#219) 2020-01-24 10:59:08 +09:00
Hidetake Iwata
812a965739 go mod tidy 2020-01-24 10:51:45 +09:00
dependabot-preview[bot]
6de1fca64c Build(deps): bump gopkg.in/yaml.v2 from 2.2.7 to 2.2.8 (#217)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.7 to 2.2.8.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.7...v2.2.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-24 10:50:42 +09:00
Hidetake Iwata
0eb8cdc95f Add Dockerfile for release (#218) 2020-01-24 10:46:05 +09:00
dependabot-preview[bot]
995c0997d5 Build(deps): bump github.com/golang/mock from 1.3.1 to 1.4.0 (#215)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.3.1 to 1.4.0.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/1.3.1...v1.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-23 10:59:56 +09:00
dependabot-preview[bot]
18b2437819 Build(deps): bump k8s.io/client-go from 0.17.1 to 0.17.2 (#216)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.17.1 to 0.17.2.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.17.1...v0.17.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-23 10:54:29 +09:00
dependabot-preview[bot]
5d5a33b8ea Build(deps): bump k8s.io/client-go from 0.17.0 to 0.17.1 (#212)
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.17.0 to 0.17.1.
- [Release notes](https://github.com/kubernetes/client-go/releases)
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.17.0...v0.17.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-20 11:24:47 +09:00
Hidetake Iwata
a614943642 Bump version of k8s.io/client-go, k8s.io/apimachinery (#210) 2020-01-17 22:31:48 +09:00
Hidetake Iwata
d223175b92 Refactor dependency injection (#209)
* Refactor: use func type instead of factory interface

* Refactor: remove duplicated dependencies in di.go
2020-01-17 22:01:40 +09:00
Hidetake Iwata
6075c9dbe7 Add --listen-address option to bind all interfaces (#208) 2020-01-17 20:57:05 +09:00
Hidetake Iwata
be43c2ab82 Refactor: improve CI portability (#205) 2020-01-10 12:19:43 +09:00
Hidetake Iwata
512df0c4e4 go mod tidy 2020-01-09 15:56:20 +09:00
dependabot-preview[bot]
5d5292637f Build(deps): bump github.com/google/go-cmp from 0.3.1 to 0.4.0 (#204)
Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.3.1 to 0.4.0.
- [Release notes](https://github.com/google/go-cmp/releases)
- [Commits](https://github.com/google/go-cmp/compare/v0.3.1...v0.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-09 14:51:38 +09:00
Hidetake Iwata
76f61300d6 Refactor: extract oidc.Claims model (#202)
* Refactor: extract oidc.Claims model

* Refactor: extract Claims.IsExpired()
2019-12-26 20:17:30 +09:00
Hidetake Iwata
f7f1985a89 Refactor (#201)
* Refactor: rename to tokencache.Value

* Refactor: move to cmp.Diff from deep.Equal

* Refactor: reword error messages
2019-12-26 11:51:14 +09:00
Hidetake Iwata
3d47c88a8d Fix token cache is not refreshed when oidc options changed (#200) 2019-12-25 10:44:44 +09:00
Hidetake Iwata
c7ea97ff23 Refactor: remove test of make run (#199) 2019-12-23 18:02:15 +09:00
Hidetake Iwata
af18e734ea Set CGO_ENABLED=0 for static link (#198) 2019-12-23 17:55:43 +09:00
Hidetake Iwata
b5ae469b41 Create FUNDING.yml 2019-12-20 10:02:38 +09:00
Hidetake Iwata
94f480fdc9 Update README.md 2019-12-17 15:49:49 +09:00
Hidetake Iwata
7acb6e3a7b Refactor e2e tests (#196)
* Refactor: add e2e tests for credential plugin

* Refactor: extract assertCredentialPluginOutput()

* Refactor: add credential plugin test with TLS

* Refactor: extract helpers

* Refactor: rewrite TLS test cases

* Refactor: add test cases of token lifecycle
2019-12-17 11:07:43 +09:00
Hidetake Iwata
29e9c39a41 Update README.md 2019-12-12 10:16:47 +09:00
dependabot-preview[bot]
dd86168e4b Build(deps): bump github.com/google/wire from 0.3.0 to 0.4.0 (#195)
Bumps [github.com/google/wire](https://github.com/google/wire) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/google/wire/releases)
- [Commits](https://github.com/google/wire/compare/v0.3.0...v0.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-12 09:57:16 +09:00
dependabot-preview[bot]
1d48eab6b3 Build(deps): bump gopkg.in/yaml.v2 from 2.2.6 to 2.2.7 (#191)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.6 to 2.2.7.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.6...v2.2.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-27 23:34:24 +09:00
dependabot-preview[bot]
1e655a14b8 Build(deps): bump gopkg.in/yaml.v2 from 2.2.5 to 2.2.6 (#190)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.5 to 2.2.6.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.5...v2.2.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-19 13:45:08 -08:00
140 changed files with 5367 additions and 3466 deletions

16
.circleci/Makefile Normal file
View File

@@ -0,0 +1,16 @@
.PHONY: all
all:
.PHONY: install-test-deps
install-test-deps:
go get -v github.com/int128/goxzst
.PHONY: install-release-deps
install-release-deps: go
go get -v github.com/int128/goxzst github.com/int128/ghcp
go:
curl -sSfL -o go.tgz "https://golang.org/dl/go`ruby go_version_from_config.rb < config.yml`.darwin-amd64.tar.gz"
tar -xf go.tgz
rm go.tgz
./go/bin/go version

View File

@@ -1,38 +1,60 @@
version: 2
version: 2.1
jobs:
build:
test:
docker:
- image: circleci/golang:1.13.3
- image: cimg/go:1.14.7
steps:
- run: |
mkdir -p ~/bin
echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
- run: |
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
chmod +x ~/bin/kubectl
- run: |
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.5.0/ghcp_linux_amd64
chmod +x ~/bin/ghcp
- run: |
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.21.0
- run: go get github.com/int128/goxzst
- run: go get github.com/tcnksm/ghr
- checkout
- restore_cache:
keys:
- go-sum-{{ checksum "go.sum" }}
- run: make -C .circleci install-test-deps
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make run
- run: make dist
- save_cache:
key: go-sum-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
- store_artifacts:
path: gotest.log
release:
macos:
# https://circleci.com/docs/2.0/testing-ios/
xcode: 11.5.0
steps:
- run: echo 'export PATH="$HOME/go/bin:$PWD/.circleci/go/bin:$PATH"' >> $BASH_ENV
- checkout
- restore_cache:
keys:
- go-macos-{{ checksum "go.sum" }}
- run: make -C .circleci install-release-deps
- run: make dist
- run: |
if [ "$CIRCLE_TAG" ]; then
make release
fi
- save_cache:
key: go-macos-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
workflows:
version: 2
all:
build:
jobs:
- build:
context: open-source
- test:
filters:
tags:
only: /.*/
only: /^v.*/
- release:
context: open-source
requires:
- test
filters:
branches:
only: /^release-feature.*/
tags:
only: /^v.*/

3
.circleci/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/int128/kubelogin/.circleci
go 1.13

View File

@@ -0,0 +1,11 @@
require 'yaml'
config = YAML.load(STDIN)
image = config["jobs"]["test"]["docker"][0]["image"]
if !image.start_with?("cimg/go:")
raise "unknown image #{image} in #{configPath}"
end
goVersion = image.delete_prefix("cimg/go:")
print(goVersion)

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [int128] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment**
- OS: [e.g. macOS, Linux]
- kubelogin version: [e.g. 1.19.3]
- kubectl version: [e.g. 1.19]
- OpenID Connect provider: [e.g. Google, Okta]

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

8
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
]
}

14
.github/workflows/golangci-lint.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
on:
push:
paths:
- .github/workflows/golangci-lint.yaml
- '**.go'
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: golangci/golangci-lint-action@v2
with:
version: v1.30

29
.github/workflows/system-test.yaml vendored Normal file
View File

@@ -0,0 +1,29 @@
on:
push:
paths:
- .github/workflows/system-test.yaml
- system_test/**
- pkg/**
- go.*
jobs:
system-test:
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
runs-on: ubuntu-18.04
steps:
- uses: actions/setup-go@v2
with:
go-version: 1.14
id: go
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-
# https://packages.ubuntu.com/xenial/libnss3-tools
- run: sudo apt update
- run: sudo apt install -y libnss3-tools
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
- run: make -C system_test -j3 setup
- run: make -C system_test test

8
.gitignore vendored
View File

@@ -1,9 +1,13 @@
/.idea
/.kubeconfig*
/system_test/output/
/acceptance_test/output/
/dist
/dist/output
/coverage.out
/gotest.log
/kubelogin
/kubectl-oidc_login
/.circleci/go/

View File

@@ -1,46 +1,48 @@
# CircleCI specific variables
CIRCLE_TAG ?= latest
GITHUB_USERNAME := $(CIRCLE_PROJECT_USERNAME)
GITHUB_REPONAME := $(CIRCLE_PROJECT_REPONAME)
TARGET := kubelogin
TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
TARGET_OSARCH := linux_amd64 darwin_amd64 windows_amd64 linux_arm linux_arm64
VERSION ?= $(CIRCLE_TAG)
LDFLAGS := -X main.version=$(VERSION)
all: $(TARGET)
.PHONY: check
check:
golangci-lint run
go test -v -race -cover -coverprofile=coverage.out ./...
$(TARGET): $(wildcard *.go)
$(TARGET): $(wildcard **/*.go)
go build -o $@ -ldflags "$(LDFLAGS)"
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
.PHONY: check
check:
go test -v -race -cover -coverprofile=coverage.out ./... > gotest.log
.PHONY: run
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
dist:
# make the zip files for GitHub Releases
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -i "LICENSE" -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
zipinfo dist/gh/kubelogin_linux_amd64.zip
# make the Homebrew formula
mv dist/gh/kubelogin.rb dist/
# make the yaml for krew-index
mkdir -p dist/plugins
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
.PHONY: dist
dist: dist/output
dist/output:
# make the zip files for GitHub Releases
VERSION=$(VERSION) goxzst -d dist/output -i "LICENSE" -o "$(TARGET)" -osarch "$(TARGET_OSARCH)" -t "dist/kubelogin.rb dist/oidc-login.yaml dist/Dockerfile" -- -ldflags "$(LDFLAGS)"
# test the zip file
zipinfo dist/output/kubelogin_linux_amd64.zip
# make the krew yaml structure
mkdir -p dist/output/plugins
mv dist/output/oidc-login.yaml dist/output/plugins/oidc-login.yaml
.PHONY: release
release: dist
# publish to the GitHub Releases
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
# publish to the Homebrew tap repository
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
# fork krew-index and create a branch
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(CIRCLE_TAG)" -m "Bump oidc-login to $(CIRCLE_TAG)" -C dist/ plugins/oidc-login.yaml
# publish the binaries
ghcp release -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)" -t "$(VERSION)" dist/output/
# publish the Homebrew formula
ghcp commit -u "$(GITHUB_USERNAME)" -r "homebrew-$(GITHUB_REPONAME)" -b "bump-$(VERSION)" -m "Bump the version to $(VERSION)" -C dist/output/ kubelogin.rb
ghcp pull-request -u "$(GITHUB_USERNAME)" -r "homebrew-$(GITHUB_REPONAME)" -b "bump-$(VERSION)" --title "Bump the version to $(VERSION)"
# publish the Dockerfile
ghcp commit -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)-docker" -b "bump-$(VERSION)" -m "Bump the version to $(VERSION)" -C dist/output/ Dockerfile
ghcp pull-request -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)-docker" -b "bump-$(VERSION)" --title "Bump the version to $(VERSION)"
# publish the Krew manifest
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(VERSION)" -m "Bump oidc-login to $(VERSION)" -C dist/output/ plugins/oidc-login.yaml
.PHONY: clean
clean:
-rm $(TARGET)
-rm $(TARGET_PLUGIN)
-rm -r dist/
-rm -r dist/output/
-rm coverage.out gotest.log

181
README.md
View File

@@ -4,7 +4,7 @@ This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](h
Here is an example of Kubernetes authentication with the Google Identity Platform:
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/68352462-dcfc8b00-0149-11ea-8896-1632858a43c0.gif" width="650" height="470">
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/85427290-86e43700-b5b6-11ea-9e97-ffefd736c9b7.gif" width="572" height="391">
Kubelogin is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
When you run kubectl, kubelogin opens the browser and you can log in to the provider.
@@ -18,19 +18,17 @@ Take a look at the diagram:
### Setup
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew), [Chocolatey](https://chocolatey.org/packages/kubelogin) or [GitHub Releases](https://github.com/int128/kubelogin/releases).
```sh
# Homebrew
# Homebrew (macOS and Linux)
brew install int128/kubelogin/kubelogin
# Krew
# Krew (macOS, Linux, Windows and ARM)
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.15.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
# Chocolatey (Windows)
choco install kubelogin
```
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
@@ -51,7 +49,7 @@ users:
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
See [the setup guide](docs/setup.md) for more.
See [setup guide](docs/setup.md) for more.
### Run
@@ -72,7 +70,6 @@ After authentication, kubelogin returns the credentials to kubectl and finally k
```
% kubectl get pods
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
@@ -83,155 +80,35 @@ If the cached ID token is valid, kubelogin just returns it.
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
If the refresh token has expired, kubelogin will perform reauthentication.
### Troubleshoot
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
Kubelogin will perform authentication if the token cache file does not exist.
You can dump claims of an ID token by `setup` command.
## Usage
```console
% kubectl oidc-login setup --oidc-issuer-url https://accounts.google.com --oidc-client-id REDACTED --oidc-client-secret REDACTED
...
You got a token with the following claims:
This document is for the development version.
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
Kubelogin supports the following options:
```
% kubectl oidc-login get-token -h
Run as a kubectl credential plugin
Usage:
kubelogin get-token [flags]
Flags:
--oidc-issuer-url string Issuer URL of the provider (mandatory)
--oidc-client-id string Client ID of the provider (mandatory)
--oidc-client-secret string Client secret of the provider
--oidc-extra-scope strings Scopes to request to the provider
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
-h, --help help for get-token
Global Flags:
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
{
"sub": "********",
"iss": "https://accounts.google.com",
"aud": "********",
...
}
```
See also the options of [standalone mode](docs/standalone-mode.md).
### Extra scopes
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
```yaml
- --oidc-extra-scope=email
- --oidc-extra-scope=profile
```
### CA Certificates
You can use your self-signed certificate for the provider.
```yaml
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
```
### HTTP Proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
### Authentication flows
## Docs
#### Authorization code flow
Kubelogin performs the authorization code flow by default.
It starts the local server at port 8000 or 18000 by default.
You need to register the following redirect URIs to the provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the ports by the option:
```yaml
- --listen-port=12345
- --listen-port=23456
```
#### Authorization code flow with keyboard interactive
If you cannot access the browser, instead use the authorization code flow with keyboard interactive.
```yaml
- --grant-type=authcode-keyboard
```
Kubelogin will show the URL and prompt.
Open the URL in the browser and then copy the code shown.
```
% kubectl get pods
Open https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=...
Enter code: YOUR_CODE
```
Note that this flow uses the redirect URI `urn:ietf:wg:oauth:2.0:oob` and
some OIDC providers do not support it.
#### Resource owner password credentials grant flow
Kubelogin performs the resource owner password credentials grant flow
when `--grant-type=password` or `--username` is set.
Note that most OIDC providers do not support this flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
You can set the username and password.
```yaml
- --username=USERNAME
- --password=PASSWORD
```
If the password is not set, kubelogin will show the prompt for the password.
```yaml
- --username=USERNAME
```
```
% kubectl get pods
Password:
```
If the username is not set, kubelogin will show the prompt for the username and password.
```yaml
- --grant-type=password
```
```
% kubectl get pods
Username: foo
Password:
```
- [Setup guide](docs/setup.md)
- [Usage and options](docs/usage.md)
- [Standalone mode](docs/standalone-mode.md) (deprecated)
## Related works
@@ -248,7 +125,7 @@ Feel free to open issues and pull requests for improving code and documents.
### Development
Go 1.12 or later is required.
Go 1.13 or later is required.
```sh
# Run lint and tests
@@ -258,3 +135,7 @@ make check
make
./kubelogin
```
### System test
See [system test](system_test) for more.

42
acceptance_test/Makefile Normal file
View File

@@ -0,0 +1,42 @@
CLUSTER_NAME := kubelogin-acceptance-test
OUTPUT_DIR := $(CURDIR)/output
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG
# create a Kubernetes cluster
.PHONY: cluster
cluster:
# create a cluster
mkdir -p $(OUTPUT_DIR)
sed -e "s|OIDC_ISSUER_URL|$(OIDC_ISSUER_URL)|" -e "s|OIDC_CLIENT_ID|$(OIDC_CLIENT_ID)|" cluster.yaml > $(OUTPUT_DIR)/cluster.yaml
kind create cluster --name $(CLUSTER_NAME) --config $(OUTPUT_DIR)/cluster.yaml
# set up access control
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=$(YOUR_EMAIL)
# set up kubectl
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=$(CURDIR)/../kubelogin \
--exec-arg=get-token \
--exec-arg=--token-cache-dir=$(OUTPUT_DIR)/token-cache \
--exec-arg=--oidc-issuer-url=$(OIDC_ISSUER_URL) \
--exec-arg=--oidc-client-id=$(OIDC_CLIENT_ID) \
--exec-arg=--oidc-client-secret=$(OIDC_CLIENT_SECRET) \
--exec-arg=--oidc-extra-scope=email
# switch the default user
kubectl config set-context --current --user=oidc
# clean up the resources
.PHONY: clean
clean:
-rm -r $(OUTPUT_DIR)
.PHONY: delete-cluster
delete-cluster:
kind delete cluster --name $(CLUSTER_NAME)
.PHONY: check
check:
docker version
kind version
kubectl version --client

75
acceptance_test/README.md Normal file
View File

@@ -0,0 +1,75 @@
# kubelogin/acceptance_test
This is a manual test for verifying Kubernetes OIDC authentication with your OIDC provider.
## Purpose
This test checks the following points:
1. You can set up your OIDC provider using [setup guide](../docs/setup.md).
1. The plugin works with your OIDC provider.
## Getting Started
### Prerequisite
You need to build the plugin into the parent directory.
```sh
make -C ..
```
You need to set up your provider.
See [setup guide](../docs/setup.md) for more.
You need to install the following tools:
- Docker
- Kind
- kubectl
You can check if the tools are available.
```sh
make check
```
### 1. Create a cluster
Create a cluster.
For example, you can create a cluster with Google account authentication.
```sh
make OIDC_ISSUER_URL=https://accounts.google.com \
OIDC_CLIENT_ID=REDACTED.apps.googleusercontent.com \
OIDC_CLIENT_SECRET=REDACTED \
YOUR_EMAIL=REDACTED@gmail.com
```
It will do the following steps:
1. Create a cluster.
1. Set up access control. It allows read-only access from your email address.
1. Set up kubectl to enable the plugin.
You can change kubectl configuration in generated `output/kubeconfig.yaml`.
### 2. Run kubectl
Make sure you can log in to the provider and access the cluster.
```console
% export KUBECONFIG=$PWD/output/kubeconfig.yaml
% kubectl get pods -A
```
### Clean up
To delete the cluster and generated files:
```sh
make delete-cluster
make clean
```

View File

@@ -0,0 +1,13 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
metadata:
name: config
apiServer:
extraArgs:
oidc-issuer-url: OIDC_ISSUER_URL
oidc-client-id: OIDC_CLIENT_ID
oidc-username-claim: email

13
dist/Dockerfile vendored Normal file
View File

@@ -0,0 +1,13 @@
FROM alpine:3.12
ARG KUBELOGIN_VERSION="{{ env "VERSION" }}"
ARG KUBELOGIN_SHA256="{{ sha256 .linux_amd64_archive }}"
# Download the release and test the checksum
RUN wget -O /kubelogin.zip "https://github.com/int128/kubelogin/releases/download/$KUBELOGIN_VERSION/kubelogin_linux_amd64.zip" && \
echo "$KUBELOGIN_SHA256 /kubelogin.zip" | sha256sum -c - && \
unzip /kubelogin.zip && \
rm /kubelogin.zip
USER daemon
ENTRYPOINT ["/kubelogin"]

View File

@@ -1,15 +1,27 @@
class Kubelogin < Formula
desc "A kubectl plugin for Kubernetes OpenID Connect authentication"
homepage "https://github.com/int128/kubelogin"
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
baseurl = "https://github.com/int128/kubelogin/releases/download"
version "{{ env "VERSION" }}"
sha256 "{{ sha256 .darwin_amd64_archive }}"
if OS.mac?
kernel = "darwin"
sha256 "{{ sha256 .darwin_amd64_archive }}"
elsif OS.linux?
kernel = "linux"
sha256 "{{ sha256 .linux_amd64_archive }}"
end
url baseurl + "/#{version}/kubelogin_#{kernel}_amd64.zip"
def install
bin.install "kubelogin" => "kubelogin"
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
end
test do
system "#{bin}/kubelogin -h"
system "#{bin}/kubectl-oidc_login -h"
end
end

View File

@@ -22,8 +22,6 @@ spec:
caveats: |
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
See https://github.com/int128/kubelogin for more.
version: {{ env "VERSION" }}
platforms:
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
@@ -62,3 +60,27 @@ spec:
matchLabels:
os: windows
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm.zip
sha256: "{{ sha256 .linux_arm_archive }}"
bin: kubelogin
files:
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
arch: arm
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm64.zip
sha256: "{{ sha256 .linux_arm64_archive }}"
bin: kubelogin
files:
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
arch: arm64

View File

@@ -1,6 +1,6 @@
# Kubernetes OpenID Connection authentication
This document guides how to set up the Kubernetes OpenID Connect (OIDC) authentication.
This document guides how to set up Kubernetes OpenID Connect (OIDC) authentication.
Let's see the following steps:
1. Set up the OIDC provider
@@ -35,7 +35,7 @@ Variable | Value
You can log in with a user of Keycloak.
Make sure you have an administrator role of the Keycloak realm.
Open the Keycloak and create an OIDC client as follows:
Open Keycloak and create an OIDC client as follows:
- Client ID: `YOUR_CLIENT_ID`
- Valid Redirect URLs:
@@ -52,7 +52,7 @@ You can associate client roles by adding the following mapper:
- Token Claim Name: `groups`
- Add to ID token: on
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
For example, if you have `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
Replace the following variables in the later sections.
@@ -72,7 +72,7 @@ Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an a
- Homepage URL: `https://dex.example.com`
- Authorization callback URL: `https://dex.example.com/callback`
Deploy the [dex](https://github.com/dexidp/dex) with the following config:
Deploy [Dex](https://github.com/dexidp/dex) with the following config:
```yaml
issuer: https://dex.example.com
@@ -126,6 +126,9 @@ Variable | Value
You do not need to set `YOUR_CLIENT_SECRET`.
If you need `groups` claim for access control,
see [jetstack/okta-kubectl-auth](https://github.com/jetstack/okta-kubectl-auth/blob/master/docs/okta-setup.md) and [#250](https://github.com/int128/kubelogin/issues/250).
## 2. Verify authentication
@@ -138,31 +141,23 @@ kubectl oidc-login setup \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
It will open the browser and you can log in to the provider.
Then it will show the instruction.
It launches the browser and navigates to `http://localhost:8000`.
Please log in to the provider.
You can set extra options, for example, extra scope or CA certificate.
See also the full options.
```sh
kubectl oidc-login setup --help
```
## 3. Bind a cluster role
In this tutorial, bind the `cluster-admin` role to you.
Apply the following manifest:
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: ISSUER_URL#YOUR_SUBJECT
```
Here bind `cluster-admin` role to you.
```sh
kubectl apply -f oidc-cluster-admin.yaml
kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='ISSUER_URL#YOUR_SUBJECT'
```
As well as you can create a custom cluster role and bind it.
@@ -170,14 +165,14 @@ As well as you can create a custom cluster role and bind it.
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
Add the following flags to kube-apiserver:
```
--oidc-issuer-url=ISSUER_URL
--oidc-client-id=YOUR_CLIENT_ID
```
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
See [Kubernetes Authenticating: OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for the all flags.
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
@@ -200,35 +195,32 @@ If you are using [kube-aws](https://github.com/kubernetes-incubator/kube-aws), a
## 5. Set up the kubeconfig
Add the following user to the kubeconfig:
Add `oidc` user to the kubeconfig.
```yaml
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=ISSUER_URL
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```sh
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=ISSUER_URL \
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
```sh
kubectl --user=oidc cluster-info
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
You can switch the current context to oidc.
```sh
kubectl config set-context --current --user=oidc
```
You can share the kubeconfig to your team members for on-boarding.

View File

@@ -75,56 +75,6 @@ If the refresh token has expired, kubelogin will proceed the authentication.
## Usage
Kubelogin supports the following options:
```
% kubectl oidc-login -h
Login to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
Usage:
main [flags]
main [command]
Available Commands:
get-token Run as a kubectl credential plugin
help Help about any command
setup Show the setup instruction
version Print the version information
Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
-h, --help help for main
--version version for main
```
### Kubeconfig
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

216
docs/usage.md Normal file
View File

@@ -0,0 +1,216 @@
# Usage
Kubelogin supports the following options:
```
Usage:
kubelogin get-token [flags]
Flags:
--oidc-issuer-url string Issuer URL of the provider (mandatory)
--oidc-client-id string Client ID of the provider (mandatory)
--oidc-client-secret string Client secret of the provider
--oidc-extra-scope strings Scopes to request to the provider
--token-cache-dir string Path to a directory for token cache (default "~/.kube/cache/oidc-login")
--certificate-authority string Path to a cert file for the certificate authority
--certificate-authority-data string Base64 encoded cert for the certificate authority
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--grant-type string Authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-address strings [authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--skip-open-browser [authcode] Do not open the browser automatically
--open-url-after-authentication string [authcode] If set, open the URL in the browser after authentication
--oidc-redirect-url-hostname string [authcode] Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString [authcode, authcode-keyboard] Extra query parameters to send with an authentication request (default [])
--username string [password] Username for resource owner password credentials grant
--password string [password] Password for resource owner password credentials grant
-h, --help help for get-token
Global Flags:
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
## Options
### Extra scopes
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
```yaml
- --oidc-extra-scope=email
- --oidc-extra-scope=profile
```
### CA Certificate
You can use your self-signed certificate for the provider.
```yaml
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
- --certificate-authority-data=LS0t...
```
### HTTP Proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
## Authentication flows
Kubelogin support the following flows:
- Authorization code flow
- Authorization code flow with a keyboard
- Resource owner password credentials grant flow
### Authorization code flow
Kubelogin performs the authorization code flow by default.
It starts the local server at port 8000 or 18000 by default.
You need to register the following redirect URIs to the provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the listening address.
```yaml
- --listen-address=127.0.0.1:12345
- --listen-address=127.0.0.1:23456
```
You can change the hostname of redirect URI from the default value `localhost`.
```yaml
- --oidc-redirect-url-hostname=127.0.0.1
```
You can add extra parameters to the authentication request.
```yaml
- --oidc-auth-request-extra-params=ttl=86400
```
When authentication completed, kubelogin shows a message to close the browser.
You can change the URL to show after authentication.
```yaml
- --open-url-after-authentication=https://example.com/success.html
```
You can skip opening the browser if you encounter some environment problem.
```yaml
- --skip-open-browser
```
For Linux users, you change the default browser by `BROWSER` environment variable.
### Authorization code flow with a keyboard
If you cannot access the browser, instead use the authorization code flow with a keyboard.
```yaml
- --grant-type=authcode-keyboard
```
Kubelogin will show the URL and prompt.
Open the URL in the browser and then copy the code shown.
```
% kubectl get pods
Open https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=...
Enter code: YOUR_CODE
```
Note that this flow uses the redirect URI `urn:ietf:wg:oauth:2.0:oob` and some OIDC providers do not support it.
You can add extra parameters to the authentication request.
```yaml
- --oidc-auth-request-extra-params=ttl=86400
```
### Resource owner password credentials grant flow
Kubelogin performs the resource owner password credentials grant flow
when `--grant-type=password` or `--username` is set.
Note that most OIDC providers do not support this flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
You can set the username and password.
```yaml
- --username=USERNAME
- --password=PASSWORD
```
If the password is not set, kubelogin will show the prompt for the password.
```yaml
- --username=USERNAME
```
```
% kubectl get pods
Password:
```
If the username is not set, kubelogin will show the prompt for the username and password.
```yaml
- --grant-type=password
```
```
% kubectl get pods
Username: foo
Password:
```
## Run in Docker
You can run [the Docker image](https://quay.io/repository/int128/kubelogin) instead of the binary.
The kubeconfig looks like:
```yaml
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: docker
args:
- run
- --rm
- -v
- /tmp/.token-cache:/.token-cache
- -p
- 8000:8000
- quay.io/int128/kubelogin
- get-token
- --token-cache-dir=/.token-cache
- --listen-address=0.0.0.0:8000
- --oidc-issuer-url=ISSUER_URL
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
Known limitations:
- It cannot open the browser automatically.
- The container port and listen port must be equal for consistency of the redirect URI.

View File

@@ -1,84 +0,0 @@
package e2e_test
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
// Run the integration tests of the credential plugin use-case.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the output.
//
func TestCmd_Run_CredentialPlugin(t *testing.T) {
timeout := 1 * time.Second
cacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
}
defer func() {
if err := os.RemoveAll(cacheDir); err != nil {
t.Errorf("could not clean up the cache dir: %s", err)
}
}()
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(gomock.Any()).
Do(func(out credentialplugin.Output) {
if out.Token != idToken {
t.Errorf("Token wants %s but %s", idToken, out.Token)
}
if out.Expiry != tokenExpiryFuture {
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
}
})
runGetTokenCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
credentialPluginInteraction,
"--skip-open-browser",
"--listen-port", "0",
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
)
})
}
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, interaction credentialplugin.Interface, args ...string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, interaction)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "get-token", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}

View File

@@ -1,122 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/e2e_test/idp (interfaces: Service)
// Package mock_idp is a generated GoMock package.
package mock_idp
import (
gomock "github.com/golang/mock/gomock"
idp "github.com/int128/kubelogin/e2e_test/idp"
reflect "reflect"
)
// MockService is a mock of Service interface
type MockService struct {
ctrl *gomock.Controller
recorder *MockServiceMockRecorder
}
// MockServiceMockRecorder is the mock recorder for MockService
type MockServiceMockRecorder struct {
mock *MockService
}
// NewMockService creates a new mock instance
func NewMockService(ctrl *gomock.Controller) *MockService {
mock := &MockService{ctrl: ctrl}
mock.recorder = &MockServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockService) EXPECT() *MockServiceMockRecorder {
return m.recorder
}
// AuthenticateCode mocks base method
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateCode indicates an expected call of AuthenticateCode
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
}
// AuthenticatePassword mocks base method
func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticatePassword indicates an expected call of AuthenticatePassword
func (mr *MockServiceMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockService)(nil).AuthenticatePassword), arg0, arg1, arg2)
}
// Discovery mocks base method
func (m *MockService) Discovery() *idp.DiscoveryResponse {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Discovery")
ret0, _ := ret[0].(*idp.DiscoveryResponse)
return ret0
}
// Discovery indicates an expected call of Discovery
func (mr *MockServiceMockRecorder) Discovery() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockService)(nil).Discovery))
}
// Exchange mocks base method
func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exchange", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exchange indicates an expected call of Exchange
func (mr *MockServiceMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockService)(nil).Exchange), arg0)
}
// GetCertificates mocks base method
func (m *MockService) GetCertificates() *idp.CertificatesResponse {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCertificates")
ret0, _ := ret[0].(*idp.CertificatesResponse)
return ret0
}
// GetCertificates indicates an expected call of GetCertificates
func (mr *MockServiceMockRecorder) GetCertificates() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockService)(nil).GetCertificates))
}
// Refresh mocks base method
func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockServiceMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockService)(nil).Refresh), arg0)
}

View File

@@ -1,71 +0,0 @@
package keys
import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"golang.org/x/xerrors"
)
// TLSCACert is path to the CA certificate.
// This should be generated by Makefile before test.
const TLSCACert = "keys/testdata/ca.crt"
// TLSCACertAsBase64 is a base64 encoded string of TLSCACert.
var TLSCACertAsBase64 string
// TLSCACertAsConfig is a TLS config including TLSCACert.
var TLSCACertAsConfig = &tls.Config{RootCAs: x509.NewCertPool()}
// TLSServerCert is path to the server certificate.
// This should be generated by Makefile before test.
const TLSServerCert = "keys/testdata/server.crt"
// TLSServerKey is path to the server key.
// This should be generated by Makefile before test.
const TLSServerKey = "keys/testdata/server.key"
// JWSKey is path to the key for signing ID tokens.
const JWSKey = "keys/testdata/jws.key"
// JWSKeyPair is the key pair loaded from JWSKey.
var JWSKeyPair *rsa.PrivateKey
func init() {
var err error
JWSKeyPair, err = readPrivateKey(JWSKey)
if err != nil {
panic(err)
}
b, err := ioutil.ReadFile(TLSCACert)
if err != nil {
panic(err)
}
TLSCACertAsBase64 = base64.StdEncoding.EncodeToString(b)
if !TLSCACertAsConfig.RootCAs.AppendCertsFromPEM(b) {
panic("could not append the CA cert")
}
}
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
b, err := ioutil.ReadFile(name)
if err != nil {
return nil, xerrors.Errorf("could not read JWSKey: %w", err)
}
block, rest := pem.Decode(b)
if block == nil {
return nil, xerrors.New("could not decode PEM")
}
if len(rest) > 0 {
return nil, xerrors.New("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Errorf("could not parse the key: %w", err)
}
return k, nil
}

View File

@@ -1 +0,0 @@
/CA

View File

@@ -1,59 +0,0 @@
EXPIRY := 3650
all: ca.key ca.crt server.key server.crt jws.key
.PHONY: clean
clean:
-rm -v ca.* server.* jws.*
ca.key:
openssl genrsa -out $@ 1024
.INTERMEDIATE: ca.csr
ca.csr: openssl.cnf ca.key
openssl req -config openssl.cnf \
-new \
-key ca.key \
-subj "/CN=Hello CA" \
-out $@
openssl req -noout -text -in $@
ca.crt: ca.csr ca.key
openssl x509 \
-req \
-days $(EXPIRY) \
-signkey ca.key \
-in ca.csr \
-out $@
openssl x509 -text -in $@
server.key:
openssl genrsa -out $@ 1024
.INTERMEDIATE: server.csr
server.csr: openssl.cnf server.key
openssl req -config openssl.cnf \
-new \
-key server.key \
-subj "/CN=localhost" \
-out $@
openssl req -noout -text -in $@
server.crt: openssl.cnf server.csr ca.key ca.crt
rm -fr ./CA
mkdir -p ./CA
touch CA/index.txt
touch CA/index.txt.attr
echo 00 > CA/serial
openssl ca -config openssl.cnf \
-days $(EXPIRY) \
-extensions v3_req \
-batch \
-cert ca.crt \
-keyfile ca.key \
-in server.csr \
-out $@
openssl x509 -text -in $@
jws.key:
openssl genrsa -out $@ 1024

View File

@@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBnTCCAQYCCQC/aR7GRyndljANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
ZWxsbyBDQTAeFw0xOTA5MjUxNDQ0NTFaFw0yOTA5MjIxNDQ0NTFaMBMxETAPBgNV
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
AE/gsgTC4jzYC3icZdhALJTe3JsZ7geN702dE95zSI5LXAzzHJ/j8wGmorQjrMs2
iNPjVOdTU6cVWa1Ba29wWakVyVCUqDmDiWHaVhM/Qyyxo6mVlZGFwSnto3zq/h4y
KMFJ8lUtFCYMrzo5wqgj2xOjVrN77F6F4XWZbMufh50G
-----END CERTIFICATE-----

View File

@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDnSTDsRx4UJmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ
4Pc51itOycWN1nclNVb489i9J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3L
u4mysxyOtYZNsdST8GBCP0m42tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQAB
AoGAaYmTYm29QvKW4et9oPxDjpYG0bqlz7P0xFRR9kKtKTATAMHjWeu2xFR/JI+b
rvJLIdZqHmWe5AmMb3NxZgfLonEB71ohaKQha1L8Vc7aoedRheJvqqaNr+ZxoCMO
8xcjsaMYxLEVt0tg6XyKyEhi1/hOufFZ4BSng4oQbrpaNIkCQQD6hPEzzPZtMEEe
eRdwTVUIStKFMQbRdwZ5Oc7pyDk2U+SFRJiqkBkqnmFekcf2UgbBQxem+GMhWNgE
LItKy/wVAkEA7FiHxbzn2msaE3hZCWudtnXqmJNuPO0zJ5icXe2svwmwPfLA/rm9
iazCuyzyK67J8IG9QgIjQFYXtQbMr2chGwJBAMm3dghBx0LQEf8Zfdf9TLSqmqyI
d3b+IgZGl+cCQ58NGfp863ibIsiAUuK0+4/JKItBHLBjXF6jjPx/aYFGkqkCQH4w
GnXCEYx1qJuCow87jR4xQQsrlC0lfC2E9t/TmWr6UkYRCWg3ZXJPcj0bl0Upcppd
ut22ZHniPZAizEBOcMcCQQDnMEOxufxhMsx2NC8yON/noewLINqKcbkMsl6DjvJl
+wLbQmzJ0j+uIBgdpsj4rWnEr7GxoL2eWG44QDUBco79
-----END RSA PRIVATE KEY-----

View File

@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQCZukkN1GxMlNXkpOZxCnvCF874/rn1sNKzO98fOwmBRPG2+m/c
yqBY7t2L2nihqz3+GZTiHmzSrBzMAGPVW1qGmk9KYg3m7akz9SiCxdoUkgM9MCCp
X/s8IhgtXkyoKFPcGdwHblDl/3aJG02b6TAQD8vTNQAKoKw7L0FST+pvRwIDAQAB
AoGBAJT1fXR5MbfDQL+dSe6fSex5RYTgzzDTdldW3I1Wl487Tz0OzvYTIe0LCIJL
4DhHxnpCL5IsCSbav8ytVA+ZxczHpEW6UxbalXt5UfgFu0joTrdoGxDcVWgUCW3J
Olbln0lOP55wViKh509gt45Za3VxJrNul3khVfVj7qGG9cKBAkEAxwtT8LxwqTYF
nqoeZvPp15JAqlgdk38ttJa4KEqvpBTSxNIXkL9T5gJ+irKZAzxlz/U7bhn5mw6E
3xFiljOXpQJBAMW3XRFOjgNBXNjbt81wREF5LdZl9EI8cRMSH6xljt2uwSqw4EG3
76gFvccUd+WnfspFQZVypSSD4pWzsAqh13sCQQCA9BLW5Y7r4ab0a2y08JNwaT1h
3yKSO5QF6pu25uQyHpeKkj5YNcyKONV40EqXsRqZB10QcN2omlh1GJNRkm1NAkEA
qV3lr4mnRUqcinfM/4MINT3k8h/sGUFFa5y+3SMyOtwURMm3kRRLi5c/dmYmPug4
SHUDNU48AQeo9awzRShWOQJBAJdw+cfRgi4fo3HY33uZdFa1T9G+qTA2ijhco6O3
8tOc0yOFEtPNXM87MsHGIQP3ZCLfIY1gs2O3WCTFbPxR4rc=
-----END RSA PRIVATE KEY-----

View File

@@ -1,36 +0,0 @@
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = ./CA
certs = $dir
crl_dir = $dir
database = $dir/index.txt
new_certs_dir = $dir
default_md = sha256
policy = policy_match
serial = $dir/serial
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
x509_extensions = v3_ca
[ req_distinguished_name ]
commonName = Common Name (e.g. server FQDN or YOUR name)
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = DNS:localhost
[ v3_ca ]
basicConstraints = CA:true

View File

@@ -1,52 +0,0 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 0 (0x0)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Hello CA
Validity
Not Before: Aug 18 06:00:06 2019 GMT
Not After : Aug 15 06:00:06 2029 GMT
Subject: CN=localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (1024 bit)
Modulus:
00:d6:4e:eb:3a:cb:25:f9:7e:92:22:f2:63:99:da:
08:05:8b:a3:e7:d3:fd:71:3e:bd:da:c5:d5:63:b7:
d3:7b:f8:cd:1a:2e:5c:a2:4f:48:98:c2:b4:da:e8:
1e:d3:d7:8f:d8:ee:a9:70:d0:9d:4f:f4:8d:95:e5:
8e:9a:71:b6:80:aa:0b:cb:28:1d:f6:0d:7e:aa:78:
bf:30:e6:58:d7:6b:92:8f:19:1c:7d:95:f8:d5:2f:
8c:58:49:98:88:05:50:88:80:a9:77:c4:16:b4:c1:
00:45:1e:d3:d0:ed:98:4d:f7:a3:5d:f1:82:cb:a5:
4d:19:64:4d:43:db:13:d4:17
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
DNS:localhost
Signature Algorithm: sha256WithRSAEncryption
5a:5c:5e:8b:de:82:86:f4:98:40:0e:cf:c5:51:fe:89:46:49:
f0:26:d2:a5:06:e3:91:43:c1:f8:b2:ad:b7:a1:23:13:1a:80:
45:00:51:70:b6:06:63:c6:a8:c8:22:5d:1b:00:e0:4a:8c:2e:
ce:b4:da:b1:89:8a:d2:d0:e3:eb:0f:16:34:45:a1:bd:64:5c:
48:41:8c:0a:bf:66:be:1c:a8:35:47:ce:b0:dc:c8:4f:5e:c1:
ec:ef:21:fb:45:55:95:e3:99:40:46:0b:6c:8a:b3:d5:f0:bf:
39:a4:ba:c4:d7:58:88:58:08:07:98:59:6e:ca:9c:08:e4:c4:
4f:db
-----BEGIN CERTIFICATE-----
MIIBzTCCATagAwIBAgIBADANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhIZWxs
byBDQTAeFw0xOTA4MTgwNjAwMDZaFw0yOTA4MTUwNjAwMDZaMBQxEjAQBgNVBAMM
CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1k7rOssl+X6S
IvJjmdoIBYuj59P9cT692sXVY7fTe/jNGi5cok9ImMK02uge09eP2O6pcNCdT/SN
leWOmnG2gKoLyygd9g1+qni/MOZY12uSjxkcfZX41S+MWEmYiAVQiICpd8QWtMEA
RR7T0O2YTfejXfGCy6VNGWRNQ9sT1BcCAwEAAaMwMC4wCQYDVR0TBAIwADALBgNV
HQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4GB
AFpcXovegob0mEAOz8VR/olGSfAm0qUG45FDwfiyrbehIxMagEUAUXC2BmPGqMgi
XRsA4EqMLs602rGJitLQ4+sPFjRFob1kXEhBjAq/Zr4cqDVHzrDcyE9ewezvIftF
VZXjmUBGC2yKs9XwvzmkusTXWIhYCAeYWW7KnAjkxE/b
-----END CERTIFICATE-----

View File

@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDWTus6yyX5fpIi8mOZ2ggFi6Pn0/1xPr3axdVjt9N7+M0aLlyi
T0iYwrTa6B7T14/Y7qlw0J1P9I2V5Y6acbaAqgvLKB32DX6qeL8w5ljXa5KPGRx9
lfjVL4xYSZiIBVCIgKl3xBa0wQBFHtPQ7ZhN96Nd8YLLpU0ZZE1D2xPUFwIDAQAB
AoGBAJhNR7Dl1JwFzndViWE6aP7/6UEFEBWeADDs7aTLbFmrTJ+xmRWkgLRHk14L
HnVwuYLywaoyJ8o9wy1nEbxC2e4zWZ94d351MQf3/komCXDBzEsktfAcNsAFnMmS
HZuGXfhi0FYWoftpIGxUmEBmQRcq0ctycbLves6TY3y+oajpAkEA8UHmSr/zsM3E
XQXPp2BCAvRrTH/njk4R0jwB29Bi89gt/XDD4uvfWbHw7TZxnZuCpWisnxpMPIwa
1rjqIQmhEwJBAONncQUOxwYCIuvraIhV0QtkIUa+YpTvAxP8ZNXx+agtHmHG2TTf
kGv2YddvjxXZItN/FZOzUGm9OptaeLRTpW0CQHO8CEzNnoqve0agtgf2LlSaiiqt
pRhoLTZsYPvhEMcnapCNGvtt6bxul0REfOZ9poPRHhZJGE9naqydEnv80Y8CQQC3
pxLfws95SsBpR/VkJepuCK/XMmrrXRxfR7coEgROjiG7VZyV1vgMOS9Ljg1A19wI
cto6LtcCjpCGZsqU1/kBAkEAv2tXBts3vuIjguZNMz7KLWmu3zG2SQRaqdEZwL+R
DQmD5tbI6gEtd5OmgmSiW8A4mpfgFYvG7Um2fwi7TTXtSA==
-----END RSA PRIVATE KEY-----

View File

@@ -1,357 +0,0 @@
package e2e_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/e2e_test/kubeconfig"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
var (
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
)
// Run the integration tests of the Login use-case.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the kubeconfig.
//
func TestCmd_Run_Standalone(t *testing.T) {
timeout := 5 * time.Second
type testParameter struct {
startServer func(t *testing.T, h http.Handler) (string, localserver.Shutdowner)
kubeconfigIDPCertificateAuthority string
clientTLSConfig *tls.Config
}
testParameters := map[string]testParameter{
"NoTLS": {
startServer: localserver.Start,
},
"CACert": {
startServer: func(t *testing.T, h http.Handler) (string, localserver.Shutdowner) {
return localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, h)
},
kubeconfigIDPCertificateAuthority: keys.TLSCACert,
clientTLSConfig: keys.TLSCACertAsConfig,
},
}
runTest := func(t *testing.T, p testParameter) {
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
}
for name, p := range testParameters {
t.Run(name, func(t *testing.T) {
runTest(t, p)
})
}
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
"--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "profile groups openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
}
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: expiry.Unix(),
}
claims.Nonce = nonce
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
var nonce string
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
service.EXPECT().Exchange("YOUR_AUTH_CODE").
DoAndReturn(func(string) (*idp.TokenResponse, error) {
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
})
}
func runCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, args ...string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, nil)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func openBrowserOnReadyFunc(t *testing.T, ctx context.Context, clientConfig *tls.Config) authentication.LocalServerReadyFunc {
return func(url string) {
client := http.Client{Transport: &http.Transport{TLSClientConfig: clientConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

32
go.mod
View File

@@ -3,23 +3,25 @@ module github.com/int128/kubelogin
go 1.12
require (
github.com/coreos/go-oidc v2.1.0+incompatible
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda
github.com/go-test/deep v1.0.4
github.com/golang/mock v1.3.1
github.com/google/wire v0.3.0
github.com/int128/oauth2cli v1.8.1
github.com/chromedp/chromedp v0.5.3
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.2
github.com/google/wire v0.4.0
github.com/int128/oauth2cli v1.13.0
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.2.5
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
k8s.io/klog v0.4.0
gopkg.in/yaml.v2 v2.3.0
k8s.io/apimachinery v0.19.0
k8s.io/client-go v0.19.0
k8s.io/klog v1.0.0
)

616
go.sum
View File

@@ -1,168 +1,628 @@
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/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
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.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
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/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/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac h1:T7V5BXqnYd55Hj/g5uhDYumg9Fp3rMTS6bykYtTIFX4=
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
github.com/chromedp/chromedp v0.5.3 h1:F9LafxmYpsQhWQBdCs+6Sret1zzeeFyHS5LkRF//Ffg=
github.com/chromedp/chromedp v0.5.3/go.mod h1:YLdPtndaHQ4rCpSpBG+IPpy9JvX0VD+7aaLxYgYj28w=
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/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda h1:NyywMz59neOoVRFDz+ccfKWxn784fiHMDnZSy6T+JXY=
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
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/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
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 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
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/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60=
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
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/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/int128/listener v1.0.0 h1:a9H3m4jbXgXpxJUK3fxWrh37Iic/UU/kYOGE0WtjbbI=
github.com/int128/listener v1.0.0/go.mod h1:sho0rrH7mNRRZH4hYOYx+xwRDGmtRndaUiu2z9iumes=
github.com/int128/oauth2cli v1.7.0 h1:lguQEIJ4IcSFRTqQ6y7avnfvPqVe0U6dlkW8mC1Epts=
github.com/int128/oauth2cli v1.7.0/go.mod h1:bucNn0/es9IhOf0a2MWPvJ5xO5f6JYrCfitQTyjI5lA=
github.com/int128/oauth2cli v1.8.1 h1:Vkmfx0w225l4qUpJ1ZWGw1elw7hnXAybSiYoYyh1iBw=
github.com/int128/oauth2cli v1.8.1/go.mod h1:MkxKWhHUaPOaq/92Z5ifdCWySAKJKo04hUXaKA7OgDE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/int128/listener v1.1.0 h1:2Jb41DWLpkQ3I9bIdBzO8H/tNwMvyl/OBZWtCV5Pjuw=
github.com/int128/listener v1.1.0/go.mod h1:68WkmTN8PQtLzc9DucIaagAKeGVyMnyyKIkW4Xn47UA=
github.com/int128/oauth2cli v1.13.0 h1:3wR48gSHdOPFgHmtnXzs4wEFA6p24prPqLXu6QUOujw=
github.com/int128/oauth2cli v1.13.0/go.mod h1:EUpEzcUumoVjLPHD7y2KGxwfXYqxDVqwDfqQ+u7qAwM=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
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=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
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-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
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 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
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 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
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 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
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-20191005200804-aed5e4c7ecf9/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-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
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=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
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 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
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 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
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 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
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.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
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 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
k8s.io/api v0.0.0-20190620084959-7cf5895f2711 h1:BblVYz/wE5WtBsD/Gvu54KyBUTJMflolzc5I2DTvh50=
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9L4wrirlwYJ6w88jUQxTUw=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc=
k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw=
k8s.io/apimachinery v0.19.0 h1:gjKnAda/HZp5k4xQYjL0K/Yb66IvNqjthCb03QlKpaQ=
k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k=
k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg=
k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
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=
sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA=
sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

View File

@@ -0,0 +1,420 @@
package integration_test
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/integration_test/httpdriver"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)
// Run the integration tests of the credential plugin use-case.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the output.
//
func TestCredentialPlugin(t *testing.T) {
timeout := 3 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
tokenCacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
}
defer func() {
if err := os.RemoveAll(tokenCacheDir); err != nil {
t.Errorf("could not clean up the cache dir: %s", err)
}
}()
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
args []string
}{
"NoTLS": {},
"TLS": {
keyPair: keypair.Server,
args: []string{"--certificate-authority", keypair.Server.CACertPath},
},
} {
httpDriverOption := httpdriver.Option{
TLSConfig: tc.keyPair.TLSConfig,
BodyContains: "Authenticated",
}
t.Run(name, func(t *testing.T) {
t.Run("AuthCode", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ROPC", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
Username: "USER1",
Password: "PASS1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.Zero(t),
now: now,
stdout: &stdout,
args: append([]string{
"--username", "USER1",
"--password", "PASS1",
}, tc.args...),
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("TokenCacheLifecycle", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{})
defer sv.Shutdown(t, ctx)
t.Run("NoCache", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
RefreshToken: "REFRESH_TOKEN_1",
},
})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("Valid", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.Zero(t),
now: now,
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("Refresh", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(3 * time.Hour),
RefreshToken: "REFRESH_TOKEN_2",
},
})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(2 * time.Hour),
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(3*time.Hour))
})
t.Run("RefreshAgain", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_2",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(5 * time.Hour),
},
})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(4 * time.Hour),
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(5*time.Hour))
})
})
})
}
t.Run("PKCE", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
CodeChallengeMethod: "S256",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
CodeChallengeMethodsSupported: []string{"plain", "S256"},
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("TLSData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig, BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{"--certificate-authority-data", keypair.Server.CACertBase64},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "email profile openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("OpenURLAfterAuthentication", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "URL=https://example.com/success"}),
now: now,
stdout: &stdout,
args: []string{"--open-url-after-authentication", "https://example.com/success"},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("RedirectURLHostname", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://127.0.0.1:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{"--oidc-redirect-url-hostname", "127.0.0.1"},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ExtraParams", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
ExtraParams: map[string]string{
"ttl": "86400",
"reauth": "false",
},
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=false",
},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
}
type getTokenConfig struct {
tokenCacheDir string
issuerURL string
httpDriver browser.Interface
stdout io.Writer
now time.Time
args []string
}
func runGetToken(t *testing.T, ctx context.Context, cfg getTokenConfig) {
cmd := di.NewCmdForHeadless(clock.Fake(cfg.now), os.Stdin, cfg.stdout, logger.New(t), cfg.httpDriver)
exitCode := cmd.Run(ctx, append([]string{
"kubelogin",
"get-token",
"--token-cache-dir", cfg.tokenCacheDir,
"--oidc-issuer-url", cfg.issuerURL,
"--oidc-client-id", "kubernetes",
"--listen-address", "127.0.0.1:0",
}, cfg.args...), "latest")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func assertCredentialPluginStdout(t *testing.T, stdout io.Reader, token string, expiry time.Time) {
var got clientauthenticationv1beta1.ExecCredential
if err := json.NewDecoder(stdout).Decode(&got); err != nil {
t.Errorf("could not decode json of the credential plugin: %s", err)
return
}
want := clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: token,
ExpirationTimestamp: &metav1.Time{Time: expiry},
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
}
}

View File

@@ -0,0 +1,70 @@
// Package httpdriver provides a test double of the browser.
package httpdriver
import (
"context"
"crypto/tls"
"io/ioutil"
"net/http"
"strings"
"testing"
)
type Option struct {
TLSConfig *tls.Config
BodyContains string
}
// New returns a client to simulate browser access.
func New(ctx context.Context, t *testing.T, o Option) *client {
return &client{ctx, t, o}
}
// Zero returns a client which call is not expected.
func Zero(t *testing.T) *zeroClient {
return &zeroClient{t}
}
type client struct {
ctx context.Context
t *testing.T
o Option
}
func (c *client) Open(url string) error {
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.o.TLSConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
c.t.Errorf("could not create a request: %s", err)
return nil
}
req = req.WithContext(c.ctx)
resp, err := client.Do(req)
if err != nil {
c.t.Errorf("could not send a request: %s", err)
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
c.t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.t.Errorf("could not read body: %s", err)
return nil
}
body := string(b)
if !strings.Contains(body, c.o.BodyContains) {
c.t.Errorf("body should contain %s but was %s", c.o.BodyContains, body)
}
return nil
}
type zeroClient struct {
t *testing.T
}
func (c *zeroClient) Open(url string) error {
c.t.Errorf("unexpected function call Open(%s)", url)
return nil
}

View File

@@ -0,0 +1,62 @@
package keypair
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io"
"io/ioutil"
"os"
"strings"
)
// KeyPair represents a pair of certificate and key.
type KeyPair struct {
CertPath string
KeyPath string
CACertPath string
CACertBase64 string
TLSConfig *tls.Config
}
// None represents non-TLS.
var None KeyPair
// Server is a KeyPair for TLS server.
// These files should be generated by Makefile before test.
var Server = KeyPair{
CertPath: "keypair/testdata/server.crt",
KeyPath: "keypair/testdata/server.key",
CACertPath: "keypair/testdata/ca.crt",
CACertBase64: readAsBase64("keypair/testdata/ca.crt"),
TLSConfig: newTLSConfig("keypair/testdata/ca.crt"),
}
func readAsBase64(name string) string {
f, err := os.Open(name)
if err != nil {
panic(err)
}
defer f.Close()
var s strings.Builder
e := base64.NewEncoder(base64.StdEncoding, &s)
if _, err := io.Copy(e, f); err != nil {
panic(err)
}
if err := e.Close(); err != nil {
panic(err)
}
return s.String()
}
func newTLSConfig(name string) *tls.Config {
b, err := ioutil.ReadFile(name)
if err != nil {
panic(err)
}
p := x509.NewCertPool()
if !p.AppendCertsFromPEM(b) {
panic("could not append the CA cert")
}
return &tls.Config{RootCAs: p}
}

View File

@@ -0,0 +1,26 @@
EXPIRY := 3650
OUTPUT_DIR := testdata
TARGETS := ca.key
TARGETS += ca.crt
TARGETS += server.key
TARGETS += server.crt
.PHONY: all
all: $(TARGETS)
ca.key:
openssl genrsa -out $@ 2048
ca.csr: ca.key
openssl req -new -key ca.key -out $@ -subj "/CN=hello-ca" -config openssl.cnf
ca.crt: ca.key ca.csr
openssl x509 -req -in ca.csr -signkey ca.key -out $@ -days $(EXPIRY)
server.key:
openssl genrsa -out $@ 2048
server.csr: openssl.cnf server.key
openssl req -new -key server.key -out $@ -subj "/CN=localhost" -config openssl.cnf
server.crt: openssl.cnf server.csr ca.crt ca.key
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out $@ -sha256 -days $(EXPIRY) -extensions v3_req -extfile openssl.cnf
.PHONY: clean
clean:
-rm -v $(TARGETS)

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICojCCAYoCCQCNsdXicWqF2DANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAho
ZWxsby1jYTAeFw0yMDAyMDYwMTMzMzNaFw0zMDAyMDMwMTMzMzNaMBMxETAPBgNV
BAMMCGhlbGxvLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxcPN
dS88B6ewqVn/m9yO74OLIwNrqMci8l7olP9XlcJhxUZs+3WZQpsSj5nC4yEx8uPQ
bTtBKnXXVDe+8k7OsLTruu9+isTaYk4o/TZbuw/N31ZAiT0pJw8hdypTQyMLbeDr
Vl4bbrfbYywx30DyrHxUkgzOWs459Uwc1wWu0W7M21GY4KENHFE3OAcD58FMvvrh
vgkslATwwW4M2UtXUFJ8XHh26g/J450DU2gwNxpcSdIsvFE6zSyAxU55RElph7mE
ru9cNWAYhCRZvlZQ2VlH7C6JQ3SHyA9RZmBbPpXhtl9zavFkGx2MEwDp/3FmUukR
yJnS2KnAo0QdBeS1LQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAXGfoDwuKY4TyF
fhKg553Y5I6VDCDc97jyW9yyfc0kvPjQc4EGQV+1eQqMSeh2THpgEEKk9hH/g35a
grPRcBTsEWpbQd16yWyulQeyOtPeWZB2FvAigMaAdMmeXlTs6++gJ6PjPuACa2Jl
nJ/AjCqKFxkn0yEVkPTY0c/I9A12xhCmATqIrQiK1pPowiFxQb4M8Cm0z0AkaJZr
iW0NCOJlLzBqRpFquL4umNaIsxTmOshfM70NpQGRjKREBuK6S0qWsRR0wz4b9Rvi
62qW4zU94q2EDIoCItjHP4twGENXJDC0vLCsKfA5AvbPzszd5/4ifYe2C00Rn7/O
lIxrspMm
-----END CERTIFICATE-----

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICrDCCAZQCAQAwEzERMA8GA1UEAwwIaGVsbG8tY2EwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDFw811LzwHp7CpWf+b3I7vg4sjA2uoxyLyXuiU/1eV
wmHFRmz7dZlCmxKPmcLjITHy49BtO0EqdddUN77yTs6wtOu6736KxNpiTij9Nlu7
D83fVkCJPSknDyF3KlNDIwtt4OtWXhtut9tjLDHfQPKsfFSSDM5azjn1TBzXBa7R
bszbUZjgoQ0cUTc4BwPnwUy++uG+CSyUBPDBbgzZS1dQUnxceHbqD8njnQNTaDA3
GlxJ0iy8UTrNLIDFTnlESWmHuYSu71w1YBiEJFm+VlDZWUfsLolDdIfID1FmYFs+
leG2X3Nq8WQbHYwTAOn/cWZS6RHImdLYqcCjRB0F5LUtAgMBAAGgVDBSBgkqhkiG
9w0BCQ4xRTBDMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMBQGA1UdEQQNMAuCCWxv
Y2FsaG9zdDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA
o/Uthp3NWx0ydTn/GBZI+vA3gI7Qd9UwLvwkeinldRlPM9DOXpG6LR0C40f6u+fj
mXvUDerveYJI8rpo+Ds0UVqy63AH/zZLG7M96L5Nv2KnK40bkfVNez858Yqp1u17
/ci1ZsQIElU5v2qKozaHdQThDVtD5ZZdZoQwLvBLE/Dwpe/4VZZFh8smPMR+Mhcq
+b7gpSy1RiUffk0ZMjuF9Nc9OODdQMTCf+86i0qWXGVzkhfHKAGv+xarHEztcmxF
GUgUYW8DMvYBjEzaGRM1n0aIFkQO6y8SUXvGMIGBSMC4jfBH3ghIXg1+nD6Uah4D
16r+CLjFsDUdn/DK/fIQVw==
-----END CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxcPNdS88B6ewqVn/m9yO74OLIwNrqMci8l7olP9XlcJhxUZs
+3WZQpsSj5nC4yEx8uPQbTtBKnXXVDe+8k7OsLTruu9+isTaYk4o/TZbuw/N31ZA
iT0pJw8hdypTQyMLbeDrVl4bbrfbYywx30DyrHxUkgzOWs459Uwc1wWu0W7M21GY
4KENHFE3OAcD58FMvvrhvgkslATwwW4M2UtXUFJ8XHh26g/J450DU2gwNxpcSdIs
vFE6zSyAxU55RElph7mEru9cNWAYhCRZvlZQ2VlH7C6JQ3SHyA9RZmBbPpXhtl9z
avFkGx2MEwDp/3FmUukRyJnS2KnAo0QdBeS1LQIDAQABAoIBAHSoWtMsaMnPNlu/
thM32K0auIGP6/rkdQ3pxGLX+M9jmY7oSzNOHHj4xssklZyroS45CmLU2Ez2tG1+
cMm4iR4dqwxbaBbtpjDlEDLF1PiUiwmadHlANb1PpJsJwZHR41UOn2QUITR/ig+H
K2gZhM0QjkaU/Uj9a5zyJ/UC6iupmgCtj8ij65B49qKMODxV4gqZstRSZiJ3gb6R
TBeR3PUWQS62MZueEQz2eF0eXkXKsFWcbLfHArjfIJ533zW68A0vabgqOhCwJTks
+rkyaKUjEwJJQcgpCfUI4t9HAwICqYtw0fQdDDaMak2XTDFnGHn1/VfSi818U5Sa
jZ+/uIECgYEA4uCd5ISsLWebSv2r5f97N8+WcW0MSJ0XP5MniiMvlaNOtJmoN3H0
PlgR3uwpH9TNWHITZEEb/I93r2E3f24v2018g0uJuwiRDusxHs7yuxw/93Y5rwtG
3twL1k0GyeLCxfM2y2QQU/awXISx7nNk2f0umvTWmjrEw632oQSWCSECgYEA3yaF
zDk7k1u1GdZAh+pQAyUtjzHSvQjiE2JzLaH8BTorACrczRascX5yyMi0QfLYnoYt
UL9dCb4Z8HUclj55kBSx8anvVtf3XAT3hZ3sm8LV3JLGDeURGS8rGdV+tyFk7zw9
XgvaLj3xjB5CoJvtOhbXvqF9M9yp4TMBb5El7o0CgYBTx5Bm15thNPZCrgQxbbOJ
u42ZmyRDGEeCgYvDVhT3VBP3WxqkRt9juk/3GwxgpcuikpWYmvaDwFL5H5RH6V+g
wy9sqJNWzuYKNU2xS8iU0ezJLA5HFon4OBfi7hTIroUwZgzg9LWW2+zqbVHrdQ9T
9Eumiy1ITNVmUTJW6YOiIQKBgBE4BsEAdZFkVTAeMTKLqQrlFoPjI1DE27UFNsAB
rNG2cFT9+bW1ly7WxAKsQgSIuaBZ2CtP6Nz0l0nPr5oEThsJDcYJB9faqFKoa3Ua
/4PxX9E6Xh/6WfxogFno+HMnF4PCUTXtkjNZQkc+moOMJJ0D4DfsfB3BXDZtWiIC
wDuNAoGBAN5ZFTXwE1VpWxIWq5dV9+0aVOP/eB+h7Pt+u2xyRip31FGcpGEzGniu
VmOzWApW7NmvD0QWtstJWlCpsw+rOptAL0oVunlB70KuYuA+2Q5g/Cw3kUrvXqbe
mkCoV21eFAmpU+I6z/n7pUDXPuUsmTkzmwedv0HUuNSQJmHZOMPD
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1 @@
9E12C7A1AF348811

View File

@@ -0,0 +1,14 @@
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
extendedKeyUsage = serverAuth
[alt_names]
DNS.1 = localhost

View File

@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC7zCCAdegAwIBAgIJAJ4Sx6GvNIgRMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
BAMMCGhlbGxvLWNhMB4XDTIwMDIwNjAxMzMzM1oXDTMwMDIwMzAxMzMzM1owFDES
MBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F7yVyphLtJr+/AxI8k5Er
MFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4TKyVaU0XK41ZSbDAchbW
349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTkC3gSVF92+TzrrlbkZ5+W
YVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDgkGuwwhzXfVUpxJ/DYLXZ
yHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV+2ovP0Nggn6XJNh/g1QK
o4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABo0UwQzAJBgNVHRMEAjAAMAsGA1UdDwQE
AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
DQYJKoZIhvcNAQELBQADggEBACqlq8b7trNRtKUm1PbY7dnrAFCOnV4OT2R98s17
Q6tmCXM1DvQ101W0ih/lh6iPyU4JM2A0kvO+gizuL/Dmvb6oh+3ox0mMLposptso
gCE1K3SXlvlcLdM6hXRJ5+XwlSCHM6o2Y4yABnKjT6Zr+CMh86a5abDx33hkJ4QR
6I+/iBHVLiCVv0wUF3jD/T+HxinEQrB4cQsSgKmfPClrc8n7rkWWE+MdwEL4VZCH
ufabw178aYibVvJ8k3rushjLlftkDyCNno2rz8YWrnaxabVR0EqSPInyYQenmv2r
/CedVKpmdH2RV8ubkwqa0s6cmpRkyu6FS2g3LviJhmm0nwQ=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICrTCCAZUCAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F
7yVyphLtJr+/AxI8k5ErMFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4
TKyVaU0XK41ZSbDAchbW349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTk
C3gSVF92+TzrrlbkZ5+WYVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDg
kGuwwhzXfVUpxJ/DYLXZyHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV
+2ovP0Nggn6XJNh/g1QKo4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABoFQwUgYJKoZI
hvcNAQkOMUUwQzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAUBgNVHREEDTALggls
b2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEB
ACUQ0j3XuUZY3fso5tEgOGVjVsTU+C4pIsEw3e1KyIB93i56ANKaq1uBLtnlWtYi
R4RxZ3Sf08GzoEHAHpWoQ4BQ3WGDQjxSdezbudWMuNnNyvyhkh36tmmp/PLA4iZD
Q/d1odzGWg1HMqY0/Q3hfz40MQ9IEBBm+5zKw3tLsKNIKdSdlY7Ul3Z9PUsqsOVW
uF0LKsTMEh1CpbYnOBS2EQjComVM5kYfdQwDNh+BMok8rH7mHFYRmrwYUrU9njsM
eoKqhLkoSu6hw1Cgd9Yru5lC541KfxsSN4Cj6rkm+Qv/5zjvIPxYZ+akSQQR9hR3
2O9THHKqaQS0xmD+y8NjfYk=
-----END CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F7yVyphLt
Jr+/AxI8k5ErMFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4TKyVaU0X
K41ZSbDAchbW349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTkC3gSVF92
+TzrrlbkZ5+WYVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDgkGuwwhzX
fVUpxJ/DYLXZyHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV+2ovP0Ng
gn6XJNh/g1QKo4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABAoIBAEYvWFb4C0wurOj2
ABrvhP2Ekaesh4kxMp+zgTlqxzsTJnWarS/gjKcPBm9KJon3La3P3tnd7y3pBXcV
2F0IBl4DTHRpBTUS6HBVnENc8LnJgK2dHZkOLPyYtRLrA0Et+A73PQ2hNmchC+5V
b9K9oQH0Pvim1gCHocnrSIi480hQPazO9/gnHvFtu9Tdsx9kM9jgChhh5VaR+nzM
uw6MpUSzCri3/W6K5Hz8F1lLQcZ6o3vyyFtLPjFrWT4J1UiHqAoxuCGeWTvMbGQl
9Cg0SMX4FLBpbIoMonhM3hb9Cw3FVRGU/1i4oF+gEIGlX/v9CPT5HAZmD59rawc8
11x7yTECgYEA8EYVGKauLHeI1nJdCoVrWI73W1wtwt6prJbzltpoAR4tb3Qu6gBX
JQNd+Ifhl28lK6lPnCGk/SsmfiKSp/XE4IyS1GBd2LpxWj5R50GePCg4hAzk9Xyx
M9SJAFQw73pODgu4RWFXTCOMo9crahJ6X/O+MckHqbFqqohJ8nChGDkCgYEA6oro
Ql/ymO7UdYCay7OlzKeg8ud6XgkZ+wkpSG/5TQ0QZWVN3aSOLztEdVfh3PAqWob0
KgVhLmq6CYwq+HSzU92bvFqCgYUMU8tYzeRboGLxyEdAGL+EW0j8wQyZKYR5cNz4
yM0hpM6kbJ0zZqkYVM/XfRT2RTGwTMakF/FOHZcCgYEAmf4guTriuIcoAWEstniK
Myj16ezrO1Df6Eia+B0kuUqxDhSlmL39HDDLQmU8NYU7in8qEcQSbVwBgKgB3HoM
42nVFR5qJ2RfD9qPPar1klKo3iExgRCYtcJKyBYtgt6dNi1WvcjEXX0PP1bBcWtE
WUjrphbUvXKDDabp1eNPrCkCgYBOw/F2APTeyS4Oe+8AQ8eFcDIMARLGK7ZO6Oe1
TO1jI+UCuD+rFI0vbW7zHV1brkf6+OFcj0vwo6TweeMgZ0il/IFFgvva9UyLg3nC
Q1NGDJR4Fv1+kiqn4V4IkuuI1tVVws/F16XZzA/J7g0KB/WE3fvXJMgDuskjL36C
D+aU5wKBgQCvEg+mJYny1QiR/mQuowX34xf4CkMl7Xq9YDH7W/3AlwuPrPNHaZjh
SvSCz9I8vV0E4ur6atazgCblnvA/G3d4r8YYx+e1l30WJHMgoZRxxHMH74tmUPAj
Klic16BJikSQclMeFdlJqf2UHd37eEuYnxorpep8YGP7/eN1ghHWAw==
-----END RSA PRIVATE KEY-----

View File

@@ -1,5 +1,5 @@
// Package idp provides a test double of the identity provider of OpenID Connect.
package idp
// Package handler provides a HTTP handler for the OpenID Connect Provider.
package handler
import (
"encoding/json"
@@ -10,16 +10,16 @@ import (
"golang.org/x/xerrors"
)
func NewHandler(t *testing.T, service Service) *Handler {
return &Handler{t, service}
func New(t *testing.T, provider Provider) *Handler {
return &Handler{t, provider}
}
// Handler provides a HTTP handler for the identity provider of OpenID Connect.
// You need to implement the Service interface.
// Handler provides a HTTP handler for the OpenID Connect Provider.
// You need to implement the Provider interface.
// Note that this skips some security checks and is only for testing.
type Handler struct {
t *testing.T
service Service
t *testing.T
provider Provider
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -58,25 +58,31 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
p := r.URL.Path
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
discoveryResponse := h.service.Discovery()
discoveryResponse := h.provider.Discovery()
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(discoveryResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/certs":
certificatesResponse := h.service.GetCertificates()
certificatesResponse := h.provider.GetCertificates()
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(certificatesResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/auth":
// 3.1.2.1. Authentication Request
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
q := r.URL.Query()
redirectURI, scope, state, nonce := q.Get("redirect_uri"), q.Get("scope"), q.Get("state"), q.Get("nonce")
code, err := h.service.AuthenticateCode(scope, nonce)
redirectURI, state := q.Get("redirect_uri"), q.Get("state")
code, err := h.provider.AuthenticateCode(AuthenticationRequest{
RedirectURI: redirectURI,
State: state,
Scope: q.Get("scope"),
Nonce: q.Get("nonce"),
CodeChallenge: q.Get("code_challenge"),
CodeChallengeMethod: q.Get("code_challenge_method"),
RawQuery: q,
})
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
@@ -89,10 +95,10 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
grantType := r.Form.Get("grant_type")
switch grantType {
case "authorization_code":
// 3.1.3.1. Token Request
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
code := r.Form.Get("code")
tokenResponse, err := h.service.Exchange(code)
tokenResponse, err := h.provider.Exchange(TokenRequest{
Code: r.Form.Get("code"),
CodeVerifier: r.Form.Get("code_verifier"),
})
if err != nil {
return xerrors.Errorf("token request error: %w", err)
}
@@ -105,7 +111,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
// 4.3. Resource Owner Password Credentials Grant
// https://tools.ietf.org/html/rfc6749#section-4.3
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
tokenResponse, err := h.service.AuthenticatePassword(username, password, scope)
tokenResponse, err := h.provider.AuthenticatePassword(username, password, scope)
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
@@ -118,7 +124,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
// 12.1. Refresh Request
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
refreshToken := r.Form.Get("refresh_token")
tokenResponse, err := h.service.Refresh(refreshToken)
tokenResponse, err := h.provider.Refresh(refreshToken)
if err != nil {
return xerrors.Errorf("token refresh error: %w", err)
}

View File

@@ -1,23 +1,19 @@
package idp
//go:generate mockgen -destination mock_idp/mock_service.go github.com/int128/kubelogin/e2e_test/idp Service
package handler
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
"net/url"
)
// Service provides discovery and authentication methods.
// Provider provides discovery and authentication methods.
// If an implemented method returns an ErrorResponse,
// the handler will respond 400 and corresponding json of the ErrorResponse.
// Otherwise, the handler will respond 500 and fail the current test.
type Service interface {
type Provider interface {
Discovery() *DiscoveryResponse
GetCertificates() *CertificatesResponse
AuthenticateCode(scope, nonce string) (code string, err error)
Exchange(code string) (*TokenResponse, error)
AuthenticateCode(req AuthenticationRequest) (code string, err error)
Exchange(req TokenRequest) (*TokenResponse, error)
AuthenticatePassword(username, password, scope string) (*TokenResponse, error)
Refresh(refreshToken string) (*TokenResponse, error)
}
@@ -38,26 +34,6 @@ type DiscoveryResponse struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
// NewDiscoveryResponse returns a DiscoveryResponse for the local server.
// This is based on https://accounts.google.com/.well-known/openid-configuration.
func NewDiscoveryResponse(issuer string) *DiscoveryResponse {
return &DiscoveryResponse{
Issuer: issuer,
AuthorizationEndpoint: issuer + "/auth",
TokenEndpoint: issuer + "/token",
JwksURI: issuer + "/certs",
UserinfoEndpoint: issuer + "/userinfo",
RevocationEndpoint: issuer + "/revoke",
ResponseTypesSupported: []string{"code id_token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
ScopesSupported: []string{"openid", "email", "profile"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
CodeChallengeMethodsSupported: []string{"plain", "S256"},
ClaimsSupported: []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
}
}
type CertificatesResponse struct {
Keys []*CertificatesResponseKey `json:"keys"`
}
@@ -71,21 +47,23 @@ type CertificatesResponseKey struct {
E string `json:"e"`
}
// NewCertificatesResponse returns a CertificatesResponse using the key pair.
// This is used for verifying a signature of ID token.
func NewCertificatesResponse(idTokenKeyPair *rsa.PrivateKey) *CertificatesResponse {
return &CertificatesResponse{
Keys: []*CertificatesResponseKey{
{
Kty: "RSA",
Alg: "RS256",
Use: "sig",
Kid: "dummy",
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
N: base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
},
},
}
// AuthenticationRequest represents a type of:
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type AuthenticationRequest struct {
RedirectURI string
State string
Scope string // space separated string
Nonce string
CodeChallenge string
CodeChallengeMethod string
RawQuery url.Values
}
// TokenRequest represents a type of:
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
type TokenRequest struct {
Code string
CodeVerifier string
}
type TokenResponse struct {
@@ -96,17 +74,6 @@ type TokenResponse struct {
IDToken string `json:"id_token"`
}
// NewTokenResponse returns a TokenResponse.
func NewTokenResponse(idToken, refreshToken string) *TokenResponse {
return &TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
IDToken: idToken,
RefreshToken: refreshToken,
}
}
// ErrorResponse represents an error response described in the following section:
// 5.2 Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2

View File

@@ -1,13 +1,13 @@
// Package localserver provides a http server running on localhost.
// This is only for testing.
//
package localserver
// Package http provides a http server running on localhost for testing.
package http
import (
"context"
"net"
"net/http"
"testing"
"github.com/int128/kubelogin/integration_test/keypair"
)
type Shutdowner interface {
@@ -15,20 +15,23 @@ type Shutdowner interface {
}
type shutdowner struct {
l net.Listener
s *http.Server
}
func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
// s.Shutdown() closes the lister as well,
// so we do not need to call l.Close() explicitly
if err := s.s.Shutdown(ctx); err != nil {
t.Errorf("Could not shutdown the server: %s", err)
t.Errorf("could not shutdown the server: %s", err)
}
}
// Start starts an authentication server.
func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
func Start(t *testing.T, h http.Handler, k keypair.KeyPair) (string, Shutdowner) {
if k == keypair.None {
return startNoTLS(t, h)
}
return startTLS(t, h, k)
}
func startNoTLS(t *testing.T, h http.Handler) (string, *shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
@@ -41,11 +44,10 @@ func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
t.Error(err)
}
}()
return url, &shutdowner{l, s}
return url, &shutdowner{s}
}
// Start starts an authentication server with TLS.
func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Shutdowner) {
func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) (string, *shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
@@ -53,12 +55,12 @@ func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Sh
Handler: h,
}
go func() {
err := s.ServeTLS(l, cert, key)
err := s.ServeTLS(l, k.CertPath, k.KeyPath)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return url, &shutdowner{l, s}
return url, &shutdowner{s}
}
func newLocalhostListener(t *testing.T) (net.Listener, string) {

View File

@@ -0,0 +1,217 @@
// Package oidcserver provides a stub of OpenID Connect provider.
package oidcserver
import (
"crypto/sha256"
"encoding/base64"
"math/big"
"strings"
"testing"
"time"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/oidcserver/handler"
"github.com/int128/kubelogin/integration_test/oidcserver/http"
"github.com/int128/kubelogin/pkg/testing/jwt"
"golang.org/x/xerrors"
)
type Server interface {
http.Shutdowner
IssuerURL() string
SetConfig(Config)
LastTokenResponse() *handler.TokenResponse
}
// Want represents a set of expected values.
type Want struct {
Scope string
RedirectURIPrefix string
CodeChallengeMethod string // optional
ExtraParams map[string]string // optional
Username string // optional
Password string // optional
RefreshToken string // optional
}
// Response represents a set of response values.
type Response struct {
IDTokenExpiry time.Time
RefreshToken string
RefreshError string // if set, Refresh() will return the error
CodeChallengeMethodsSupported []string // optional
}
// Config represents a configuration of the OpenID Connect provider.
type Config struct {
Want Want
Response Response
}
// New starts a HTTP server for the OpenID Connect provider.
func New(t *testing.T, k keypair.KeyPair, c Config) Server {
sv := server{Config: c, t: t}
sv.issuerURL, sv.Shutdowner = http.Start(t, handler.New(t, &sv), k)
return &sv
}
type server struct {
Config
http.Shutdowner
t *testing.T
issuerURL string
lastAuthenticationRequest *handler.AuthenticationRequest
lastTokenResponse *handler.TokenResponse
}
func (sv *server) IssuerURL() string {
return sv.issuerURL
}
func (sv *server) SetConfig(cfg Config) {
sv.Config = cfg
}
func (sv *server) LastTokenResponse() *handler.TokenResponse {
return sv.lastTokenResponse
}
func (sv *server) Discovery() *handler.DiscoveryResponse {
// based on https://accounts.google.com/.well-known/openid-configuration
return &handler.DiscoveryResponse{
Issuer: sv.issuerURL,
AuthorizationEndpoint: sv.issuerURL + "/auth",
TokenEndpoint: sv.issuerURL + "/token",
JwksURI: sv.issuerURL + "/certs",
UserinfoEndpoint: sv.issuerURL + "/userinfo",
RevocationEndpoint: sv.issuerURL + "/revoke",
ResponseTypesSupported: []string{"code id_token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
ScopesSupported: []string{"openid", "email", "profile"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
CodeChallengeMethodsSupported: sv.Config.Response.CodeChallengeMethodsSupported,
ClaimsSupported: []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
}
}
func (sv *server) GetCertificates() *handler.CertificatesResponse {
idTokenKeyPair := jwt.PrivateKey
return &handler.CertificatesResponse{
Keys: []*handler.CertificatesResponseKey{
{
Kty: "RSA",
Alg: "RS256",
Use: "sig",
Kid: "dummy",
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
N: base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
},
},
}
}
func (sv *server) AuthenticateCode(req handler.AuthenticationRequest) (code string, err error) {
if req.Scope != sv.Want.Scope {
sv.t.Errorf("scope wants `%s` but was `%s`", sv.Want.Scope, req.Scope)
}
if !strings.HasPrefix(req.RedirectURI, sv.Want.RedirectURIPrefix) {
sv.t.Errorf("redirectURI wants prefix `%s` but was `%s`", sv.Want.RedirectURIPrefix, req.RedirectURI)
}
if req.CodeChallengeMethod != sv.Want.CodeChallengeMethod {
sv.t.Errorf("code_challenge_method wants `%s` but was `%s`", sv.Want.CodeChallengeMethod, req.CodeChallengeMethod)
}
for k, v := range sv.Want.ExtraParams {
got := req.RawQuery.Get(k)
if got != v {
sv.t.Errorf("parameter %s wants `%s` but was `%s`", k, v, got)
}
}
sv.lastAuthenticationRequest = &req
return "YOUR_AUTH_CODE", nil
}
func (sv *server) Exchange(req handler.TokenRequest) (*handler.TokenResponse, error) {
if req.Code != "YOUR_AUTH_CODE" {
return nil, xerrors.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.Code)
}
if sv.lastAuthenticationRequest.CodeChallengeMethod == "S256" {
// https://tools.ietf.org/html/rfc7636#section-4.6
challenge := computeS256Challenge(req.CodeVerifier)
if challenge != sv.lastAuthenticationRequest.CodeChallenge {
sv.t.Errorf("pkce S256 challenge did not match (want %s but was %s)", sv.lastAuthenticationRequest.CodeChallenge, challenge)
}
}
resp := &handler.TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
RefreshToken: sv.Response.RefreshToken,
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
claims.Issuer = sv.issuerURL
claims.Subject = "SUBJECT"
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
claims.Audience = []string{"kubernetes"}
claims.Nonce = sv.lastAuthenticationRequest.Nonce
}),
}
sv.lastTokenResponse = resp
return resp, nil
}
func computeS256Challenge(verifier string) string {
c := sha256.Sum256([]byte(verifier))
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(c[:])
}
func (sv *server) AuthenticatePassword(username, password, scope string) (*handler.TokenResponse, error) {
if scope != sv.Want.Scope {
sv.t.Errorf("scope wants `%s` but was `%s`", sv.Want.Scope, scope)
}
if username != sv.Want.Username {
sv.t.Errorf("username wants `%s` but was `%s`", sv.Want.Username, username)
}
if password != sv.Want.Password {
sv.t.Errorf("password wants `%s` but was `%s`", sv.Want.Password, password)
}
resp := &handler.TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
RefreshToken: sv.Response.RefreshToken,
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
claims.Issuer = sv.issuerURL
claims.Subject = "SUBJECT"
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
claims.Audience = []string{"kubernetes"}
}),
}
sv.lastTokenResponse = resp
return resp, nil
}
func (sv *server) Refresh(refreshToken string) (*handler.TokenResponse, error) {
if refreshToken != sv.Want.RefreshToken {
sv.t.Errorf("refreshToken wants %s but was %s", sv.Want.RefreshToken, refreshToken)
}
if sv.Response.RefreshError != "" {
return nil, &handler.ErrorResponse{Code: "invalid_request", Description: sv.Response.RefreshError}
}
resp := &handler.TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
RefreshToken: sv.Response.RefreshToken,
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
claims.Issuer = sv.issuerURL
claims.Subject = "SUBJECT"
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
claims.Audience = []string{"kubernetes"}
}),
}
sv.lastTokenResponse = resp
return resp, nil
}

View File

@@ -0,0 +1,334 @@
package integration_test
import (
"context"
"os"
"testing"
"time"
"github.com/int128/kubelogin/integration_test/httpdriver"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/kubeconfig"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
)
// Run the integration tests of the Login use-case.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the kubeconfig.
//
func TestStandalone(t *testing.T) {
timeout := 3 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
args []string
}{
"NoTLS": {},
"TLS": {
keyPair: keypair.Server,
},
} {
httpDriverOption := httpdriver.Option{
TLSConfig: tc.keyPair.TLSConfig,
BodyContains: "Authenticated",
}
t.Run(name, func(t *testing.T) {
t.Run("AuthCode", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthority: tc.keyPair.CACertPath,
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("ROPC", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
Username: "USER1",
Password: "PASS1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthority: tc.keyPair.CACertPath,
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.Zero(t),
now: now,
args: []string{
"--username", "USER1",
"--password", "PASS1",
},
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("TokenLifecycle", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthority: tc.keyPair.CACertPath,
})
defer os.Remove(kubeConfigFilename)
t.Run("NoToken", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
RefreshToken: "REFRESH_TOKEN_1",
},
})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_1",
})
})
t.Run("Valid", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.Zero(t),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_1",
})
})
t.Run("Refresh", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(3 * time.Hour),
RefreshToken: "REFRESH_TOKEN_2",
},
})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(2 * time.Hour),
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_2",
})
})
t.Run("RefreshAgain", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_2",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(5 * time.Hour),
},
})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(4 * time.Hour),
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_2",
})
})
})
})
}
t.Run("TLSData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthorityData: keypair.Server.CACertBase64,
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig}),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("env_KUBECONFIG", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "profile groups openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
}
type standaloneConfig struct {
issuerURL string
kubeConfigFilename string
httpDriver browser.Interface
now time.Time
args []string
}
func runStandalone(t *testing.T, ctx context.Context, cfg standaloneConfig) {
cmd := di.NewCmdForHeadless(clock.Fake(cfg.now), os.Stdin, os.Stdout, logger.New(t), cfg.httpDriver)
exitCode := cmd.Run(ctx, append([]string{
"kubelogin",
"--kubeconfig", cfg.kubeConfigFilename,
"--listen-address", "127.0.0.1:0",
}, cfg.args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

View File

@@ -0,0 +1,33 @@
package browser
import (
"os"
"github.com/google/wire"
"github.com/pkg/browser"
)
//go:generate mockgen -destination mock_browser/mock_browser.go github.com/int128/kubelogin/pkg/adaptors/browser Interface
func init() {
// In credential plugin mode, some browser launcher writes a message to stdout
// and it may break the credential json for client-go.
// This prevents the browser launcher from breaking the credential json.
browser.Stdout = os.Stderr
}
var Set = wire.NewSet(
wire.Struct(new(Browser)),
wire.Bind(new(Interface), new(*Browser)),
)
type Interface interface {
Open(url string) error
}
type Browser struct{}
// Open opens the default browser.
func (*Browser) Open(url string) error {
return browser.OpenURL(url)
}

View File

@@ -0,0 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/browser (interfaces: Interface)
// Package mock_browser is a generated GoMock package.
package mock_browser
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Open mocks base method.
func (m *MockInterface) Open(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Open", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Open indicates an expected call of Open.
func (mr *MockInterfaceMockRecorder) Open(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockInterface)(nil).Open), arg0)
}

View File

@@ -11,24 +11,19 @@ import (
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool FactoryInterface,Interface
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
wire.Value(NewFunc(New)),
wire.Struct(new(CertPool), "*"),
wire.Bind(new(Interface), new(*CertPool)),
)
type FactoryInterface interface {
New() Interface
}
type Factory struct{}
type NewFunc func() Interface
// New returns an instance which implements the Interface.
func (f *Factory) New() Interface {
func New() Interface {
return &CertPool{pool: x509.NewCertPool()}
}

View File

@@ -8,8 +8,7 @@ import (
func TestCertPool_AddFile(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
var f Factory
p := f.New()
p := New()
if err := p.AddFile("testdata/ca1.crt"); err != nil {
t.Errorf("AddFile error: %s", err)
}
@@ -20,8 +19,7 @@ func TestCertPool_AddFile(t *testing.T) {
}
})
t.Run("Invalid", func(t *testing.T) {
var f Factory
p := f.New()
p := New()
err := p.AddFile("testdata/Makefile")
if err == nil {
t.Errorf("AddFile wants an error but was nil")
@@ -30,8 +28,7 @@ func TestCertPool_AddFile(t *testing.T) {
}
func TestCertPool_AddBase64Encoded(t *testing.T) {
var f Factory
p := f.New()
p := New()
if err := p.AddBase64Encoded(readFile(t, "testdata/ca2.crt.base64")); err != nil {
t.Errorf("AddBase64Encoded error: %s", err)
}
@@ -43,8 +40,7 @@ func TestCertPool_AddBase64Encoded(t *testing.T) {
}
func TestCertPool_SetRootCAs(t *testing.T) {
var f Factory
p := f.New()
p := New()
var cfg tls.Config
p.SetRootCAs(&cfg)
if cfg.RootCAs != nil {

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/certpool (interfaces: FactoryInterface,Interface)
// Source: github.com/int128/kubelogin/pkg/adaptors/certpool (interfaces: Interface)
// Package mock_certpool is a generated GoMock package.
package mock_certpool
@@ -7,71 +7,33 @@ package mock_certpool
import (
tls "crypto/tls"
gomock "github.com/golang/mock/gomock"
certpool "github.com/int128/kubelogin/pkg/adaptors/certpool"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New() certpool.Interface {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New")
ret0, _ := ret[0].(certpool.Interface)
return ret0
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New))
}
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AddBase64Encoded mocks base method
// AddBase64Encoded mocks base method.
func (m *MockInterface) AddBase64Encoded(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddBase64Encoded", arg0)
@@ -79,13 +41,13 @@ func (m *MockInterface) AddBase64Encoded(arg0 string) error {
return ret0
}
// AddBase64Encoded indicates an expected call of AddBase64Encoded
// AddBase64Encoded indicates an expected call of AddBase64Encoded.
func (mr *MockInterfaceMockRecorder) AddBase64Encoded(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBase64Encoded", reflect.TypeOf((*MockInterface)(nil).AddBase64Encoded), arg0)
}
// AddFile mocks base method
// AddFile mocks base method.
func (m *MockInterface) AddFile(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddFile", arg0)
@@ -93,19 +55,19 @@ func (m *MockInterface) AddFile(arg0 string) error {
return ret0
}
// AddFile indicates an expected call of AddFile
// AddFile indicates an expected call of AddFile.
func (mr *MockInterfaceMockRecorder) AddFile(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFile", reflect.TypeOf((*MockInterface)(nil).AddFile), arg0)
}
// SetRootCAs mocks base method
// SetRootCAs mocks base method.
func (m *MockInterface) SetRootCAs(arg0 *tls.Config) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetRootCAs", arg0)
}
// SetRootCAs indicates an expected call of SetRootCAs
// SetRootCAs indicates an expected call of SetRootCAs.
func (mr *MockInterfaceMockRecorder) SetRootCAs(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRootCAs", reflect.TypeOf((*MockInterface)(nil).SetRootCAs), arg0)

View File

@@ -0,0 +1,24 @@
// Package clock provides the system clock.
package clock
import (
"time"
"github.com/google/wire"
)
var Set = wire.NewSet(
wire.Struct(new(Real), "*"),
wire.Bind(new(Interface), new(*Real)),
)
type Interface interface {
Now() time.Time
}
type Real struct{}
// Now returns the current time.
func (c *Real) Now() time.Time {
return time.Now()
}

View File

@@ -0,0 +1,87 @@
package cmd
import (
"fmt"
"strings"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
SkipOpenBrowser bool
OpenURLAfterAuthentication string
RedirectURLHostname string
AuthRequestExtraParams map[string]string
Username string
Password string
}
// determineListenAddress returns the addresses from the flags.
// Note that --listen-address is always given due to the default value.
// If --listen-port is not set, it returns --listen-address.
// If --listen-port is set, it returns the strings of --listen-port.
func (o *authenticationOptions) determineListenAddress() []string {
if len(o.ListenPort) == 0 {
return o.ListenAddress
}
var a []string
for _, p := range o.ListenPort {
a = append(a, fmt.Sprintf("127.0.0.1:%d", p))
}
return a
}
var allGrantType = strings.Join([]string{
"auto",
"authcode",
"authcode-keyboard",
"password",
}, "|")
func (o *authenticationOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("Authorization grant type to use. One of (%s)", allGrantType))
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "[authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order")
//TODO: remove the deprecated flag
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "[authcode] deprecated: port to bind to the local server")
if err := f.MarkDeprecated("listen-port", "use --listen-address instead"); err != nil {
panic(err)
}
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "[authcode] Do not open the browser automatically")
f.StringVar(&o.OpenURLAfterAuthentication, "open-url-after-authentication", "", "[authcode] If set, open the URL in the browser after authentication")
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "[authcode] Hostname of the redirect URL")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "[authcode, authcode-keyboard] Extra query parameters to send with an authentication request")
f.StringVar(&o.Username, "username", "", "[password] Username for resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "[password] Password for resource owner password credentials grant")
}
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeBrowserOption = &authcode.BrowserOption{
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
OpenURLAfterAuthentication: o.OpenURLAfterAuthentication,
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authcode.KeyboardOption{
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &ropc.Option{
Username: o.Username,
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}

View File

@@ -2,8 +2,7 @@ package cmd
import (
"context"
"fmt"
"path/filepath"
"runtime"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
@@ -24,16 +23,9 @@ type Interface interface {
Run(ctx context.Context, args []string, version string) int
}
var defaultListenPort = []int{8000, 18000}
var defaultListenAddress = []string{"127.0.0.1:8000", "127.0.0.1:18000"}
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
func translateListenPortToBindAddress(ports []int) (address []string) {
for _, p := range ports {
address = append(address, fmt.Sprintf("127.0.0.1:%d", p))
}
return
}
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Root *Root
@@ -45,17 +37,15 @@ type Cmd struct {
// Run parses the command line arguments and executes the specified use-case.
// It returns an exit code, that is 0 on success or 1 on error.
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
executable := filepath.Base(args[0])
rootCmd := cmd.Root.New(ctx, executable)
rootCmd := cmd.Root.New()
rootCmd.Version = version
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
getTokenCmd := cmd.GetToken.New(ctx)
getTokenCmd := cmd.GetToken.New()
rootCmd.AddCommand(getTokenCmd)
setupCmd := cmd.Setup.New(ctx)
setupCmd := cmd.Setup.New()
rootCmd.AddCommand(setupCmd)
versionCmd := &cobra.Command{
@@ -63,13 +53,13 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
Short: "Print the version information",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.Printf("%s version %s", executable, version)
cmd.Logger.Printf("kubelogin version %s (%s %s_%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
},
}
rootCmd.AddCommand(versionCmd)
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
if err := rootCmd.ExecuteContext(ctx); err != nil {
cmd.Logger.Printf("error: %s", err)
cmd.Logger.V(1).Infof("stacktrace: %+v", err)
return 1

View File

@@ -5,8 +5,10 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
@@ -26,8 +28,41 @@ func TestCmd_Run(t *testing.T) {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
RedirectURLHostname: "localhost",
},
},
},
},
"when --listen-port is set, it should convert the port to address": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
},
"when --listen-port is set, it should ignore --listen-address flags": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:30080",
"--listen-address", "127.0.0.1:40080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
@@ -38,12 +73,14 @@ func TestCmd_Run(t *testing.T) {
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--certificate-authority-data", "BASE64ENCODED",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--open-url-after-authentication", "https://example.com/success.html",
"--username", "USER",
"--password", "PASS",
},
@@ -52,11 +89,14 @@ func TestCmd_Run(t *testing.T) {
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "localhost",
},
},
},
@@ -67,21 +107,21 @@ func TestCmd_Run(t *testing.T) {
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
AuthCodeKeyboardOption: &authcode.KeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"--grant-type", "password",
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
@@ -90,14 +130,14 @@ func TestCmd_Run(t *testing.T) {
},
"GrantType=auto": {
args: []string{executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
@@ -116,9 +156,9 @@ func TestCmd_Run(t *testing.T) {
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
@@ -133,9 +173,9 @@ func TestCmd_Run(t *testing.T) {
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
@@ -160,8 +200,9 @@ func TestCmd_Run(t *testing.T) {
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
RedirectURLHostname: "localhost",
},
},
},
@@ -175,12 +216,16 @@ func TestCmd_Run(t *testing.T) {
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--certificate-authority-data", "BASE64ENCODED",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--open-url-after-authentication", "https://example.com/success.html",
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=true",
"--username", "USER",
"--password", "PASS",
},
@@ -191,11 +236,15 @@ func TestCmd_Run(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "localhost",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
},
},
},
@@ -206,13 +255,16 @@ func TestCmd_Run(t *testing.T) {
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "authcode-keyboard",
"--oidc-auth-request-extra-params", "ttl=86400",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
AuthCodeKeyboardOption: &authcode.KeyboardOption{
AuthRequestExtraParams: map[string]string{"ttl": "86400"},
},
},
},
},
@@ -222,8 +274,8 @@ func TestCmd_Run(t *testing.T) {
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "password",
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
@@ -232,7 +284,7 @@ func TestCmd_Run(t *testing.T) {
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
@@ -244,8 +296,8 @@ func TestCmd_Run(t *testing.T) {
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
@@ -254,7 +306,7 @@ func TestCmd_Run(t *testing.T) {
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
@@ -272,13 +324,13 @@ func TestCmd_Run(t *testing.T) {
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
@@ -293,13 +345,13 @@ func TestCmd_Run(t *testing.T) {
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
@@ -313,13 +365,13 @@ func TestCmd_Run(t *testing.T) {
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
Logger: logger.New(t),
},
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {

View File

@@ -1,8 +1,6 @@
package cmd
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/spf13/cobra"
@@ -16,22 +14,19 @@ type getTokenOptions struct {
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
TokenCacheDir string
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
func (o *getTokenOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
o.authenticationOptions.register(f)
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for token cache")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
}
type GetToken struct {
@@ -39,7 +34,7 @@ type GetToken struct {
Logger logger.Interface
}
func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
func (cmd *GetToken) New() *cobra.Command {
var o getTokenOptions
c := &cobra.Command{
Use: "get-token [flags]",
@@ -56,27 +51,29 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
}
return nil
},
RunE: func(*cobra.Command, []string) error {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("error: %w", err)
return xerrors.Errorf("get-token: %w", err)
}
in := credentialplugin.Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
CACertFilename: o.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
}
if err := cmd.GetToken.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
if err := cmd.GetToken.Do(c.Context(), in); err != nil {
return xerrors.Errorf("get-token: %w", err)
}
return nil
},
}
o.register(c.Flags())
c.Flags().SortFlags = false
o.addFlags(c.Flags())
return c
}

View File

@@ -1,23 +1,18 @@
package cmd
import (
"context"
"fmt"
"strings"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
const longDescription = `Login to the OpenID Connect provider.
const rootDescription = `Log in to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
To show the setup instruction:
kubectl oidc-login setup
@@ -29,62 +24,16 @@ type rootOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *rootOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
func (o *rootOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
}
type authenticationOptions struct {
GrantType string
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
var allGrantType = strings.Join([]string{
"auto",
"authcode",
"authcode-keyboard",
"password",
}, "|")
func (o *authenticationOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("The authorization grant type to use. One of (%s)", allGrantType))
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeOption = &authentication.AuthCodeOption{
BindAddress: translateListenPortToBindAddress(o.ListenPort),
SkipOpenBrowser: o.SkipOpenBrowser,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &authentication.ROPCOption{
Username: o.Username,
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
f.StringVar(&o.Context, "context", "", "Name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "Name of the kubeconfig user to use. Prior to --context")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
}
type Root struct {
@@ -92,14 +41,14 @@ type Root struct {
Logger logger.Interface
}
func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
func (cmd *Root) New() *cobra.Command {
var o rootOptions
rootCmd := &cobra.Command{
Use: executable,
Short: "Login to the OpenID Connect provider",
Long: longDescription,
c := &cobra.Command{
Use: "kubelogin",
Short: "Log in to the OpenID Connect provider",
Long: rootDescription,
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("invalid option: %w", err)
@@ -108,17 +57,19 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
CACertFilename: o.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
}
if err := cmd.Standalone.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
return xerrors.Errorf("login: %w", err)
}
return nil
},
}
o.register(rootCmd.Flags())
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
return rootCmd
c.Flags().SortFlags = false
o.addFlags(c.Flags())
cmd.Logger.AddFlags(c.PersistentFlags())
return c
}

View File

@@ -1,8 +1,6 @@
package cmd
import (
"context"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -15,27 +13,24 @@ type setupOptions struct {
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *setupOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
func (o *setupOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
}
type Setup struct {
Setup setup.Interface
}
func (cmd *Setup) New(ctx context.Context) *cobra.Command {
func (cmd *Setup) New() *cobra.Command {
var o setupOptions
c := &cobra.Command{
Use: "setup",
@@ -44,30 +39,32 @@ func (cmd *Setup) New(ctx context.Context) *cobra.Command {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("error: %w", err)
return xerrors.Errorf("setup: %w", err)
}
in := setup.Stage2Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
CACertFilename: o.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
}
if c.Flags().Lookup("listen-port").Changed {
in.ListenPortArgs = o.authenticationOptions.ListenPort
if c.Flags().Lookup("listen-address").Changed {
in.ListenAddressArgs = o.authenticationOptions.ListenAddress
}
if in.IssuerURL == "" || in.ClientID == "" {
cmd.Setup.DoStage1()
return nil
}
if err := cmd.Setup.DoStage2(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
if err := cmd.Setup.DoStage2(c.Context(), in); err != nil {
return xerrors.Errorf("setup: %w", err)
}
return nil
},
}
o.register(c.Flags())
c.Flags().SortFlags = false
o.addFlags(c.Flags())
return c
}

15
pkg/adaptors/cmd/tls.go Normal file
View File

@@ -0,0 +1,15 @@
package cmd
import "github.com/spf13/pflag"
type tlsOptions struct {
CACertFilename string
CACertData string
SkipTLSVerify bool
}
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded cert for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
}

View File

@@ -1,51 +0,0 @@
// Package credentialplugin provides interaction with kubectl for a credential plugin.
package credentialplugin
import (
"encoding/json"
"os"
"time"
"github.com/google/wire"
"golang.org/x/xerrors"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)
//go:generate mockgen -destination mock_credentialplugin/mock_credentialplugin.go github.com/int128/kubelogin/pkg/adaptors/credentialplugin Interface
var Set = wire.NewSet(
wire.Struct(new(Interaction), "*"),
wire.Bind(new(Interface), new(*Interaction)),
)
type Interface interface {
Write(out Output) error
}
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}
type Interaction struct{}
// Write writes the ExecCredential to standard output for kubectl.
func (*Interaction) Write(out Output) error {
ec := &v1beta1.ExecCredential{
TypeMeta: v1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &v1beta1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &v1.Time{Time: out.Expiry},
},
}
e := json.NewEncoder(os.Stdout)
if err := e.Encode(ec); err != nil {
return xerrors.Errorf("could not write the ExecCredential: %w", err)
}
return nil
}

View File

@@ -0,0 +1,53 @@
// Package credentialpluginwriter provides a writer for a credential plugin.
package credentialpluginwriter
import (
"encoding/json"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/xerrors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)
//go:generate mockgen -destination mock_credentialpluginwriter/mock_credentialpluginwriter.go github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter Interface
var Set = wire.NewSet(
wire.Struct(new(Writer), "*"),
wire.Bind(new(Interface), new(*Writer)),
)
type Interface interface {
Write(out Output) error
}
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}
type Writer struct {
Stdout stdio.Stdout
}
// Write writes the ExecCredential to standard output for kubectl.
func (w *Writer) Write(out Output) error {
ec := &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
},
}
e := json.NewEncoder(w.Stdout)
if err := e.Encode(ec); err != nil {
return xerrors.Errorf("could not write the ExecCredential: %w", err)
}
return nil
}

View File

@@ -1,47 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialplugin (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter (interfaces: Interface)
// Package mock_credentialplugin is a generated GoMock package.
package mock_credentialplugin
// Package mock_credentialpluginwriter is a generated GoMock package.
package mock_credentialpluginwriter
import (
gomock "github.com/golang/mock/gomock"
credentialplugin "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
credentialpluginwriter "github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Write mocks base method
func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
// Write mocks base method.
func (m *MockInterface) Write(arg0 credentialpluginwriter.Output) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write
// Write indicates an expected call of Write.
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)

View File

@@ -1,76 +0,0 @@
// Package env provides environment dependent facilities.
package env
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"github.com/google/wire"
"github.com/pkg/browser"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_env/mock_env.go github.com/int128/kubelogin/pkg/adaptors/env Interface
func init() {
// In credential plugin mode, some browser launcher writes a message to stdout
// and it may break the credential json for client-go.
// This prevents the browser launcher from breaking the credential json.
browser.Stdout = os.Stderr
}
// Set provides an implementation and interface for Env.
var Set = wire.NewSet(
wire.Struct(new(Env), "*"),
wire.Bind(new(Interface), new(*Env)),
)
type Interface interface {
ReadString(prompt string) (string, error)
ReadPassword(prompt string) (string, error)
OpenBrowser(url string) error
}
// Env provides environment specific facilities.
type Env struct{}
// ReadString reads a string from the stdin.
func (*Env) ReadString(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("could not write the prompt: %w", err)
}
r := bufio.NewReader(os.Stdin)
s, err := r.ReadString('\n')
if err != nil {
return "", xerrors.Errorf("could not read from stdin: %w", err)
}
s = strings.TrimRight(s, "\r\n")
return s, nil
}
// ReadPassword reads a password from the stdin without echo back.
func (*Env) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("could not write the prompt: %w", err)
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", xerrors.Errorf("could not read from stdin: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", xerrors.Errorf("could not write a new line: %w", err)
}
return string(b), nil
}
// OpenBrowser opens the default browser.
func (env *Env) OpenBrowser(url string) error {
if err := browser.OpenURL(url); err != nil {
return xerrors.Errorf("could not open the browser: %w", err)
}
return nil
}

View File

@@ -1,74 +0,0 @@
// Package jwtdecoder provides decoding a JWT.
package jwtdecoder
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_jwtdecoder/mock_jwtdecoder.go github.com/int128/kubelogin/pkg/adaptors/jwtdecoder Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Struct(new(Decoder), "*"),
wire.Bind(new(Interface), new(*Decoder)),
)
type Interface interface {
Decode(s string) (*Claims, error)
}
// Claims represents claims of a token.
type Claims struct {
Subject string
Expiry time.Time
Pretty map[string]string // string representation for debug and logging
}
type Decoder struct{}
// Decode returns the claims of the JWT.
// Note that this method does not verify the signature and always trust it.
func (d *Decoder) Decode(s string) (*Claims, error) {
parts := strings.Split(s, ".")
if len(parts) != 3 {
return nil, xerrors.Errorf("token contains an invalid number of segments")
}
b, err := jwt.DecodeSegment(parts[1])
if err != nil {
return nil, xerrors.Errorf("could not decode the token: %w", err)
}
var claims jwt.StandardClaims
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&claims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
var rawClaims map[string]interface{}
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&rawClaims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
return &Claims{
Subject: claims.Subject,
Expiry: time.Unix(claims.ExpiresAt, 0),
Pretty: dumpRawClaims(rawClaims),
}, nil
}
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
claims := make(map[string]string)
for k, v := range rawClaims {
switch v := v.(type) {
case float64:
claims[k] = fmt.Sprintf("%.f", v)
default:
claims[k] = fmt.Sprintf("%v", v)
}
}
return claims
}

View File

@@ -1,87 +0,0 @@
package jwtdecoder
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
)
func TestDecoder_Decode(t *testing.T) {
var decoder Decoder
t.Run("ValidToken", func(t *testing.T) {
expiry := time.Now().Round(time.Second)
idToken := newIDToken(t, "https://issuer.example.com", expiry)
decodedToken, err := decoder.Decode(idToken)
if err != nil {
t.Fatalf("Decode error: %s", err)
}
if decodedToken.Expiry != expiry {
t.Errorf("Expiry wants %s but %s", expiry, decodedToken.Expiry)
}
t.Logf("Pretty=%+v", decodedToken.Pretty)
})
t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := decoder.Decode("HEADER.INVALID_TOKEN.SIGNATURE")
if err == nil {
t.Errorf("error wants non-nil but nil")
} else {
t.Logf("expected error: %+v", err)
}
if decodedToken != nil {
t.Errorf("decodedToken wants nil but %+v", decodedToken)
}
})
}
func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
t.Helper()
claims := struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
EmailVerified bool `json:"email_verified"`
}{
StandardClaims: jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: expiry.Unix(),
},
Nonce: "NONCE",
Groups: []string{"admin", "users"},
EmailVerified: false,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(readPrivateKey(t, "testdata/jws.key"))
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func readPrivateKey(t *testing.T, name string) *rsa.PrivateKey {
t.Helper()
b, err := ioutil.ReadFile(name)
if err != nil {
t.Fatalf("could not read the file: %s", err)
}
block, rest := pem.Decode(b)
if block == nil {
t.Fatalf("could not decode PEM")
}
if len(rest) > 0 {
t.Fatalf("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("could not parse the key: %s", err)
}
return k
}

View File

@@ -1,49 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/jwtdecoder (interfaces: Interface)
// Package mock_jwtdecoder is a generated GoMock package.
package mock_jwtdecoder
import (
gomock "github.com/golang/mock/gomock"
jwtdecoder "github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Decode mocks base method
func (m *MockInterface) Decode(arg0 string) (*jwtdecoder.Claims, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Decode", arg0)
ret0, _ := ret[0].(*jwtdecoder.Claims)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Decode indicates an expected call of Decode
func (mr *MockInterfaceMockRecorder) Decode(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decode", reflect.TypeOf((*MockInterface)(nil).Decode), arg0)
}

View File

@@ -1,8 +0,0 @@
all: jws.key
jws.key:
openssl genrsa -out $@ 1024
.PHONY: clean
clean:
-rm -v jws.key

View File

@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrH34yA/f/sBOUlkYnRtd2jgDZ3WivhidqvoQaa73xqTazbkn6
GZ9r7jx0CGLRV2bmErj2WoyT54yrhezrKh0YXAHlrwLdsmV4dwiV0lOfUJd9P/vF
e2hiAWv4CcO9ZuNkTsrxM5W8Wdj2tjqOvsIn4We+HWPkpknT7VtT5RrumwIDAQAB
AoGAFqy5oA7+kZbXQV0YNqQgcMkoO7Ym5Ps1xeMwxf94z8jIQsZebxFuGnMa95UU
4wBd1ias85fUANUxwpigaBjQee5Hk+dnfUe1snUWYNm9H6tKrXEF8ajer3a2knEv
GfK0CSEumFougfW2xG88ChGTS60wc+MIRfXERCvWpGm/5EECQQDdv5IBSi89g/R1
5AGZKFCoqr6Zw5bWEKPzCCYJZzncR1ER9vP2AnMExM8Io/87WYvmpZIUrXJvQYm8
hkfVOcBZAkEAxY4VcqmRWru3zmnbj21MwcwtgESaONkWsHeYs1C/Y/3zt7TuelYz
ZJ9aUuUsaiJLEs9Y26nMt0L0snWGr2noEwJBANaDp1PWFyMUTt3pB17JcFXqb15C
pt1I1cGapWk9Uez1lMijNNhNAEWhuoKqW5Nnif5DN7EHJYfZR8x3vm/YYWkCQHAA
0iAkCwjKDLe2RIjYiwAE5ncmbdl1GuwJokVnrlrei+LHbb1mSdTuk6MT006JCs8r
R1GivzHXgCv9fdLN1IkCQHxRvv9RPND80eEkdMv4qu0s22OLRhLQ/pb+YeT5Cjjv
pJYWKrvXdRZcuNde9JiiTgK2UW1wM8KeD/EGvK2yF6M=
-----END RSA PRIVATE KEY-----

View File

@@ -11,7 +11,7 @@ import (
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, xerrors.Errorf("could not load kubeconfig: %w", err)
return nil, xerrors.Errorf("could not load the kubeconfig: %w", err)
}
auth, err := findCurrentAuthProvider(config, contextName, userName)
if err != nil {
@@ -25,7 +25,7 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, xerrors.Errorf("error while loading config: %w", err)
return nil, xerrors.Errorf("load error: %w", err)
}
return config, err
}

View File

@@ -4,7 +4,7 @@ import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
"k8s.io/client-go/tools/clientcmd/api"
)
@@ -76,7 +76,7 @@ func unsetenv(t *testing.T, key string) {
func Test_findCurrentAuthProvider(t *testing.T) {
t.Run("CurrentContext", func(t *testing.T) {
auth, err := findCurrentAuthProvider(&api.Config{
got, err := findCurrentAuthProvider(&api.Config{
CurrentContext: "theContext",
Contexts: map[string]*api.Context{
"theContext": {
@@ -118,13 +118,13 @@ func Test_findCurrentAuthProvider(t *testing.T) {
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("ByContextName", func(t *testing.T) {
auth, err := findCurrentAuthProvider(&api.Config{
got, err := findCurrentAuthProvider(&api.Config{
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
@@ -151,13 +151,13 @@ func Test_findCurrentAuthProvider(t *testing.T) {
ContextName: "theContext",
IDPIssuerURL: "https://accounts.google.com",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("ByUserName", func(t *testing.T) {
auth, err := findCurrentAuthProvider(&api.Config{
got, err := findCurrentAuthProvider(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
@@ -178,8 +178,8 @@ func Test_findCurrentAuthProvider(t *testing.T) {
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})

View File

@@ -10,30 +10,30 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// GetCurrentAuthProvider mocks base method
// GetCurrentAuthProvider mocks base method.
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
@@ -42,13 +42,13 @@ func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.Cont
return ret0, ret1
}
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider.
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuthProvider mocks base method
// UpdateAuthProvider mocks base method.
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
@@ -56,7 +56,7 @@ func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error
return ret0
}
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider.
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)

View File

@@ -4,6 +4,8 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
@@ -32,9 +34,10 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
t.Fatalf("Could not read kubeconfig: %s", err)
}
got := string(b)
want := `apiVersion: v1
clusters: []
contexts: []
clusters: null
contexts: null
current-context: ""
kind: Config
preferences: {}
@@ -50,8 +53,8 @@ users:
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
}
})
@@ -81,9 +84,10 @@ users:
t.Fatalf("Could not read kubeconfig: %s", err)
}
got := string(b)
want := `apiVersion: v1
clusters: []
contexts: []
clusters: null
contexts: null
current-context: ""
kind: Config
preferences: {}
@@ -102,8 +106,8 @@ users:
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
}
})
}

View File

@@ -4,16 +4,24 @@ package oidcclient
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
)
type FactoryInterface interface {
New(ctx context.Context, config Config) (Interface, error)
}
@@ -29,6 +37,7 @@ type Config struct {
}
type Factory struct {
Clock clock.Interface
Logger logger.Interface
}
@@ -52,7 +61,11 @@ func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.IssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDCClientFactory issuer: %w", err)
return nil, xerrors.Errorf("oidc discovery error: %w", err)
}
supportedPKCEMethods, err := extractSupportedPKCEMethods(provider)
if err != nil {
return nil, xerrors.Errorf("could not determine supported PKCE methods: %w", err)
}
return &client{
httpClient: httpClient,
@@ -63,6 +76,18 @@ func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
ClientSecret: config.ClientSecret,
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
clock: f.Clock,
logger: f.Logger,
supportedPKCEMethods: supportedPKCEMethods,
}, nil
}
func extractSupportedPKCEMethods(provider *oidc.Provider) ([]string, error) {
var d struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
if err := provider.Claims(&d); err != nil {
return nil, fmt.Errorf("invalid discovery document: %w", err)
}
return d.CodeChallengeMethodsSupported, nil
}

View File

@@ -8,7 +8,7 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/testing/logger"
)
type mockTransport struct {
@@ -37,7 +37,7 @@ dummy`)), req)
transport := &Transport{
Base: &mockTransport{resp: resp},
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
gotResp, err := transport.RoundTrip(req)
if err != nil {

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidcclient (interfaces: FactoryInterface,Interface)
// Source: github.com/int128/kubelogin/pkg/adaptors/oidcclient (interfaces: Interface)
// Package mock_oidcclient is a generated GoMock package.
package mock_oidcclient
@@ -8,86 +8,49 @@ import (
context "context"
gomock "github.com/golang/mock/gomock"
oidcclient "github.com/int128/kubelogin/pkg/adaptors/oidcclient"
oidc "github.com/int128/kubelogin/pkg/oidc"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New(arg0 context.Context, arg1 oidcclient.Config) (oidcclient.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(oidcclient.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New), arg0, arg1)
}
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// ExchangeAuthCode mocks base method
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidcclient.TokenSet, error) {
// ExchangeAuthCode mocks base method.
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExchangeAuthCode", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode.
func (mr *MockInterfaceMockRecorder) ExchangeAuthCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthCode", reflect.TypeOf((*MockInterface)(nil).ExchangeAuthCode), arg0, arg1)
}
// GetAuthCodeURL mocks base method
// GetAuthCodeURL mocks base method.
func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthCodeURL", arg0)
@@ -95,53 +58,67 @@ func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string
return ret0
}
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL.
func (mr *MockInterfaceMockRecorder) GetAuthCodeURL(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthCodeURL", reflect.TypeOf((*MockInterface)(nil).GetAuthCodeURL), arg0)
}
// GetTokenByAuthCode mocks base method
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidcclient.TokenSet, error) {
// GetTokenByAuthCode mocks base method.
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode.
func (mr *MockInterfaceMockRecorder) GetTokenByAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByAuthCode", reflect.TypeOf((*MockInterface)(nil).GetTokenByAuthCode), arg0, arg1, arg2)
}
// GetTokenByROPC mocks base method
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidcclient.TokenSet, error) {
// GetTokenByROPC mocks base method.
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByROPC indicates an expected call of GetTokenByROPC
// GetTokenByROPC indicates an expected call of GetTokenByROPC.
func (mr *MockInterfaceMockRecorder) GetTokenByROPC(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByROPC", reflect.TypeOf((*MockInterface)(nil).GetTokenByROPC), arg0, arg1, arg2)
}
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.TokenSet, error) {
// Refresh mocks base method.
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
// Refresh indicates an expected call of Refresh.
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}
// SupportedPKCEMethods mocks base method.
func (m *MockInterface) SupportedPKCEMethods() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportedPKCEMethods")
ret0, _ := ret[0].([]string)
return ret0
}
// SupportedPKCEMethods indicates an expected call of SupportedPKCEMethods.
func (mr *MockInterfaceMockRecorder) SupportedPKCEMethods() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedPKCEMethods", reflect.TypeOf((*MockInterface)(nil).SupportedPKCEMethods))
}

View File

@@ -2,72 +2,63 @@ package oidcclient
import (
"context"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
gooidc "github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/pkce"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient FactoryInterface,Interface
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
)
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient Interface
type Interface interface {
GetAuthCodeURL(in AuthCodeURLInput) string
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error)
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error)
GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error)
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error)
GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error)
SupportedPKCEMethods() []string
}
type AuthCodeURLInput struct {
State string
Nonce string
CodeChallenge string
CodeChallengeMethod string
RedirectURI string
State string
Nonce string
PKCEParams pkce.Params
RedirectURI string
AuthRequestExtraParams map[string]string
}
type ExchangeAuthCodeInput struct {
Code string
CodeVerifier string
Nonce string
RedirectURI string
Code string
PKCEParams pkce.Params
Nonce string
RedirectURI string
}
type GetTokenByAuthCodeInput struct {
BindAddress []string
Nonce string
CodeChallenge string
CodeChallengeMethod string
CodeVerifier string
}
// TokenSet represents an output DTO of
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenSubject string
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
BindAddress []string
State string
Nonce string
PKCEParams pkce.Params
RedirectURLHostname string
AuthRequestExtraParams map[string]string
LocalServerSuccessHTML string
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
httpClient *http.Client
provider *gooidc.Provider
oauth2Config oauth2.Config
clock clock.Interface
logger logger.Interface
supportedPKCEMethods []string
}
func (c *client) wrapContext(ctx context.Context) context.Context {
@@ -78,25 +69,22 @@ func (c *client) wrapContext(ctx context.Context) context.Context {
}
// GetTokenByAuthCode performs the authorization code flow.
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error) {
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error) {
ctx = c.wrapContext(ctx)
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
AuthCodeOptions: []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
oidc.Nonce(in.Nonce),
oauth2.SetAuthURLParam("code_challenge", in.CodeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", in.CodeChallengeMethod),
},
TokenRequestOptions: []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier),
},
OAuth2Config: c.oauth2Config,
State: in.State,
AuthCodeOptions: authorizationRequestOptions(in.Nonce, in.PKCEParams, in.AuthRequestExtraParams),
TokenRequestOptions: tokenRequestOptions(in.PKCEParams),
LocalServerBindAddress: in.BindAddress,
LocalServerReadyChan: localServerReadyChan,
RedirectURLHostname: in.RedirectURLHostname,
LocalServerSuccessHTML: in.LocalServerSuccessHTML,
Logf: c.logger.V(1).Infof,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
return nil, xerrors.Errorf("oauth2 error: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
@@ -105,38 +93,65 @@ func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeIn
func (c *client) GetAuthCodeURL(in AuthCodeURLInput) string {
cfg := c.oauth2Config
cfg.RedirectURL = in.RedirectURI
return cfg.AuthCodeURL(in.State,
oauth2.AccessTypeOffline,
oidc.Nonce(in.Nonce),
oauth2.SetAuthURLParam("code_challenge", in.CodeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", in.CodeChallengeMethod),
)
opts := authorizationRequestOptions(in.Nonce, in.PKCEParams, in.AuthRequestExtraParams)
return cfg.AuthCodeURL(in.State, opts...)
}
// ExchangeAuthCode exchanges the authorization code and token.
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error) {
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
ctx = c.wrapContext(ctx)
cfg := c.oauth2Config
cfg.RedirectURL = in.RedirectURI
token, err := cfg.Exchange(ctx, in.Code, oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier))
opts := tokenRequestOptions(in.PKCEParams)
token, err := cfg.Exchange(ctx, in.Code, opts...)
if err != nil {
return nil, xerrors.Errorf("could not exchange the authorization code: %w", err)
return nil, xerrors.Errorf("exchange error: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
func authorizationRequestOptions(n string, p pkce.Params, e map[string]string) []oauth2.AuthCodeOption {
o := []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
gooidc.Nonce(n),
}
if !p.IsZero() {
o = append(o,
oauth2.SetAuthURLParam("code_challenge", p.CodeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", p.CodeChallengeMethod),
)
}
for key, value := range e {
o = append(o, oauth2.SetAuthURLParam(key, value))
}
return o
}
func tokenRequestOptions(p pkce.Params) (o []oauth2.AuthCodeOption) {
if !p.IsZero() {
o = append(o, oauth2.SetAuthURLParam("code_verifier", p.CodeVerifier))
}
return
}
// SupportedPKCEMethods returns the PKCE methods supported by the provider.
// This may return nil if PKCE is not supported.
func (c *client) SupportedPKCEMethods() []string {
return c.supportedPKCEMethods
}
// GetTokenByROPC performs the resource owner password credentials flow.
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error) {
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error) {
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
return nil, xerrors.Errorf("resource owner password credentials flow error: %w", err)
}
return c.verifyToken(ctx, token, "")
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
func (c *client) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
@@ -152,46 +167,30 @@ func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, e
// verifyToken verifies the token with the certificates of the provider and the nonce.
// If the nonce is an empty string, it does not verify the nonce.
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*TokenSet, error) {
idTokenString, ok := token.Extra("id_token").(string)
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*oidc.TokenSet, error) {
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
idToken, err := verifier.Verify(ctx, idTokenString)
verifier := c.provider.Verifier(&gooidc.Config{ClientID: c.oauth2Config.ClientID, Now: c.clock.Now})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the ID token: %w", err)
}
if nonce != "" && nonce != idToken.Nonce {
return nil, xerrors.Errorf("nonce did not match (wants %s but was %s)", nonce, idToken.Nonce)
if nonce != "" && nonce != verifiedIDToken.Nonce {
return nil, xerrors.Errorf("nonce did not match (wants %s but got %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(idToken)
pretty, err := jwt.DecodePayloadAsPrettyJSON(idToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
return nil, xerrors.Errorf("could not decode the token: %w", err)
}
return &TokenSet{
IDToken: idTokenString,
RefreshToken: token.RefreshToken,
IDTokenExpiry: idToken.Expiry,
IDTokenClaims: claims,
return &oidc.TokenSet{
IDToken: idToken,
IDTokenClaims: jwt.Claims{
Subject: verifiedIDToken.Subject,
Expiry: verifiedIDToken.Expiry,
Pretty: pretty,
},
RefreshToken: token.RefreshToken,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
return dumpRawClaims(rawClaims), err
}
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
claims := make(map[string]string)
for k, v := range rawClaims {
switch v := v.(type) {
case float64:
claims[k] = fmt.Sprintf("%.f", v)
default:
claims[k] = fmt.Sprintf("%v", v)
}
}
return claims
}

View File

@@ -1,52 +1,38 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/env (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/adaptors/reader (interfaces: Interface)
// Package mock_env is a generated GoMock package.
package mock_env
// Package mock_reader is a generated GoMock package.
package mock_reader
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// OpenBrowser mocks base method
func (m *MockInterface) OpenBrowser(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// OpenBrowser indicates an expected call of OpenBrowser
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
}
// ReadPassword mocks base method
// ReadPassword mocks base method.
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadPassword", arg0)
@@ -55,13 +41,13 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword
// ReadPassword indicates an expected call of ReadPassword.
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}
// ReadString mocks base method
// ReadString mocks base method.
func (m *MockInterface) ReadString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadString", arg0)
@@ -70,7 +56,7 @@ func (m *MockInterface) ReadString(arg0 string) (string, error) {
return ret0, ret1
}
// ReadString indicates an expected call of ReadString
// ReadString indicates an expected call of ReadString.
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)

View File

@@ -0,0 +1,61 @@
// Package reader provides the reader of standard input.
package reader
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_reader/mock_reader.go github.com/int128/kubelogin/pkg/adaptors/reader Interface
// Set provides an implementation and interface for Reader.
var Set = wire.NewSet(
wire.Struct(new(Reader), "*"),
wire.Bind(new(Interface), new(*Reader)),
)
type Interface interface {
ReadString(prompt string) (string, error)
ReadPassword(prompt string) (string, error)
}
type Reader struct {
Stdin stdio.Stdin
}
// ReadString reads a string from the stdin.
func (x *Reader) ReadString(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("write error: %w", err)
}
r := bufio.NewReader(x.Stdin)
s, err := r.ReadString('\n')
if err != nil {
return "", xerrors.Errorf("read error: %w", err)
}
s = strings.TrimRight(s, "\r\n")
return s, nil
}
// ReadPassword reads a password from the stdin without echo back.
func (*Reader) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("write error: %w", err)
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", xerrors.Errorf("read error: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", xerrors.Errorf("write error: %w", err)
}
return string(b), nil
}

View File

@@ -0,0 +1,17 @@
// Package stdio wraps os.Stdin and os.Stdout for testing.
package stdio
import (
"io"
"os"
"github.com/google/wire"
)
var Set = wire.NewSet(
wire.InterfaceValue(new(Stdin), os.Stdin),
wire.InterfaceValue(new(Stdout), os.Stdout),
)
type Stdout io.Writer
type Stdin io.Reader

View File

@@ -10,53 +10,53 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// FindByKey mocks base method
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.TokenCache, error) {
// FindByKey mocks base method.
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.Value, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*tokencache.TokenCache)
ret0, _ := ret[0].(*tokencache.Value)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByKey indicates an expected call of FindByKey
// FindByKey indicates an expected call of FindByKey.
func (mr *MockInterfaceMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockInterface)(nil).FindByKey), arg0, arg1)
}
// Save mocks base method
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.TokenCache) error {
// Save mocks base method.
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.Value) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Save indicates an expected call of Save
// Save indicates an expected call of Save.
func (mr *MockInterfaceMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInterface)(nil).Save), arg0, arg1, arg2)

View File

@@ -2,6 +2,7 @@ package tokencache
import (
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"encoding/json"
"os"
@@ -20,18 +21,23 @@ var Set = wire.NewSet(
)
type Interface interface {
FindByKey(dir string, key Key) (*TokenCache, error)
Save(dir string, key Key, cache TokenCache) error
FindByKey(dir string, key Key) (*Value, error)
Save(dir string, key Key, value Value) error
}
// Key represents a key of a token cache.
type Key struct {
IssuerURL string
ClientID string
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CACertFilename string
CACertData string
SkipTLSVerify bool
}
// TokenCache represents a token cache.
type TokenCache struct {
// Value represents a value of a token cache.
type Value struct {
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
@@ -40,42 +46,52 @@ type TokenCache struct {
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
type Repository struct{}
func (r *Repository) FindByKey(dir string, key Key) (*TokenCache, error) {
filename := filepath.Join(dir, computeFilename(key))
f, err := os.Open(filename)
func (r *Repository) FindByKey(dir string, key Key) (*Value, error) {
filename, err := computeFilename(key)
if err != nil {
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
return nil, xerrors.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.Open(p)
if err != nil {
return nil, xerrors.Errorf("could not open file %s: %w", p, err)
}
defer f.Close()
d := json.NewDecoder(f)
var c TokenCache
var c Value
if err := d.Decode(&c); err != nil {
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
return nil, xerrors.Errorf("invalid json file %s: %w", p, err)
}
return &c, nil
}
func (r *Repository) Save(dir string, key Key, cache TokenCache) error {
func (r *Repository) Save(dir string, key Key, value Value) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return xerrors.Errorf("could not create directory %s: %w", dir, err)
}
filename := filepath.Join(dir, computeFilename(key))
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
filename, err := computeFilename(key)
if err != nil {
return xerrors.Errorf("could not create file %s: %w", filename, err)
return xerrors.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return xerrors.Errorf("could not create file %s: %w", p, err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(&cache); err != nil {
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
if err := e.Encode(&value); err != nil {
return xerrors.Errorf("json encode error: %w", err)
}
return nil
}
func computeFilename(key Key) string {
func computeFilename(key Key) (string, error) {
s := sha256.New()
_, _ = s.Write([]byte(key.IssuerURL))
_, _ = s.Write([]byte{0x00})
_, _ = s.Write([]byte(key.ClientID))
return hex.EncodeToString(s.Sum(nil))
e := gob.NewEncoder(s)
if err := e.Encode(&key); err != nil {
return "", xerrors.Errorf("could not encode the key: %w", err)
}
h := hex.EncodeToString(s.Sum(nil))
return h, nil
}

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"testing"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
)
func TestRepository_FindByKey(t *testing.T) {
@@ -23,22 +23,30 @@ func TestRepository_FindByKey(t *testing.T) {
}
}()
key := Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"openid", "email"},
CACertFilename: "/path/to/cert",
SkipTLSVerify: false,
}
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
filename := filepath.Join(dir, computeFilename(key))
if err := ioutil.WriteFile(filename, []byte(json), 0600); err != nil {
filename, err := computeFilename(key)
if err != nil {
t.Errorf("could not compute the key: %s", err)
}
p := filepath.Join(dir, filename)
if err := ioutil.WriteFile(p, []byte(json), 0600); err != nil {
t.Fatalf("could not write to the temp file: %s", err)
}
tokenCache, err := r.FindByKey(dir, key)
value, err := r.FindByKey(dir, key)
if err != nil {
t.Errorf("err wants nil but %+v", err)
}
want := &TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := deep.Equal(tokenCache, want); diff != nil {
t.Error(diff)
want := &Value{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := cmp.Diff(want, value); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
@@ -58,23 +66,32 @@ func TestRepository_Save(t *testing.T) {
}()
key := Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"openid", "email"},
CACertFilename: "/path/to/cert",
SkipTLSVerify: false,
}
tokenCache := TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, tokenCache); err != nil {
value := Value{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, value); err != nil {
t.Errorf("err wants nil but %+v", err)
}
filename := filepath.Join(dir, computeFilename(key))
b, err := ioutil.ReadFile(filename)
filename, err := computeFilename(key)
if err != nil {
t.Errorf("could not compute the key: %s", err)
}
p := filepath.Join(dir, filename)
b, err := ioutil.ReadFile(p)
if err != nil {
t.Fatalf("could not read the token cache file: %s", err)
}
want := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}
`
if diff := deep.Equal(string(b), want); diff != nil {
t.Error(diff)
got := string(b)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}

View File

@@ -5,17 +5,19 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/authentication"
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
@@ -23,42 +25,34 @@ import (
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() cmd.Interface {
wire.Build(
// use-cases
authentication.Set,
wire.Value(authentication.DefaultLocalServerReadyFunc),
standalone.Set,
credentialPluginUseCase.Set,
setup.Set,
NewCmdForHeadless,
// adaptors
cmd.Set,
env.Set,
kubeconfig.Set,
tokencache.Set,
credentialPluginAdaptor.Set,
oidcclient.Set,
jwtdecoder.Set,
certpool.Set,
// dependencies for production
clock.Set,
stdio.Set,
logger.Set,
browser.Set,
)
return nil
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(logger.Interface, authentication.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interface, browser.Interface) cmd.Interface {
wire.Build(
// use-cases
authentication.Set,
standalone.Set,
credentialPluginUseCase.Set,
credentialplugin.Set,
setup.Set,
// adaptors
cmd.Set,
env.Set,
reader.Set,
kubeconfig.Set,
tokencache.Set,
oidcclient.Set,
jwtdecoder.Set,
certpool.Set,
credentialpluginwriter.Set,
)
return nil
}

View File

@@ -6,73 +6,96 @@
package di
import (
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/authentication"
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"os"
)
// Injectors from di.go:
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() cmd.Interface {
clockReal := &clock.Real{}
stdin := _wireFileValue
stdout := _wireOsFileValue
loggerInterface := logger.New()
browserBrowser := &browser.Browser{}
cmdInterface := NewCmdForHeadless(clockReal, stdin, stdout, loggerInterface, browserBrowser)
return cmdInterface
}
var (
_wireFileValue = os.Stdin
_wireOsFileValue = os.Stdout
)
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout stdio.Stdout, loggerInterface logger.Interface, browserInterface browser.Interface) cmd.Interface {
factory := &oidcclient.Factory{
Clock: clockInterface,
Logger: loggerInterface,
}
decoder := &jwtdecoder.Decoder{}
envEnv := &env.Env{}
localServerReadyFunc := _wireLocalServerReadyFuncValue
authCode := &authentication.AuthCode{
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
authcodeBrowser := &authcode.Browser{
Browser: browserInterface,
Logger: loggerInterface,
}
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Env: envEnv,
readerReader := &reader.Reader{
Stdin: stdin,
}
keyboard := &authcode.Keyboard{
Reader: readerReader,
Logger: loggerInterface,
}
ropc := &authentication.ROPC{
Env: envEnv,
ropcROPC := &ropc.ROPC{
Reader: readerReader,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
OIDCClientFactory: factory,
JWTDecoder: decoder,
Logger: loggerInterface,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
OIDCClient: factory,
Logger: loggerInterface,
Clock: clockInterface,
AuthCodeBrowser: authcodeBrowser,
AuthCodeKeyboard: keyboard,
ROPC: ropcROPC,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
certpoolFactory := &certpool.Factory{}
newFunc := _wireNewFuncValue
standaloneStandalone := &standalone.Standalone{
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
NewCertPool: newFunc,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
interaction := &credentialplugin.Interaction{}
getToken := &credentialplugin2.GetToken{
writer := &credentialpluginwriter.Writer{
Stdout: stdout,
}
getToken := &credentialplugin.GetToken{
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
CertPoolFactory: certpoolFactory,
Interaction: interaction,
NewCertPool: newFunc,
Writer: writer,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
@@ -80,9 +103,9 @@ func NewCmd() cmd.Interface {
Logger: loggerInterface,
}
setupSetup := &setup.Setup{
Authentication: authenticationAuthentication,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
NewCertPool: newFunc,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
Setup: setupSetup,
@@ -97,75 +120,5 @@ func NewCmd() cmd.Interface {
}
var (
_wireLocalServerReadyFuncValue = authentication.DefaultLocalServerReadyFunc
_wireNewFuncValue = certpool.NewFunc(certpool.New)
)
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc authentication.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
factory := &oidcclient.Factory{
Logger: loggerInterface,
}
decoder := &jwtdecoder.Decoder{}
envEnv := &env.Env{}
authCode := &authentication.AuthCode{
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Env: envEnv,
Logger: loggerInterface,
}
ropc := &authentication.ROPC{
Env: envEnv,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
OIDCClientFactory: factory,
JWTDecoder: decoder,
Logger: loggerInterface,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
certpoolFactory := &certpool.Factory{}
standaloneStandalone := &standalone.Standalone{
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
getToken := &credentialplugin2.GetToken{
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
CertPoolFactory: certpoolFactory,
Interaction: credentialpluginInterface,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
GetToken: getToken,
Logger: loggerInterface,
}
setupSetup := &setup.Setup{
Authentication: authenticationAuthentication,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
Setup: setupSetup,
}
cmdCmd := &cmd.Cmd{
Root: root,
GetToken: cmdGetToken,
Setup: cmdSetup,
Logger: loggerInterface,
}
return cmdCmd
}

View File

@@ -1,65 +0,0 @@
package oidc
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"strings"
"golang.org/x/xerrors"
)
func NewState() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
func NewNonce() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
type PKCEParams struct {
CodeChallenge string
CodeChallengeMethod string
CodeVerifier string
}
func NewPKCEParams() (*PKCEParams, error) {
b, err := random32()
if err != nil {
return nil, xerrors.Errorf("could not generate a random: %w", err)
}
s := computeS256(b)
return &s, nil
}
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, xerrors.Errorf("could not read: %w", err)
}
return b, nil
}
func computeS256(b []byte) PKCEParams {
v := base64URLEncode(b)
s := sha256.New()
_, _ = s.Write([]byte(v))
return PKCEParams{
CodeChallenge: base64URLEncode(s.Sum(nil)),
CodeChallengeMethod: "S256",
CodeVerifier: v,
}
}
func base64URLEncode(b []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
}

View File

@@ -1,25 +0,0 @@
package oidc
import (
"testing"
)
func Test_computeS256(t *testing.T) {
// Testdata described at:
// https://tools.ietf.org/html/rfc7636#appendix-B
b := []byte{
116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
132, 141, 121,
}
p := computeS256(b)
if want := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; want != p.CodeVerifier {
t.Errorf("CodeVerifier wants %s but was %s", want, p.CodeVerifier)
}
if want := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; want != p.CodeChallenge {
t.Errorf("CodeChallenge wants %s but was %s", want, p.CodeChallenge)
}
if p.CodeChallengeMethod != "S256" {
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
}
}

72
pkg/jwt/decode.go Normal file
View File

@@ -0,0 +1,72 @@
// Package jwt provides JWT manipulations.
// See https://tools.ietf.org/html/rfc7519#section-4.1.3
package jwt
import (
"bytes"
"encoding/base64"
"encoding/json"
"strings"
"time"
"golang.org/x/xerrors"
)
// DecodeWithoutVerify decodes the JWT string and returns the claims.
// Note that this method does not verify the signature and always trust it.
func DecodeWithoutVerify(s string) (*Claims, error) {
payload, err := DecodePayloadAsRawJSON(s)
if err != nil {
return nil, xerrors.Errorf("could not decode the payload: %w", err)
}
var claims struct {
Subject string `json:"sub,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
}
if err := json.NewDecoder(bytes.NewReader(payload)).Decode(&claims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
var prettyJson bytes.Buffer
if err := json.Indent(&prettyJson, payload, "", " "); err != nil {
return nil, xerrors.Errorf("could not indent the json of token: %w", err)
}
return &Claims{
Subject: claims.Subject,
Expiry: time.Unix(claims.ExpiresAt, 0),
Pretty: prettyJson.String(),
}, nil
}
// DecodePayloadAsPrettyJSON decodes the JWT string and returns the pretty JSON string.
func DecodePayloadAsPrettyJSON(s string) (string, error) {
payload, err := DecodePayloadAsRawJSON(s)
if err != nil {
return "", xerrors.Errorf("could not decode the payload: %w", err)
}
var prettyJson bytes.Buffer
if err := json.Indent(&prettyJson, payload, "", " "); err != nil {
return "", xerrors.Errorf("could not indent the json of token: %w", err)
}
return prettyJson.String(), nil
}
// DecodePayloadAsRawJSON extracts the payload and returns the raw JSON.
func DecodePayloadAsRawJSON(s string) ([]byte, error) {
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, xerrors.Errorf("wants %d segments but got %d segments", 3, len(parts))
}
payloadJSON, err := decodePayload(parts[1])
if err != nil {
return nil, xerrors.Errorf("could not decode the payload: %w", err)
}
return payloadJSON, nil
}
func decodePayload(payload string) ([]byte, error) {
b, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(payload)
if err != nil {
return nil, xerrors.Errorf("invalid base64: %w", err)
}
return b, nil
}

48
pkg/jwt/decode_test.go Normal file
View File

@@ -0,0 +1,48 @@
package jwt
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestDecode(t *testing.T) {
t.Run("ValidToken", func(t *testing.T) {
const (
// https://tools.ietf.org/html/rfc7519#section-3.1
header = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9"
payload = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
signature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
token = header + "." + payload + "." + signature
)
got, err := DecodeWithoutVerify(token)
if err != nil {
t.Fatalf("Decode error: %s", err)
}
want := &Claims{
Subject: "",
Expiry: time.Unix(1300819380, 0),
Pretty: `{
"iss": "joe",
"exp": 1300819380,
"http://example.com/is_root": true
}`,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := DecodeWithoutVerify("HEADER.INVALID_TOKEN.SIGNATURE")
if err == nil {
t.Errorf("error wants non-nil but nil")
} else {
t.Logf("expected error: %+v", err)
}
if decodedToken != nil {
t.Errorf("decodedToken wants nil but %+v", decodedToken)
}
})
}

20
pkg/jwt/token.go Normal file
View File

@@ -0,0 +1,20 @@
package jwt
import "time"
// Claims represents claims of an ID token.
type Claims struct {
Subject string
Expiry time.Time
Pretty string // string representation for debug and logging
}
// Clock provides the current time.
type Clock interface {
Now() time.Time
}
// IsExpired returns true if the token is expired.
func (c *Claims) IsExpired(clock Clock) bool {
return c.Expiry.Before(clock.Now())
}

Some files were not shown because too many files have changed in this diff Show More