Compare commits

...

84 Commits

Author SHA1 Message Date
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
101 changed files with 2726 additions and 2166 deletions

View File

@@ -1,27 +1,60 @@
version: 2
version: 2.1
jobs:
build:
test:
docker:
- image: circleci/golang:1.13.4
- image: cimg/go:1.14.4
steps:
- run: mkdir -p ~/bin
- run: echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
- checkout
- run: make ci-setup-linux-amd64
- run: make VERSION=$CIRCLE_TAG ci
- run: |
if [ "$CIRCLE_TAG" ]; then
make VERSION=$CIRCLE_TAG GITHUB_USERNAME=$CIRCLE_PROJECT_USERNAME GITHUB_REPONAME=$CIRCLE_PROJECT_REPONAME release
fi
- restore_cache:
keys:
- go-sum-{{ checksum "go.sum" }}
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- save_cache:
key: go-sum-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
- store_artifacts:
path: gotest.log
crossbuild:
macos:
xcode: 11.5.0
steps:
- run: |
curl -sSfL https://dl.google.com/go/go1.14.4.darwin-amd64.tar.gz | tar -C /tmp -xz
echo 'export PATH="$PATH:/tmp/go/bin:$HOME/go/bin"' >> $BASH_ENV
- checkout
- restore_cache:
keys:
- go-macos-{{ checksum "go.sum" }}
- run:
command: go get -v github.com/int128/goxzst github.com/int128/ghcp
working_directory: .circleci
- 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: /.*/
- crossbuild:
context: open-source
requires:
- test
filters:
tags:
only: /.*/

3
.circleci/go.mod Normal file
View File

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

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.

View File

@@ -1,14 +1,12 @@
name: acceptance-test
on: [push]
jobs:
build:
name: test
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@v1
with:
go-version: 1.13
go-version: 1.14.1
id: go
- uses: actions/checkout@v1
- uses: actions/cache@v1
@@ -19,12 +17,13 @@ jobs:
go-
# https://kind.sigs.k8s.io/docs/user/quick-start/
- run: |
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-linux-amd64"
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.8.1/kind-linux-amd64"
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind version
# 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 acceptance_test -j3 setup
- run: make -C acceptance_test test
- run: make -C system_test -j3 setup
- run: make -C system_test test

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/.idea
/system_test/output/
/acceptance_test/output/
/dist/output

View File

@@ -1,10 +1,11 @@
# CI must provide the following variables (on tag push)
# VERSION
# GITHUB_USERNAME
# GITHUB_REPONAME
# CircleCI specific variables
CIRCLE_TAG ?= latest
GITHUB_USERNAME := $(CIRCLE_PROJECT_USERNAME)
GITHUB_REPONAME := $(CIRCLE_PROJECT_REPONAME)
TARGET := kubelogin
VERSION ?= latest
TARGET_OSARCH := linux_amd64 darwin_amd64 windows_amd64 linux_arm linux_arm64
VERSION ?= $(CIRCLE_TAG)
LDFLAGS := -X main.version=$(VERSION)
all: $(TARGET)
@@ -12,12 +13,6 @@ all: $(TARGET)
$(TARGET): $(wildcard **/*.go)
go build -o $@ -ldflags "$(LDFLAGS)"
.PHONY: ci
ci:
$(MAKE) check
bash -c "bash <(curl -s https://codecov.io/bash)"
$(MAKE) dist
.PHONY: check
check:
golangci-lint run
@@ -27,7 +22,7 @@ check:
dist: dist/output
dist/output:
# make the zip files for GitHub Releases
VERSION=$(VERSION) CGO_ENABLED=0 goxzst -d dist/output -i "LICENSE" -o "$(TARGET)" -t "dist/kubelogin.rb dist/oidc-login.yaml dist/Dockerfile" -- -ldflags "$(LDFLAGS)"
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
@@ -52,15 +47,3 @@ clean:
-rm $(TARGET)
-rm -r dist/output/
-rm coverage.out gotest.log
.PHONY: ci-setup-linux-amd64
ci-setup-linux-amd64:
mkdir -p ~/bin
# https://github.com/golangci/golangci-lint
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.21.0
# https://github.com/int128/goxzst
curl -sfL -o /tmp/goxzst.zip https://github.com/int128/goxzst/releases/download/v0.3.0/goxzst_linux_amd64.zip
unzip /tmp/goxzst.zip -d ~/bin
# https://github.com/int128/ghcp
curl -sfL -o /tmp/ghcp.zip https://github.com/int128/ghcp/releases/download/v1.8.0/ghcp_linux_amd64.zip
unzip /tmp/ghcp.zip -d ~/bin

165
README.md
View File

@@ -1,10 +1,10 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin) ![acceptance-test](https://github.com/int128/kubelogin/workflows/acceptance-test/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
Here is an example of Kubernetes authentication with the Google Identity Platform:
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/70971501-7bcebc80-20e4-11ea-8afc-539dcaea0aa8.gif" width="652" height="455">
<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,22 +18,14 @@ 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) 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.16.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
# Docker
docker run --rm quay.io/int128/kubelogin:v1.16.0
```
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
@@ -54,7 +46,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
@@ -91,18 +83,25 @@ If the refresh token has expired, kubelogin will perform reauthentication.
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 the claims of token by passing `-v1` option.
You can dump claims of an ID token by `setup` command.
```console
% kubectl oidc-login setup --oidc-issuer-url https://accounts.google.com --oidc-client-id REDACTED --oidc-client-secret REDACTED
authentication in progress...
## 2. Verify authentication
You got a token with the following claims:
{
"sub": "********",
"iss": "https://accounts.google.com",
"aud": "********",
...
}
```
I1212 10:14:17.754394 2517 get_token.go:91] the ID token has the claim: sub=********
I1212 10:14:17.754434 2517 get_token.go:91] the ID token has the claim: at_hash=********
I1212 10:14:17.754449 2517 get_token.go:91] the ID token has the claim: nonce=********
I1212 10:14:17.754459 2517 get_token.go:91] the ID token has the claim: iat=1576113256
I1212 10:14:17.754467 2517 get_token.go:91] the ID token has the claim: exp=1576116856
I1212 10:14:17.754484 2517 get_token.go:91] the ID token has the claim: iss=https://accounts.google.com
I1212 10:14:17.754497 2517 get_token.go:91] the ID token has the claim: azp=********.apps.googleusercontent.com
I1212 10:14:17.754506 2517 get_token.go:91] the ID token has the claim: aud=********.apps.googleusercontent.com
```
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
## Usage
@@ -117,21 +116,23 @@ 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
--certificate-authority-data string Base64 encoded data 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-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--listen-port ints (Deprecated: use --listen-address)
--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
--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
--certificate-authority-data string Base64 encoded data 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-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--listen-port ints (Deprecated: use --listen-address)
--skip-open-browser If true, it does not open the browser on authentication
--oidc-redirect-url-hostname string Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
--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
@@ -173,39 +174,6 @@ You can use your self-signed certificate for the provider.
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).
### 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:v1.16.0
- 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.
### Authentication flows
#### Authorization code flow
@@ -225,6 +193,18 @@ You can change the listening address.
- --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
```
#### Authorization code flow with keyboard interactive
If you cannot access the browser, instead use the authorization code flow with keyboard interactive.
@@ -245,6 +225,12 @@ 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
@@ -283,6 +269,39 @@ Username: foo
Password:
```
### 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.
## Related works
@@ -309,4 +328,4 @@ make
./kubelogin
```
See also [the acceptance test](acceptance_test).
See also [the system test](system_test).

View File

@@ -1,99 +1,31 @@
CLUSTER_NAME := kubelogin-acceptance-test
OUTPUT_DIR := $(CURDIR)/output
PATH := $(PATH):$(OUTPUT_DIR)/bin
export PATH
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG
# run the login script instead of opening chrome
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
export BROWSER
.PHONY: test
test: build
# see the setup instruction
kubectl oidc-login setup \
--oidc-issuer-url=https://dex-server:10443/dex \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET \
--oidc-extra-scope=email \
--certificate-authority=$(OUTPUT_DIR)/ca.crt
# set up the kubeconfig
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=https://dex-server:10443/dex \
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
--exec-arg=--oidc-extra-scope=email \
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
# make sure we can access the cluster
kubectl --user=oidc cluster-info
# switch the current context
kubectl config set-context --current --user=oidc
# make sure we can access the cluster
kubectl cluster-info
.PHONY: setup
setup: build dex cluster setup-chrome
.PHONY: setup-chrome
setup-chrome: $(OUTPUT_DIR)/ca.crt
# add the dex server certificate to the trust store
mkdir -p ~/.pki/nssdb
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
# build binaries
.PHONY: build
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
go build -o $@ ..
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
go build -o $@ ./chromelogin
# create a Dex server
.PHONY: dex
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
docker create --name dex-server -p 10443:10443 quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
docker cp $(OUTPUT_DIR)/server.key dex-server:/
docker cp dex.yaml dex-server:/
docker start dex-server
docker logs dex-server
$(OUTPUT_DIR)/ca.key:
mkdir -p $(OUTPUT_DIR)
openssl genrsa -out $@ 2048
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
$(OUTPUT_DIR)/server.key:
mkdir -p $(OUTPUT_DIR)
openssl genrsa -out $@ 2048
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
# create a Kubernetes cluster
.PHONY: cluster
cluster: dex create-cluster
# add the Dex container IP to /etc/hosts of kube-apiserver
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
# wait for kube-apiserver oidc initialization
# (oidc authenticator will retry oidc discovery every 10s)
sleep 10
.PHONY: create-cluster
create-cluster: $(OUTPUT_DIR)/ca.crt
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-acceptance-test-dex-ca.crt
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
kubectl apply -f role.yaml
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
@@ -102,7 +34,9 @@ clean:
.PHONY: delete-cluster
delete-cluster:
kind delete cluster --name $(CLUSTER_NAME)
.PHONY: delete-dex
delete-dex:
docker stop dex-server
docker rm dex-server
.PHONY: check
check:
docker version
kind version
kubectl version --client

View File

@@ -1,91 +1,75 @@
# kubelogin/acceptance_test
This is an acceptance test to verify behavior of kubelogin using a real Kubernetes cluster and OpenID Connect provider.
It runs on [GitHub Actions](https://github.com/int128/kubelogin/actions?query=workflow%3Aacceptance-test).
Let's take a look at the diagram.
![diagram](../docs/acceptance-test-diagram.svg)
It prepares the following resources:
1. Generate a pair of CA certificate and TLS server certificate for Dex.
1. Run Dex on a container.
1. Create a Kubernetes cluster using Kind.
1. Mutate `/etc/hosts` of the CI machine to access Dex.
1. Mutate `/etc/hosts` of the kube-apiserver pod to access Dex.
It performs the test by the following steps:
1. Run kubectl.
1. kubectl automatically runs kubelogin.
1. kubelogin automatically runs [chromelogin](chromelogin).
1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
1. kubelogin gets an authorization code from the browser.
1. kubelogin gets a token.
1. kubectl accesses an API with the token.
1. kube-apiserver verifies the token by Dex.
1. Check if kubectl exited with code 0.
This is a manual test for verifying Kubernetes OIDC authentication with your OIDC provider.
## Technical consideration
## Purpose
### Network and DNS
This test checks the following points:
Consider the following issues:
- kube-apiserver runs on the host network of the kind container.
- kube-apiserver cannot resolve a service name by kube-dns.
- kube-apiserver cannot access a cluster IP.
- kube-apiserver can access another container via the Docker network.
- Chrome requires exactly match of domain name between Dex URL and a server certificate.
Consequently,
- kube-apiserver accesses Dex by resolving `/etc/hosts` and via the Docker network.
- kubelogin and Chrome accesses Dex by resolving `/etc/hosts` and via the Docker network.
### TLS server certificate
Consider the following issues:
- kube-apiserver requires `--oidc-issuer` is HTTPS URL.
- kube-apiserver requires a CA certificate at startup, if `--oidc-ca-file` is given.
- kube-apiserver mounts `/usr/local/share/ca-certificates` from the kind container.
- It is possible to mount a file from the CI machine.
- It is not possible to issue a certificate using Let's Encrypt in runtime.
- Chrome requires a valid certificate in `~/.pki/nssdb`.
As a result,
- kube-apiserver uses the CA certificate of `/usr/local/share/ca-certificates/dex-ca.crt`. See the `extraMounts` section of [`cluster.yaml`](cluster.yaml).
- kubelogin uses the CA certificate in `output/ca.crt`.
- Chrome uses the CA certificate in `~/.pki/nssdb`.
### Test environment
- Set the issuer URL to kubectl. See [`kubeconfig_oidc.yaml`](kubeconfig_oidc.yaml).
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.
1. You can set up your OIDC provider using [setup guide](../docs/setup.md).
1. The plugin works with your OIDC provider.
## Run locally
## Getting Started
You need to set up Docker and Kind.
### Prerequisite
You need to add the following line to `/etc/hosts`:
You need to build the plugin into the parent directory.
```
127.0.0.1 dex-server
```sh
make -C ..
```
Run:
You need to set up your provider.
See [setup guide](../docs/setup.md) for more.
```shell script
# run the test
make
You need to install the following tools:
# clean up
- 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 delete-dex
make clean
```

View File

@@ -1,6 +1,5 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
# https://github.com/dexidp/dex/blob/master/Documentation/kubernetes.md
kubeadmConfigPatches:
- |
apiVersion: kubeadm.k8s.io/v1beta2
@@ -9,12 +8,6 @@ kubeadmConfigPatches:
name: config
apiServer:
extraArgs:
oidc-issuer-url: https://dex-server:10443/dex
oidc-client-id: YOUR_CLIENT_ID
oidc-issuer-url: OIDC_ISSUER_URL
oidc-client-id: OIDC_CLIENT_ID
oidc-username-claim: email
oidc-ca-file: /usr/local/share/ca-certificates/dex-ca.crt
nodes:
- role: control-plane
extraMounts:
- hostPath: /tmp/kubelogin-acceptance-test-dex-ca.crt
containerPath: /usr/local/share/ca-certificates/dex-ca.crt

View File

@@ -1,21 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: readonly-all-resources
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: readonly-all-resources
subjects:
- kind: User
name: admin@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: readonly-all-resources
apiGroup: rbac.authorization.k8s.io

16
dist/kubelogin.rb vendored
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

26
dist/oidc-login.yaml vendored
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

@@ -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
@@ -152,24 +155,9 @@ kubectl oidc-login setup --help
## 3. Bind a cluster role
Here bind `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
```
```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.

View File

@@ -99,31 +99,33 @@ Available Commands:
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-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--listen-port ints (Deprecated: use --listen-address)
--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 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-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--listen-port ints (Deprecated: use --listen-address)
--skip-open-browser If true, it does not open the browser on authentication
--oidc-redirect-url-hostname string Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
--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 kubelogin
--version version for kubelogin
```
### Kubeconfig

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

20
go.mod
View File

@@ -6,21 +6,21 @@ require (
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.0
github.com/google/go-cmp v0.4.0
github.com/golang/mock v1.4.3
github.com/google/go-cmp v0.5.0
github.com/google/wire v0.4.0
github.com/int128/oauth2cli v1.8.1
github.com/int128/oauth2cli v1.12.1
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-20190820162420-60c769a6c586
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.2.8
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
gopkg.in/yaml.v2 v2.3.0
k8s.io/apimachinery v0.18.5
k8s.io/client-go v0.18.5
k8s.io/klog v1.0.0
)

162
go.sum
View File

@@ -10,56 +10,71 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L
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/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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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 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/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/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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-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-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.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -67,13 +82,12 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
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 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
@@ -82,8 +96,13 @@ 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/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
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=
@@ -92,18 +111,23 @@ 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.8.1 h1:Vkmfx0w225l4qUpJ1ZWGw1elw7hnXAybSiYoYyh1iBw=
github.com/int128/oauth2cli v1.8.1/go.mod h1:MkxKWhHUaPOaq/92Z5ifdCWySAKJKo04hUXaKA7OgDE=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/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.12.1 h1:F+6sykVdM+0rede+jAJ2RICP3GAsLLGvPjSFLlI0U9Q=
github.com/int128/oauth2cli v1.12.1/go.mod h1:0Wf2wAxKJNzbkPkUIYNhTjeLn/pqIBDOBAGfwrxGYQw=
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.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -113,79 +137,107 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
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/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pkg/errors v0.8.0/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/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/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 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/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/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/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
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=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20190211182817-74369b46fc67/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-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-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/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
@@ -193,17 +245,18 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
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/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190209173611-3b5209105503/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/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/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -215,16 +268,16 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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/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-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/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -236,6 +289,8 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
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=
@@ -243,36 +298,43 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
k8s.io/api v0.18.5 h1:fKbCxr+U3fu7k6jB+QeYPD/c6xKYeSJ2KVWmyUypuWM=
k8s.io/api v0.18.5/go.mod h1:tN+e/2nbdGKOAH55NMV8oGrMG+3uRlA9GaRfvnCCSNk=
k8s.io/apimachinery v0.18.5 h1:Lh6tgsM9FMkC12K5T5QjRm7rDs6aQN5JHkA0JomULDM=
k8s.io/apimachinery v0.18.5/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/client-go v0.18.5 h1:cLhGZdOmyPhwtt20Lrb7uAqxxB1uvY+NTmNJvno1oKA=
k8s.io/client-go v0.18.5/go.mod h1:EsiD+7Fx+bRckKWZXnAXRKKetm1WuzPagH4iOSC8x58=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
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/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU=
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
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

@@ -1,25 +1,25 @@
package integration_test
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/integration_test/idp"
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
"github.com/int128/kubelogin/integration_test/keys"
"github.com/int128/kubelogin/integration_test/localserver"
"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/adaptors/browser/mock_browser"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"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.
@@ -30,6 +30,8 @@ import (
// 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)
@@ -40,279 +42,348 @@ func TestCredentialPlugin(t *testing.T) {
}
}()
t.Run("NoTLS", func(t *testing.T) {
testCredentialPlugin(t, credentialPluginTestCase{
TokenCacheDir: tokenCacheDir,
Keys: keys.None,
ExtraArgs: []string{
"--token-cache-dir", tokenCacheDir,
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
args []string
}{
"NoTLS": {},
"TLS": {
keyPair: keypair.Server,
args: []string{"--certificate-authority", keypair.Server.CACertPath},
},
} {
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, tc.keyPair.TLSConfig),
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, tc.keyPair.TLSConfig),
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, tc.keyPair.TLSConfig),
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, tc.keyPair.TLSConfig),
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"},
},
})
})
t.Run("TLS", func(t *testing.T) {
t.Run("CertFile", func(t *testing.T) {
testCredentialPlugin(t, credentialPluginTestCase{
TokenCacheDir: tokenCacheDir,
TokenCacheKey: tokencache.Key{CACertFilename: keys.Server.CACertPath},
Keys: keys.Server,
ExtraArgs: []string{
"--token-cache-dir", tokenCacheDir,
"--certificate-authority", keys.Server.CACertPath,
},
})
})
t.Run("CertData", func(t *testing.T) {
testCredentialPlugin(t, credentialPluginTestCase{
TokenCacheDir: tokenCacheDir,
TokenCacheKey: tokencache.Key{CACertData: keys.Server.CACertBase64},
Keys: keys.Server,
ExtraArgs: []string{
"--token-cache-dir", tokenCacheDir,
"--certificate-authority-data", keys.Server.CACertBase64,
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
stdout: &stdout,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
}
type credentialPluginTestCase struct {
TokenCacheDir string
TokenCacheKey tokencache.Key
Keys keys.Keys
ExtraArgs []string
}
func testCredentialPlugin(t *testing.T, tc credentialPluginTestCase) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Run("TLSData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
defer server.Shutdown(t, ctx)
var idToken string
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
args := []string{
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, tc.ExtraArgs...)
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
})
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
browserMock := mock_browser.NewMockInterface(ctrl)
args := []string{
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
"--username", "USER",
"--password", "PASS",
}
args = append(args, tc.ExtraArgs...)
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
})
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
browserMock := mock_browser.NewMockInterface(ctrl)
setupTokenCache(t, tc, serverURL, tokencache.Value{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
args := []string{
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, tc.ExtraArgs...)
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
assertTokenCache(t, tc, serverURL, tokencache.Value{
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
defer server.Shutdown(t, ctx)
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(validIDToken, "NEW_REFRESH_TOKEN"), nil)
setupTokenCache(t, tc, serverURL, tokencache.Value{
IDToken: expiredIDToken,
RefreshToken: "VALID_REFRESH_TOKEN",
})
writerMock := newCredentialPluginWriterMock(t, ctrl, &validIDToken)
browserMock := mock_browser.NewMockInterface(ctrl)
args := []string{
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, tc.ExtraArgs...)
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
assertTokenCache(t, tc, serverURL, tokencache.Value{
IDToken: validIDToken,
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
defer server.Shutdown(t, ctx)
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
setupAuthCodeFlow(t, provider, serverURL, "openid", &validIDToken)
provider.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
setupTokenCache(t, tc, serverURL, tokencache.Value{
IDToken: expiredIDToken,
RefreshToken: "EXPIRED_REFRESH_TOKEN",
})
writerMock := newCredentialPluginWriterMock(t, ctrl, &validIDToken)
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
args := []string{
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, tc.ExtraArgs...)
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
assertTokenCache(t, tc, serverURL, tokencache.Value{
IDToken: validIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, keypair.Server.TLSConfig),
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()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
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, nil),
now: now,
stdout: &stdout,
args: []string{
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
defer server.Shutdown(t, ctx)
var idToken string
setupAuthCodeFlow(t, provider, serverURL, "email profile openid", &idToken)
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
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, nil),
now: now,
stdout: &stdout,
args: []string{"--oidc-redirect-url-hostname", "127.0.0.1"},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
args := []string{
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
}
args = append(args, tc.ExtraArgs...)
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
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, nil),
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))
})
}
func newCredentialPluginWriterMock(t *testing.T, ctrl *gomock.Controller, idToken *string) *mock_credentialpluginwriter.MockInterface {
writer := mock_credentialpluginwriter.NewMockInterface(ctrl)
writer.EXPECT().
Write(gomock.Any()).
Do(func(got credentialpluginwriter.Output) {
want := credentialpluginwriter.Output{
Token: *idToken,
Expiry: tokenExpiryFuture,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
return writer
type getTokenConfig struct {
tokenCacheDir string
issuerURL string
httpDriver browser.Interface
stdout io.Writer
now time.Time
args []string
}
func runGetTokenCmd(t *testing.T, ctx context.Context, b browser.Interface, w credentialpluginwriter.Interface, args []string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), b, w)
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",
"--v=1",
"kubelogin",
"get-token",
"--token-cache-dir", cfg.tokenCacheDir,
"--oidc-issuer-url", cfg.issuerURL,
"--oidc-client-id", "kubernetes",
"--listen-address", "127.0.0.1:0",
}, args...), "HEAD")
}, cfg.args...), "latest")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func setupTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, v tokencache.Value) {
k := tc.TokenCacheKey
k.IssuerURL = serverURL
k.ClientID = "kubernetes"
var r tokencache.Repository
err := r.Save(tc.TokenCacheDir, k, v)
if err != nil {
t.Errorf("could not set up the token cache: %s", err)
}
}
func assertTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, want tokencache.Value) {
k := tc.TokenCacheKey
k.IssuerURL = serverURL
k.ClientID = "kubernetes"
var r tokencache.Repository
got, err := r.FindByKey(tc.TokenCacheDir, k)
if err != nil {
t.Errorf("could not set up the token cache: %s", err)
}
if diff := cmp.Diff(&want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
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

@@ -1,93 +0,0 @@
package integration_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/integration_test/idp"
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
"github.com/int128/kubelogin/integration_test/keys"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
)
var (
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
)
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 setupAuthCodeFlow(t *testing.T, provider *mock_idp.MockProvider, serverURL, scope string, idToken *string) {
var nonce string
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
provider.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
provider.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 setupROPCFlow(provider *mock_idp.MockProvider, serverURL, scope, username, password, idToken string) {
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
provider.EXPECT().AuthenticatePassword(username, password, scope).
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
}
func newBrowserMock(ctx context.Context, t *testing.T, ctrl *gomock.Controller, k keys.Keys) browser.Interface {
b := mock_browser.NewMockInterface(ctrl)
b.EXPECT().
Open(gomock.Any()).
Do(func(url string) {
client := http.Client{Transport: &http.Transport{TLSClientConfig: k.TLSConfig}}
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)
}
})
return b
}

View File

@@ -0,0 +1,54 @@
// Package httpdriver provides a test double of the browser.
package httpdriver
import (
"context"
"crypto/tls"
"net/http"
"testing"
)
// New returns a client to simulate browser access.
func New(ctx context.Context, t *testing.T, tlsConfig *tls.Config) *client {
return &client{ctx, t, tlsConfig}
}
// 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
tlsConfig *tls.Config
}
func (c *client) Open(url string) error {
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.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)
}
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

@@ -1,122 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/e2e_test/idp (interfaces: Provider)
// Package mock_idp is a generated GoMock package.
package mock_idp
import (
gomock "github.com/golang/mock/gomock"
idp "github.com/int128/kubelogin/integration_test/idp"
reflect "reflect"
)
// MockProvider is a mock of Provider interface
type MockProvider struct {
ctrl *gomock.Controller
recorder *MockProviderMockRecorder
}
// MockProviderMockRecorder is the mock recorder for MockProvider
type MockProviderMockRecorder struct {
mock *MockProvider
}
// NewMockProvider creates a new mock instance
func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
mock := &MockProvider{ctrl: ctrl}
mock.recorder = &MockProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockProvider) EXPECT() *MockProviderMockRecorder {
return m.recorder
}
// AuthenticateCode mocks base method
func (m *MockProvider) 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 *MockProviderMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockProvider)(nil).AuthenticateCode), arg0, arg1)
}
// AuthenticatePassword mocks base method
func (m *MockProvider) 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 *MockProviderMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockProvider)(nil).AuthenticatePassword), arg0, arg1, arg2)
}
// Discovery mocks base method
func (m *MockProvider) 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 *MockProviderMockRecorder) Discovery() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockProvider)(nil).Discovery))
}
// Exchange mocks base method
func (m *MockProvider) 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 *MockProviderMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockProvider)(nil).Exchange), arg0)
}
// GetCertificates mocks base method
func (m *MockProvider) 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 *MockProviderMockRecorder) GetCertificates() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockProvider)(nil).GetCertificates))
}
// Refresh mocks base method
func (m *MockProvider) 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 *MockProviderMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockProvider)(nil).Refresh), arg0)
}

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

@@ -4,7 +4,6 @@ TARGETS := ca.key
TARGETS += ca.crt
TARGETS += server.key
TARGETS += server.crt
TARGETS += jws.key
.PHONY: all
all: $(TARGETS)
@@ -22,9 +21,6 @@ server.csr: openssl.cnf server.key
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
jws.key:
openssl genrsa -out $@ 2048
.PHONY: clean
clean:
-rm -v $(TARGETS)

View File

@@ -1,90 +0,0 @@
package keys
import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io"
"io/ioutil"
"os"
"strings"
)
// Keys represents a pair of certificate and key.
type Keys struct {
CertPath string
KeyPath string
CACertPath string
CACertBase64 string
TLSConfig *tls.Config
}
// None represents non-TLS.
var None Keys
// Server is a Keys for TLS server.
// These files should be generated by Makefile before test.
var Server = Keys{
CertPath: "keys/testdata/server.crt",
KeyPath: "keys/testdata/server.key",
CACertPath: "keys/testdata/ca.crt",
CACertBase64: readAsBase64("keys/testdata/ca.crt"),
TLSConfig: newTLSConfig("keys/testdata/ca.crt"),
}
// JWSKey is path to the key for signing ID tokens.
// This file should be generated by Makefile before test.
const JWSKey = "keys/testdata/jws.key"
// JWSKeyPair is the key pair loaded from JWSKey.
var JWSKeyPair = readPrivateKey(JWSKey)
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}
}
func readPrivateKey(name string) *rsa.PrivateKey {
b, err := ioutil.ReadFile(name)
if err != nil {
panic(err)
}
block, rest := pem.Decode(b)
if block == nil {
panic("could not decode PEM")
}
if len(rest) > 0 {
panic("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
return k
}

View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvKJxjhaEnV/64i+GBDn4+PUno693bQBumE6a/pUf5RC8faZu
dpj7vFJsTPHqHNqR5EIKCGZMlYvnxXbOJDElqbDkRtFG4l3k/WXAea5rE4v+AKDm
/49dY6SLgH7iP2Z2gmgbqICjDFyn89Mye3H4ZEY0dHiY/7Fbhg637+aaE/CkPNue
LFn3UjqmytkxiBhLX8H6zQU6RP6IVFpSjuJ6TfRVvJ4lg1ThxX+rKqUDzIdF2FNe
1KBYfEKV1J9ZY671qrV07H8jw42Kw8/hmGECb1hr93JOu0odWM6EhXhig3oL299c
2jlQAh4y04uUybS6xYhZxrk6FcpzzaJO/eRPHQIDAQABAoIBABhtlPUIl33l2xCF
hP5xH3vmC48X/wg/oRLaQxoq56l7ZF2FOxLitt7pcZr5TQ8VgwUjRDdYQByxtH8O
5p0rPCxgev9sxJg1/pyOG8HmQ3mRjIA6Vg/MWhS4T1SBmf0J4Nj8cHB+0B6etSVP
OV9hIACkUtCueWnLZwXSTCGmJFfmfeKQSM3yyF3BxQDpyAQFdzCbIwQsTmSnZOZx
5wJ8Fv/BMrqKBje158ISePBZWz54eBnFc2VKKDRxj16e5Ni0BhCwpgDI7MRUzKVS
qKAkCwsUcmXpRZPUU5mDB6yPJU8DZwTcS5L1PWUIUnY3mlosukVUxTaoJigwulr9
RXp9tmUCgYEA6x40cnT2sibSPuytycs4Tpg5UjeCkPMNCscX+x9hdSWjJkC7eolH
qTHemjC896ExTMMFzpFKjymJbdeMn6BslOt6RvHnXugJ8IRi10JgZYHFQ/hr6A3+
SsPA7cT712Ya63i19WWCySYDccy33qGt2rtdhJlrGKEfnTiNjEMw1hMCgYEAzWNc
ae/tWbmYhjEoFt/pJp1Lb/zIDRpjV4zfeyr/wPY9Q0d/llKUISPdi1slQxpNTHqT
idiRnc8Qweisqt+84UiTU9JEG7D3T71SA+MO57NIm2wTt/7U7yb3abMZZijIPw5U
6td5jh78dGT3WRPgAsGnACXA2WJchD5m0nthrA8CgYEAiCMeJSPab/8Qf8TVP+G+
gaucjSF9JWbGJ3ZuSUa7THR1ikGzDFmOt8YbaVZNJGkePZ8yro/sBwb6/zHux8LA
/F14mLmayZY7oxtUi+VwIXZJfXjLKjtoAWxlOodzdx40+iET4rpbRxMOrYbm9C7T
lrIkjRG0NDefMY68TvncvicCgYBNtkO4PbTj1yqT07OkfBI+rxNlCxMyigJ+lOnW
M53TiBgEBeCLozEzHNvtp44AxsnqnxKF/LCUMk3X4M68VK2l3A0KkSt+AsaAoFSQ
7e+s0ZQuYoVPgBdXaboBf2ej1Nh3q1eMB/2RPb4t2CoSxUdkI5upnZ9LYUE6NFY5
W7/IFwKBgAdQxrwfQmXqtuHwiorfCm3AS/w5xmGjbBTPHnZWme2zrmFTOKSWsxAs
XAVU9B+RXJsPehC84SEreHtmZi79RRxHZOZhrl4oxrYth83QuzsR546kJwadBOj0
91yAjrekXHI3YHFQJRFUBvlswgZKDiqP74DSBbmXwfO/h2wNUYyZ
-----END RSA PRIVATE KEY-----

View File

@@ -1,4 +1,5 @@
package idp
// Package handler provides a HTTP handler for the OpenID Connect Provider.
package handler
import (
"encoding/json"
@@ -9,7 +10,7 @@ import (
"golang.org/x/xerrors"
)
func NewHandler(t *testing.T, provider Provider) *Handler {
func New(t *testing.T, provider Provider) *Handler {
return &Handler{t, provider}
}
@@ -71,11 +72,17 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
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.provider.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)
}
@@ -88,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.provider.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)
}

View File

@@ -1,13 +1,8 @@
// Package idp provides a test double of an OpenID Connect Provider.
package idp
//go:generate mockgen -destination mock_idp/mock_idp.go github.com/int128/kubelogin/e2e_test/idp Provider
package handler
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
"net/url"
)
// Provider provides discovery and authentication methods.
@@ -17,8 +12,8 @@ import (
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)
}
@@ -39,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"`
}
@@ -72,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 {
@@ -97,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,7 +1,5 @@
// 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"
@@ -9,7 +7,7 @@ import (
"net/http"
"testing"
"github.com/int128/kubelogin/integration_test/keys"
"github.com/int128/kubelogin/integration_test/keypair"
)
type Shutdowner interface {
@@ -17,28 +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.
// If k is non-nil, it starts a TLS server.
func Start(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
if k == keys.None {
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) {
func startNoTLS(t *testing.T, h http.Handler) (string, *shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
@@ -51,10 +44,10 @@ func startNoTLS(t *testing.T, h http.Handler) (string, Shutdowner) {
t.Error(err)
}
}()
return url, &shutdowner{l, s}
return url, &shutdowner{s}
}
func startTLS(t *testing.T, h http.Handler, k keys.Keys) (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
@@ -67,7 +60,7 @@ func startTLS(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
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

@@ -6,16 +6,14 @@ import (
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/integration_test/idp"
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
"github.com/int128/kubelogin/integration_test/keys"
"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/localserver"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"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.
@@ -26,207 +24,241 @@ import (
// 4. Verify the kubeconfig.
//
func TestStandalone(t *testing.T) {
t.Run("NoTLS", func(t *testing.T) {
testStandalone(t, keys.None)
})
t.Run("TLS", func(t *testing.T) {
testStandalone(t, keys.Server)
})
}
timeout := 3 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
func testStandalone(t *testing.T, idpTLS keys.Keys) {
timeout := 5 * time.Second
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
args []string
}{
"NoTLS": {},
"TLS": {
keyPair: keypair.Server,
},
} {
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, tc.keyPair.TLSConfig),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("Defaults", func(t *testing.T) {
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, tc.keyPair.TLSConfig),
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, tc.keyPair.TLSConfig),
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, tc.keyPair.TLSConfig),
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()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
var idToken string
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
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: serverURL,
IDPCertificateAuthority: idpTLS.CACertPath,
Issuer: sv.IssuerURL(),
IDPCertificateAuthorityData: keypair.Server.CACertBase64,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, browserMock, args)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, keypair.Server.TLSConfig),
now: now,
})
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
browserMock := mock_browser.NewMockInterface(ctrl)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
"--username", "USER",
"--password", "PASS",
}
runRootCmd(t, ctx, browserMock, args)
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
browserMock := mock_browser.NewMockInterface(ctrl)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, browserMock, args)
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
browserMock := mock_browser.NewMockInterface(ctrl)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, browserMock, args)
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()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
provider.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
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, browserMock, args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("env_KUBECONFIG", func(t *testing.T) {
// do not run this in parallel due to change of the env var
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
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: serverURL,
IDPCertificateAuthority: idpTLS.CACertPath,
Issuer: sv.IssuerURL(),
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, browserMock, args)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
@@ -234,42 +266,49 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
provider := mock_idp.NewMockProvider(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupAuthCodeFlow(t, provider, serverURL, "profile groups openid", &idToken)
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
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: serverURL,
ExtraScopes: "profile,groups",
IDPCertificateAuthority: idpTLS.CACertPath,
Issuer: sv.IssuerURL(),
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, browserMock, args)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
}
func runRootCmd(t *testing.T, ctx context.Context, b browser.Interface, args []string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), b, nil)
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",
"--v=1",
"--kubeconfig", cfg.kubeConfigFilename,
"--listen-address", "127.0.0.1:0",
}, args...), "HEAD")
}, cfg.args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}

View File

@@ -16,7 +16,6 @@ func init() {
browser.Stdout = os.Stderr
}
// Set provides an implementation and interface for Env.
var Set = wire.NewSet(
wire.Struct(new(Browser)),
wire.Bind(new(Interface), new(*Browser)),

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

@@ -2,7 +2,7 @@ package cmd
import (
"context"
"path/filepath"
"runtime"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
@@ -37,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{
@@ -55,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,7 +5,7 @@ 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/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
@@ -27,7 +27,8 @@ func TestCmd_Run(t *testing.T) {
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: defaultListenAddress,
BindAddress: defaultListenAddress,
RedirectURLHostname: "localhost",
},
},
},
@@ -41,7 +42,8 @@ func TestCmd_Run(t *testing.T) {
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
@@ -57,7 +59,8 @@ func TestCmd_Run(t *testing.T) {
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
@@ -85,8 +88,9 @@ func TestCmd_Run(t *testing.T) {
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
RedirectURLHostname: "localhost",
},
},
},
@@ -146,9 +150,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 {
@@ -163,9 +167,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 {
@@ -191,7 +195,8 @@ func TestCmd_Run(t *testing.T) {
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
RedirectURLHostname: "localhost",
},
},
},
@@ -212,6 +217,8 @@ func TestCmd_Run(t *testing.T) {
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=true",
"--username", "USER",
"--password", "PASS",
},
@@ -226,8 +233,10 @@ func TestCmd_Run(t *testing.T) {
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
RedirectURLHostname: "localhost",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
},
},
},
@@ -238,13 +247,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: &authentication.AuthCodeKeyboardOption{
AuthRequestExtraParams: map[string]string{"ttl": "86400"},
},
},
},
},
@@ -304,13 +316,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 {
@@ -325,13 +337,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 {
@@ -345,13 +357,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"
@@ -41,7 +39,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]",
@@ -58,10 +56,10 @@ 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,
@@ -74,8 +72,8 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
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
},

View File

@@ -1,7 +1,6 @@
package cmd
import (
"context"
"fmt"
"strings"
@@ -45,12 +44,14 @@ func (o *rootOptions) register(f *pflag.FlagSet) {
}
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
SkipOpenBrowser bool
Username string
Password string
GrantType string
ListenAddress []string
ListenPort []int // deprecated
SkipOpenBrowser bool
RedirectURLHostname string
AuthRequestExtraParams map[string]string
Username string
Password string
}
// determineListenAddress returns the addresses from the flags.
@@ -81,6 +82,8 @@ func (o *authenticationOptions) register(f *pflag.FlagSet) {
//TODO: remove the deprecated flag
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "(Deprecated: use --listen-address)")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "Hostname of the redirect URL")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "Extra query parameters to send with an authentication request")
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")
}
@@ -89,11 +92,15 @@ func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSe
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeOption = &authentication.AuthCodeOption{
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{}
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &authentication.ROPCOption{
Username: o.Username,
@@ -110,14 +117,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,
Use: "kubelogin",
Short: "Login to the OpenID Connect provider",
Long: longDescription,
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)
@@ -130,8 +137,8 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
SkipTLSVerify: o.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
},

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"
@@ -37,7 +35,7 @@ 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",
@@ -46,7 +44,7 @@ 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,
@@ -65,8 +63,8 @@ func (cmd *Setup) New(ctx context.Context) *cobra.Command {
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
},

View File

@@ -3,13 +3,13 @@ package credentialpluginwriter
import (
"encoding/json"
"os"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/xerrors"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
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
@@ -29,21 +29,23 @@ type Output struct {
Expiry time.Time
}
type Writer struct{}
type Writer struct {
Stdout stdio.Stdout
}
// Write writes the ExecCredential to standard output for kubectl.
func (*Writer) Write(out Output) error {
ec := &v1beta1.ExecCredential{
TypeMeta: v1.TypeMeta{
func (w *Writer) Write(out Output) error {
ec := &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &v1beta1.ExecCredentialStatus{
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &v1.Time{Time: out.Expiry},
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
},
}
e := json.NewEncoder(os.Stdout)
e := json.NewEncoder(w.Stdout)
if err := e.Encode(ec); err != nil {
return xerrors.Errorf("could not write the ExecCredential: %w", err)
}

View File

@@ -1,66 +0,0 @@
// Package env provides environment dependent facilities.
package env
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"time"
"github.com/google/wire"
"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
// 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)
Now() time.Time
}
// 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
}
// Now returns the current time.
func (*Env) Now() time.Time {
return time.Now()
}

View File

@@ -1,68 +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"
"github.com/int128/kubelogin/pkg/domain/oidc"
"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) (*oidc.Claims, error)
}
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) (*oidc.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 &oidc.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"
oidc "github.com/int128/kubelogin/pkg/domain/oidc"
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) (*oidc.Claims, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Decode", arg0)
ret0, _ := ret[0].(*oidc.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,17 +4,27 @@ 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"
)
type NewFunc func(ctx context.Context, config Config) (Interface, error)
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)
}
// Config represents a configuration of OpenID Connect client.
type Config struct {
@@ -24,11 +34,15 @@ type Config struct {
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
Logger logger.Interface
}
type Factory struct {
Clock clock.Interface
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func New(ctx context.Context, config Config) (Interface, error) {
func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
var tlsConfig tls.Config
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
config.CertPool.SetRootCAs(&tlsConfig)
@@ -38,7 +52,7 @@ func New(ctx context.Context, config Config) (Interface, error) {
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: config.Logger,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
@@ -47,7 +61,11 @@ func 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 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,
@@ -58,6 +76,18 @@ func New(ctx context.Context, config Config) (Interface, error) {
ClientSecret: config.ClientSecret,
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
},
logger: config.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

@@ -11,30 +11,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
}
// ExchangeAuthCode mocks base method
// ExchangeAuthCode mocks base method.
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExchangeAuthCode", arg0, arg1)
@@ -43,13 +43,13 @@ func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.E
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)
@@ -57,13 +57,13 @@ 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
// GetTokenByAuthCode mocks base method.
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
@@ -72,13 +72,13 @@ func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient
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
// GetTokenByROPC mocks base method.
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
@@ -87,13 +87,13 @@ func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string)
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
// Refresh mocks base method.
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
@@ -102,8 +102,22 @@ func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.
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,14 +2,14 @@ package oidcclient
import (
"context"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
oidcModel "github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/domain/pkce"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
@@ -17,39 +17,38 @@ import (
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient Interface
var Set = wire.NewSet(
wire.Value(NewFunc(New)),
)
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)
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
BindAddress []string
State string
Nonce string
PKCEParams pkce.Params
RedirectURLHostname string
AuthRequestExtraParams map[string]string
LocalServerSuccessHTML string
}
// TokenSet represents an output DTO of
@@ -57,14 +56,16 @@ type GetTokenByAuthCodeInput struct {
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenClaims oidcModel.Claims
IDTokenClaims jwt.Claims
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
clock clock.Interface
logger logger.Interface
supportedPKCEMethods []string
}
func (c *client) wrapContext(ctx context.Context) context.Context {
@@ -78,22 +79,19 @@ func (c *client) wrapContext(ctx context.Context) context.Context {
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*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)
}
@@ -102,12 +100,8 @@ 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.
@@ -115,19 +109,50 @@ func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput)
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,
oidc.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) {
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, "")
}
@@ -150,49 +175,29 @@ 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)
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(&oidc.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,
IDTokenClaims: claims,
RefreshToken: token.RefreshToken,
IDToken: idToken,
IDTokenClaims: jwt.Claims{
Subject: verifiedIDToken.Subject,
Expiry: verifiedIDToken.Expiry,
Pretty: pretty,
},
RefreshToken: token.RefreshToken,
}, nil
}
func dumpClaims(token *oidc.IDToken) (oidcModel.Claims, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
pretty := dumpRawClaims(rawClaims)
return oidcModel.Claims{
Subject: token.Subject,
Expiry: token.Expiry,
Pretty: pretty,
}, 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,13 +1,12 @@
// 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"
time "time"
)
// MockInterface is a mock of Interface interface
@@ -33,20 +32,6 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Now mocks base method
func (m *MockInterface) Now() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Now")
ret0, _ := ret[0].(time.Time)
return ret0
}
// Now indicates an expected call of Now
func (mr *MockInterfaceMockRecorder) Now() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockInterface)(nil).Now))
}
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()

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

@@ -60,7 +60,7 @@ func (r *Repository) FindByKey(dir string, key Key) (*Value, error) {
d := json.NewDecoder(f)
var c Value
if err := d.Decode(&c); err != nil {
return nil, xerrors.Errorf("could not decode json file %s: %w", p, err)
return nil, xerrors.Errorf("invalid json file %s: %w", p, err)
}
return &c, nil
}
@@ -81,7 +81,7 @@ func (r *Repository) Save(dir string, key Key, value Value) error {
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(&value); err != nil {
return xerrors.Errorf("could not encode json to file %s: %w", p, err)
return xerrors.Errorf("json encode error: %w", err)
}
return nil
}

View File

@@ -7,13 +7,14 @@ 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"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"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"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
@@ -27,15 +28,16 @@ func NewCmd() cmd.Interface {
NewCmdForHeadless,
// dependencies for production
clock.Set,
stdio.Set,
logger.Set,
browser.Set,
credentialpluginwriter.Set,
)
return nil
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(logger.Interface, browser.Interface, credentialpluginwriter.Interface) cmd.Interface {
func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interface, browser.Interface) cmd.Interface {
wire.Build(
// use-cases
authentication.Set,
@@ -45,12 +47,12 @@ func NewCmdForHeadless(logger.Interface, browser.Interface, credentialpluginwrit
// adaptors
cmd.Set,
env.Set,
reader.Set,
kubeconfig.Set,
tokencache.Set,
oidcclient.Set,
jwtdecoder.Set,
certpool.Set,
credentialpluginwriter.Set,
)
return nil
}

View File

@@ -8,52 +8,63 @@ 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/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"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"
"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:
func NewCmd() cmd.Interface {
clockReal := &clock.Real{}
stdin := _wireFileValue
stdout := _wireOsFileValue
loggerInterface := logger.New()
browserBrowser := &browser.Browser{}
writer := &credentialpluginwriter.Writer{}
cmdInterface := NewCmdForHeadless(loggerInterface, browserBrowser, writer)
cmdInterface := NewCmdForHeadless(clockReal, stdin, stdout, loggerInterface, browserBrowser)
return cmdInterface
}
func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browser.Interface, credentialpluginwriterInterface credentialpluginwriter.Interface) cmd.Interface {
newFunc := _wireNewFuncValue
decoder := &jwtdecoder.Decoder{}
envEnv := &env.Env{}
var (
_wireFileValue = os.Stdin
_wireOsFileValue = os.Stdout
)
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,
}
authCode := &authentication.AuthCode{
Env: envEnv,
Browser: browserInterface,
Logger: loggerInterface,
}
readerReader := &reader.Reader{
Stdin: stdin,
}
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Env: envEnv,
Reader: readerReader,
Logger: loggerInterface,
}
ropc := &authentication.ROPC{
Env: envEnv,
Reader: readerReader,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
NewOIDCClient: newFunc,
JWTDecoder: decoder,
OIDCClient: factory,
Logger: loggerInterface,
Env: envEnv,
Clock: clockInterface,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
@@ -61,11 +72,11 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
certpoolNewFunc := _wireCertpoolNewFuncValue
newFunc := _wireNewFuncValue
standaloneStandalone := &standalone.Standalone{
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
NewCertPool: certpoolNewFunc,
NewCertPool: newFunc,
Logger: loggerInterface,
}
root := &cmd.Root{
@@ -73,11 +84,14 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
writer := &credentialpluginwriter.Writer{
Stdout: stdout,
}
getToken := &credentialplugin.GetToken{
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
NewCertPool: certpoolNewFunc,
Writer: credentialpluginwriterInterface,
NewCertPool: newFunc,
Writer: writer,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
@@ -86,7 +100,7 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
}
setupSetup := &setup.Setup{
Authentication: authenticationAuthentication,
NewCertPool: certpoolNewFunc,
NewCertPool: newFunc,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
@@ -102,6 +116,5 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
}
var (
_wireNewFuncValue = oidcclient.NewFunc(oidcclient.New)
_wireCertpoolNewFuncValue = certpool.NewFunc(certpool.New)
_wireNewFuncValue = certpool.NewFunc(certpool.New)
)

72
pkg/domain/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
}

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/domain/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())
}

View File

@@ -1,10 +1,10 @@
package oidc_test
package jwt_test
import (
"testing"
"time"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
)
type timeProvider time.Time
@@ -14,7 +14,7 @@ func (tp timeProvider) Now() time.Time {
}
func TestClaims_IsExpired(t *testing.T) {
claims := oidc.Claims{
claims := jwt.Claims{
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
}

View File

@@ -2,10 +2,8 @@ package oidc
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"strings"
"golang.org/x/xerrors"
)
@@ -26,40 +24,14 @@ func NewNonce() (string, error) {
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 nil, xerrors.Errorf("read error: %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), "=")
return base64.URLEncoding.WithPadding(base64.NoPadding).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)
}
}

View File

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

74
pkg/domain/pkce/pkce.go Normal file
View File

@@ -0,0 +1,74 @@
// Package pkce provides generation of the PKCE parameters.
// See also https://tools.ietf.org/html/rfc7636.
package pkce
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"golang.org/x/xerrors"
)
var Plain Params
const (
// code challenge methods defined as https://tools.ietf.org/html/rfc7636#section-4.3
methodS256 = "S256"
)
// Params represents a set of the PKCE parameters.
type Params struct {
CodeChallenge string
CodeChallengeMethod string
CodeVerifier string
}
func (p Params) IsZero() bool {
return p == Params{}
}
// New returns a parameters supported by the provider.
// You need to pass the code challenge methods defined in RFC7636.
// It returns Plain if no method is available.
func New(methods []string) (Params, error) {
for _, method := range methods {
if method == methodS256 {
return NewS256()
}
}
return Plain, nil
}
// NewS256 generates a parameters for S256.
func NewS256() (Params, error) {
b, err := random32()
if err != nil {
return Plain, xerrors.Errorf("could not generate a random: %w", err)
}
return computeS256(b), nil
}
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, xerrors.Errorf("read error: %w", err)
}
return b, nil
}
func computeS256(b []byte) Params {
v := base64URLEncode(b)
s := sha256.New()
_, _ = s.Write([]byte(v))
return Params{
CodeChallenge: base64URLEncode(s.Sum(nil)),
CodeChallengeMethod: methodS256,
CodeVerifier: v,
}
}
func base64URLEncode(b []byte) string {
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
}

View File

@@ -0,0 +1,61 @@
package pkce
import (
"testing"
)
func TestNew(t *testing.T) {
t.Run("S256", func(t *testing.T) {
p, err := New([]string{"plain", "S256"})
if err != nil {
t.Fatalf("New error: %s", err)
}
if p.CodeChallengeMethod != "S256" {
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
}
if p.CodeChallenge == "" {
t.Errorf("CodeChallenge wants non-empty but was empty")
}
if p.CodeVerifier == "" {
t.Errorf("CodeVerifier wants non-empty but was empty")
}
})
t.Run("plain", func(t *testing.T) {
p, err := New([]string{"plain"})
if err != nil {
t.Fatalf("New error: %s", err)
}
if !p.IsZero() {
t.Errorf("IsZero wants true but was false")
}
})
t.Run("nil", func(t *testing.T) {
p, err := New(nil)
if err != nil {
t.Fatalf("New error: %s", err)
}
if !p.IsZero() {
t.Errorf("IsZero wants true but was false")
}
})
}
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)
}
}

View File

@@ -0,0 +1,35 @@
package templates
// AuthCodeBrowserSuccessHTML is the success page on browser based authentication.
const AuthCodeBrowserSuccessHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Authenticated</title>
<script>
window.close()
</script>
<style>
body {
background-color: #eee;
margin: 0;
padding: 0;
font-family: sans-serif;
}
.placeholder {
margin: 2em;
padding: 2em;
background-color: #fff;
border-radius: 1em;
}
</style>
</head>
<body>
<div class="placeholder">
<h1>Authenticated</h1>
<p>You have logged in to the cluster. You can close this window.</p>
</div>
</body>
</html>
`

View File

@@ -0,0 +1,29 @@
package main
import (
"log"
"net/http"
"github.com/int128/kubelogin/pkg/templates"
)
func main() {
http.HandleFunc("/AuthCodeBrowserSuccessHTML", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "text/html")
_, _ = w.Write([]byte(templates.AuthCodeBrowserSuccessHTML))
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "text/html")
_, _ = w.Write([]byte(`
<html>
<body>
<ul>
<li><a href="AuthCodeBrowserSuccessHTML">AuthCodeBrowserSuccessHTML</a></li>
</ul>
</body>
</html>
`))
})
log.Printf("http://localhost:8000")
log.Fatal(http.ListenAndServe("127.0.0.1:8000", nil))
}

6
pkg/templates/package.go Normal file
View File

@@ -0,0 +1,6 @@
// Package templates provides templates such as HTML and messages.
//
// You can preview HTML pages by running httpserver package.
// go run ./httpserver
//
package templates

View File

@@ -0,0 +1,9 @@
package clock
import "time"
type Fake time.Time
func (f Fake) Now() time.Time {
return time.Time(f)
}

43
pkg/testing/jwt/encode.go Normal file
View File

@@ -0,0 +1,43 @@
package jwt
import (
"crypto/rand"
"crypto/rsa"
"testing"
"github.com/dgrijalva/jwt-go"
)
var PrivateKey = generateKey(1024)
func generateKey(b int) *rsa.PrivateKey {
k, err := rsa.GenerateKey(rand.Reader, b)
if err != nil {
panic(err)
}
return k
}
type Claims struct {
jwt.StandardClaims
// aud claim is either a string or an array of strings.
// https://tools.ietf.org/html/rfc7519#section-4.1.3
Audience []string `json:"aud,omitempty"`
Nonce string `json:"nonce,omitempty"`
Groups []string `json:"groups,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
}
func Encode(t *testing.T, claims Claims) string {
s, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(PrivateKey)
if err != nil {
t.Fatalf("could not encode JWT: %s", err)
}
return s
}
func EncodeF(t *testing.T, mutation func(*Claims)) string {
var claims Claims
mutation(&claims)
return Encode(t, claims)
}

View File

@@ -1,4 +1,4 @@
package mock_logger
package logger
import (
"fmt"
@@ -8,7 +8,7 @@ import (
)
func New(t testingLogger) *Logger {
return &Logger{t}
return &Logger{t: t}
}
type testingLogger interface {
@@ -17,30 +17,38 @@ type testingLogger interface {
// Logger provides logging facility using testing.T.
type Logger struct {
t testingLogger
t testingLogger
maxLevel int
}
func (*Logger) AddFlags(f *pflag.FlagSet) {
f.IntP("v", "v", 0, "dummy flag used in the tests")
func (l *Logger) AddFlags(f *pflag.FlagSet) {
f.IntVarP(&l.maxLevel, "v", "v", 0, "dummy flag used in the tests")
}
func (l *Logger) Printf(format string, args ...interface{}) {
l.t.Logf(format, args...)
}
type Verbose struct {
func (l *Logger) V(level int) logger.Verbose {
if l.IsEnabled(level) {
return &verbose{l.t, level}
}
return &noopVerbose{}
}
func (l *Logger) IsEnabled(level int) bool {
return level <= l.maxLevel
}
type verbose struct {
t testingLogger
level int
}
func (v *Verbose) Infof(format string, args ...interface{}) {
func (v *verbose) Infof(format string, args ...interface{}) {
v.t.Logf(fmt.Sprintf("I%d] ", v.level)+format, args...)
}
func (l *Logger) V(level int) logger.Verbose {
return &Verbose{l.t, level}
}
type noopVerbose struct{}
func (*Logger) IsEnabled(level int) bool {
return true
}
func (*noopVerbose) Infof(string, ...interface{}) {}

View File

@@ -4,37 +4,43 @@ import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/pkce"
"github.com/int128/kubelogin/pkg/templates"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
// AuthCode provides the authentication code flow.
type AuthCode struct {
Env env.Interface
Browser browser.Interface
Logger logger.Interface
}
func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authentication code flow")
u.Logger.V(1).Infof("starting the authentication code flow via the browser")
state, err := oidc.NewState()
if err != nil {
return nil, xerrors.Errorf("could not generate a state: %w", err)
}
nonce, err := oidc.NewNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
}
p, err := oidc.NewPKCEParams()
p, err := pkce.New(client.SupportedPKCEMethods())
if err != nil {
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
}
in := oidcclient.GetTokenByAuthCodeInput{
BindAddress: o.BindAddress,
Nonce: nonce,
CodeChallenge: p.CodeChallenge,
CodeChallengeMethod: p.CodeChallengeMethod,
CodeVerifier: p.CodeVerifier,
BindAddress: o.BindAddress,
State: state,
Nonce: nonce,
PKCEParams: p,
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
LocalServerSuccessHTML: templates.AuthCodeBrowserSuccessHTML,
}
readyChan := make(chan string, 1)
defer close(readyChan)
@@ -50,6 +56,7 @@ func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.
u.Logger.Printf("Please visit the following URL in your browser: %s", url)
return nil
}
u.Logger.V(1).Infof("opening %s in the browser", url)
if err := u.Browser.Open(url); err != nil {
u.Logger.Printf(`error: could not open the browser: %s
@@ -64,17 +71,19 @@ Please visit the following URL in your browser manually: %s`, err, url)
eg.Go(func() error {
tokenSet, err := client.GetTokenByAuthCode(ctx, in, readyChan)
if err != nil {
return xerrors.Errorf("error while the authorization code flow: %w", err)
return xerrors.Errorf("authorization code flow error: %w", err)
}
out = Output{
IDToken: tokenSet.IDToken,
IDTokenClaims: tokenSet.IDTokenClaims,
RefreshToken: tokenSet.RefreshToken,
}
u.Logger.V(1).Infof("got a token set by the authorization code flow")
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
return nil, xerrors.Errorf("authentication error: %w", err)
}
u.Logger.V(1).Infof("finished the authorization code flow via the browser")
return &out, nil
}

View File

@@ -8,17 +8,17 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
)
func TestAuthCode_Do(t *testing.T) {
dummyTokenClaims := oidc.Claims{
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
Pretty: "PRETTY_JSON",
}
timeout := 5 * time.Second
@@ -28,13 +28,25 @@ func TestAuthCode_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
RedirectURLHostname: "localhost",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
Do(func(_ context.Context, in oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
if diff := cmp.Diff(o.BindAddress, in.BindAddress); diff != "" {
t.Errorf("BindAddress mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(o.RedirectURLHostname, in.RedirectURLHostname); diff != "" {
t.Errorf("RedirectURLHostname mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(o.AuthRequestExtraParams, in.AuthRequestExtraParams); diff != "" {
t.Errorf("AuthRequestExtraParams mismatch (-want +got):\n%s", diff)
}
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
@@ -43,7 +55,7 @@ func TestAuthCode_Do(t *testing.T) {
IDTokenClaims: dummyTokenClaims,
}, nil)
u := AuthCode{
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
@@ -68,6 +80,7 @@ func TestAuthCode_Do(t *testing.T) {
BindAddress: []string{"127.0.0.1:8000"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
@@ -82,7 +95,7 @@ func TestAuthCode_Do(t *testing.T) {
mockBrowser.EXPECT().
Open("LOCAL_SERVER_URL")
u := AuthCode{
Logger: mock_logger.New(t),
Logger: logger.New(t),
Browser: mockBrowser,
}
got, err := u.Do(ctx, o, mockOIDCClient)

View File

@@ -3,10 +3,11 @@ package authentication
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/env"
"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/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/pkce"
"golang.org/x/xerrors"
)
@@ -15,12 +16,12 @@ const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
// AuthCodeKeyboard provides the authorization code flow with keyboard interactive.
type AuthCodeKeyboard struct {
Env env.Interface
Reader reader.Interface
Logger logger.Interface
}
func (u *AuthCodeKeyboard) Do(ctx context.Context, o *AuthCodeKeyboardOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authorization code flow with keyboard interactive")
u.Logger.V(1).Infof("starting the authorization code flow with keyboard interactive")
state, err := oidc.NewState()
if err != nil {
return nil, xerrors.Errorf("could not generate a state: %w", err)
@@ -29,32 +30,34 @@ func (u *AuthCodeKeyboard) Do(ctx context.Context, o *AuthCodeKeyboardOption, cl
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
}
p, err := oidc.NewPKCEParams()
p, err := pkce.New(client.SupportedPKCEMethods())
if err != nil {
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
}
authCodeURL := client.GetAuthCodeURL(oidcclient.AuthCodeURLInput{
State: state,
Nonce: nonce,
CodeChallenge: p.CodeChallenge,
CodeChallengeMethod: p.CodeChallengeMethod,
RedirectURI: oobRedirectURI,
State: state,
Nonce: nonce,
PKCEParams: p,
RedirectURI: oobRedirectURI,
AuthRequestExtraParams: o.AuthRequestExtraParams,
})
u.Logger.Printf("Open %s", authCodeURL)
code, err := u.Env.ReadString(authCodeKeyboardPrompt)
u.Logger.Printf("Please visit the following URL in your browser: %s", authCodeURL)
code, err := u.Reader.ReadString(authCodeKeyboardPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read the authorization code: %w", err)
return nil, xerrors.Errorf("could not read an authorization code: %w", err)
}
u.Logger.V(1).Infof("exchanging the code and token")
tokenSet, err := client.ExchangeAuthCode(ctx, oidcclient.ExchangeAuthCodeInput{
Code: code,
CodeVerifier: p.CodeVerifier,
Nonce: nonce,
RedirectURI: oobRedirectURI,
Code: code,
PKCEParams: p,
Nonce: nonce,
RedirectURI: oobRedirectURI,
})
if err != nil {
return nil, xerrors.Errorf("could not get the token: %w", err)
return nil, xerrors.Errorf("could not exchange the authorization code: %w", err)
}
u.Logger.V(1).Infof("finished the authorization code flow with keyboard interactive")
return &Output{
IDToken: tokenSet.IDToken,
IDTokenClaims: tokenSet.IDTokenClaims,

View File

@@ -7,20 +7,20 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/adaptors/reader/mock_reader"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
)
var nonNil = gomock.Not(gomock.Nil())
func TestAuthCodeKeyboard_Do(t *testing.T) {
dummyTokenClaims := oidc.Claims{
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
Pretty: "PRETTY_JSON",
}
timeout := 5 * time.Second
@@ -29,9 +29,18 @@ func TestAuthCodeKeyboard_Do(t *testing.T) {
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &AuthCodeKeyboardOption{
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
GetAuthCodeURL(nonNil).
Do(func(in oidcclient.AuthCodeURLInput) {
if diff := cmp.Diff(o.AuthRequestExtraParams, in.AuthRequestExtraParams); diff != "" {
t.Errorf("AuthRequestExtraParams mismatch (-want +got):\n%s", diff)
}
}).
Return("https://issuer.example.com/auth")
mockOIDCClient.EXPECT().
ExchangeAuthCode(nonNil, nonNil).
@@ -45,15 +54,15 @@ func TestAuthCodeKeyboard_Do(t *testing.T) {
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
mockReader := mock_reader.NewMockInterface(ctrl)
mockReader.EXPECT().
ReadString(authCodeKeyboardPrompt).
Return("YOUR_AUTH_CODE", nil)
u := AuthCodeKeyboard{
Env: mockEnv,
Logger: mock_logger.New(t),
Reader: mockReader,
Logger: logger.New(t),
}
got, err := u.Do(ctx, nil, mockOIDCClient)
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}

View File

@@ -5,11 +5,10 @@ import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"golang.org/x/xerrors"
)
@@ -36,8 +35,8 @@ type Input struct {
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
IDToken string // optional
RefreshToken string // optional
IDToken string // optional, from the token cache
RefreshToken string // optional, from the token cache
GrantOptionSet GrantOptionSet
}
@@ -48,22 +47,26 @@ type GrantOptionSet struct {
}
type AuthCodeOption struct {
SkipOpenBrowser bool
BindAddress []string
SkipOpenBrowser bool
BindAddress []string
RedirectURLHostname string
AuthRequestExtraParams map[string]string
}
type AuthCodeKeyboardOption struct{}
type AuthCodeKeyboardOption struct {
AuthRequestExtraParams map[string]string
}
type ROPCOption struct {
Username string
Password string // If empty, read a password using Env.ReadPassword()
Password string // If empty, read a password using Reader.ReadPassword()
}
// Output represents an output DTO of the Authentication use-case.
type Output struct {
AlreadyHasValidIDToken bool
IDToken string
IDTokenClaims oidc.Claims
IDTokenClaims jwt.Claims
RefreshToken string
}
@@ -84,10 +87,9 @@ const passwordPrompt = "Password: "
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
NewOIDCClient oidcclient.NewFunc
JWTDecoder jwtdecoder.Interface
OIDCClient oidcclient.FactoryInterface
Logger logger.Interface
Env env.Interface
Clock clock.Interface
AuthCode *AuthCode
AuthCodeKeyboard *AuthCodeKeyboard
ROPC *ROPC
@@ -99,11 +101,11 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
// Skip verification of the token to reduce time of a discovery request.
// Here it trusts the signature and claims and checks only expiration,
// because the token has been verified before caching.
claims, err := u.JWTDecoder.Decode(in.IDToken)
claims, err := jwt.DecodeWithoutVerify(in.IDToken)
if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
return nil, xerrors.Errorf("invalid token cache (you may need to remove): %w", err)
}
if !claims.IsExpired(u.Env) {
if !claims.IsExpired(u.Clock) {
u.Logger.V(1).Infof("you already have a valid token until %s", claims.Expiry)
return &Output{
AlreadyHasValidIDToken: true,
@@ -116,17 +118,16 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
}
u.Logger.V(1).Infof("initializing an OpenID Connect client")
client, err := u.NewOIDCClient(ctx, oidcclient.Config{
client, err := u.OIDCClient.New(ctx, oidcclient.Config{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: in.CertPool,
SkipTLSVerify: in.SkipTLSVerify,
Logger: u.Logger,
})
if err != nil {
return nil, xerrors.Errorf("could not initialize the OpenID Connect client: %w", err)
return nil, xerrors.Errorf("oidc error: %w", err)
}
if in.RefreshToken != "" {

View File

@@ -7,29 +7,28 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder/mock_jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/clock"
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
testingLogger "github.com/int128/kubelogin/pkg/testing/logger"
"golang.org/x/xerrors"
)
var cmpIgnoreLogger = cmpopts.IgnoreInterfaces(struct{ logger.Interface }{})
func TestAuthentication_Do(t *testing.T) {
dummyTokenClaims := oidc.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
}
timeBeforeExpiry := time.Date(2019, 1, 2, 1, 0, 0, 0, time.UTC)
timeAfterExpiry := time.Date(2019, 1, 2, 4, 0, 0, 0, time.UTC)
timeout := 5 * time.Second
testingLogger := mock_logger.New(t)
expiryTime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
cachedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://issuer.example.com"
claims.Subject = "SUBJECT"
claims.ExpiresAt = expiryTime.Unix()
})
dummyClaims := jwt.Claims{
Subject: "SUBJECT",
Expiry: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
Pretty: "PRETTY_JSON",
}
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
@@ -40,20 +39,11 @@ func TestAuthentication_Do(t *testing.T) {
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
IDToken: cachedIDToken,
}
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
Now().
Return(timeBeforeExpiry)
mockDecoder := mock_jwtdecoder.NewMockInterface(ctrl)
mockDecoder.EXPECT().
Decode("VALID_ID_TOKEN").
Return(&dummyTokenClaims, nil)
u := Authentication{
JWTDecoder: mockDecoder,
Logger: testingLogger,
Env: mockEnv,
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(-time.Hour)),
}
got, err := u.Do(ctx, in)
if err != nil {
@@ -61,8 +51,16 @@ func TestAuthentication_Do(t *testing.T) {
}
want := &Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: cachedIDToken,
IDTokenClaims: jwt.Claims{
Subject: "SUBJECT",
Expiry: expiryTime,
Pretty: `{
"exp": 1577934245,
"iss": "https://issuer.example.com",
"sub": "SUBJECT"
}`,
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -78,40 +76,29 @@ func TestAuthentication_Do(t *testing.T) {
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
IDToken: cachedIDToken,
RefreshToken: "VALID_REFRESH_TOKEN",
}
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
Now().
Return(timeAfterExpiry)
mockDecoder := mock_jwtdecoder.NewMockInterface(ctrl)
mockDecoder.EXPECT().
Decode("EXPIRED_ID_TOKEN").
Return(&dummyTokenClaims, nil)
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "VALID_REFRESH_TOKEN").
Return(&oidcclient.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDTokenClaims: dummyClaims,
}, nil)
u := Authentication{
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
want := oidcclient.Config{
OIDCClient: &oidcclientFactory{
t: t,
client: mockOIDCClient,
want: oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
if diff := cmp.Diff(want, got, cmpIgnoreLogger); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
return mockOIDCClient, nil
},
},
JWTDecoder: mockDecoder,
Logger: testingLogger,
Env: mockEnv,
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
}
got, err := u.Do(ctx, in)
if err != nil {
@@ -120,7 +107,7 @@ func TestAuthentication_Do(t *testing.T) {
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDTokenClaims: dummyClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -142,18 +129,11 @@ func TestAuthentication_Do(t *testing.T) {
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
IDToken: cachedIDToken,
RefreshToken: "EXPIRED_REFRESH_TOKEN",
}
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
Now().
Return(timeAfterExpiry)
mockDecoder := mock_jwtdecoder.NewMockInterface(ctrl)
mockDecoder.EXPECT().
Decode("EXPIRED_ID_TOKEN").
Return(&dummyTokenClaims, nil)
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
@@ -165,25 +145,22 @@ func TestAuthentication_Do(t *testing.T) {
Return(&oidcclient.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDTokenClaims: dummyClaims,
}, nil)
u := Authentication{
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
want := oidcclient.Config{
OIDCClient: &oidcclientFactory{
t: t,
client: mockOIDCClient,
want: oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
if diff := cmp.Diff(want, got, cmpIgnoreLogger); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
return mockOIDCClient, nil
},
},
JWTDecoder: mockDecoder,
Logger: testingLogger,
Env: mockEnv,
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
AuthCode: &AuthCode{
Logger: testingLogger,
Logger: testingLogger.New(t),
},
}
got, err := u.Do(ctx, in)
@@ -193,7 +170,7 @@ func TestAuthentication_Do(t *testing.T) {
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDTokenClaims: dummyClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -222,23 +199,21 @@ func TestAuthentication_Do(t *testing.T) {
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDTokenClaims: dummyClaims,
}, nil)
u := Authentication{
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
want := oidcclient.Config{
OIDCClient: &oidcclientFactory{
t: t,
client: mockOIDCClient,
want: oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
if diff := cmp.Diff(want, got, cmpIgnoreLogger); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
return mockOIDCClient, nil
},
},
Logger: testingLogger,
Logger: testingLogger.New(t),
ROPC: &ROPC{
Logger: testingLogger,
Logger: testingLogger.New(t),
},
}
got, err := u.Do(ctx, in)
@@ -248,10 +223,23 @@ func TestAuthentication_Do(t *testing.T) {
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDTokenClaims: dummyClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
type oidcclientFactory struct {
t *testing.T
client oidcclient.Interface
want oidcclient.Config
}
func (f *oidcclientFactory) New(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
if diff := cmp.Diff(f.want, got); diff != "" {
f.t.Errorf("mismatch (-want +got):\n%s", diff)
}
return f.client, nil
}

View File

@@ -3,38 +3,39 @@ package authentication
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"golang.org/x/xerrors"
)
// ROPC provides the resource owner password credentials flow.
type ROPC struct {
Env env.Interface
Reader reader.Interface
Logger logger.Interface
}
func (u *ROPC) Do(ctx context.Context, in *ROPCOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
u.Logger.V(1).Infof("starting the resource owner password credentials flow")
if in.Username == "" {
var err error
in.Username, err = u.Env.ReadString(usernamePrompt)
in.Username, err = u.Reader.ReadString(usernamePrompt)
if err != nil {
return nil, xerrors.Errorf("could not get the username: %w", err)
return nil, xerrors.Errorf("could not read a username: %w", err)
}
}
if in.Password == "" {
var err error
in.Password, err = u.Env.ReadPassword(passwordPrompt)
in.Password, err = u.Reader.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
}
}
tokenSet, err := client.GetTokenByROPC(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
return nil, xerrors.Errorf("resource owner password credentials flow error: %w", err)
}
u.Logger.V(1).Infof("finished the resource owner password credentials flow")
return &Output{
IDToken: tokenSet.IDToken,
IDTokenClaims: tokenSet.IDTokenClaims,

View File

@@ -7,19 +7,19 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/adaptors/reader/mock_reader"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"golang.org/x/xerrors"
)
func TestROPC_Do(t *testing.T) {
dummyTokenClaims := oidc.Claims{
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
Pretty: "PRETTY_JSON",
}
timeout := 5 * time.Second
@@ -37,12 +37,12 @@ func TestROPC_Do(t *testing.T) {
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadString(usernamePrompt).Return("USER", nil)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
mockReader := mock_reader.NewMockInterface(ctrl)
mockReader.EXPECT().ReadString(usernamePrompt).Return("USER", nil)
mockReader.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
Reader: mockReader,
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
@@ -76,7 +76,7 @@ func TestROPC_Do(t *testing.T) {
IDTokenClaims: dummyTokenClaims,
}, nil)
u := ROPC{
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
@@ -108,11 +108,11 @@ func TestROPC_Do(t *testing.T) {
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv := mock_reader.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
Reader: mockEnv,
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
@@ -136,11 +136,11 @@ func TestROPC_Do(t *testing.T) {
o := &ROPCOption{
Username: "USER",
}
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv := mock_reader.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
Reader: mockEnv,
Logger: logger.New(t),
}
out, err := u.Do(ctx, o, mock_oidcclient.NewMockInterface(ctrl))
if err == nil {

View File

@@ -51,7 +51,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
u.Logger.V(1).Infof("WARNING: log may contain your secrets such as token or password")
out, err := u.getTokenFromCacheOrProvider(ctx, in)
if err != nil {
return xerrors.Errorf("could not get a token from the cache or provider: %w", err)
return xerrors.Errorf("could not get a token: %w", err)
}
u.Logger.V(1).Infof("writing the token to client-go")
if err := u.Writer.Write(credentialpluginwriter.Output{Token: out.IDToken, Expiry: out.IDTokenClaims.Expiry}); err != nil {
@@ -99,11 +99,9 @@ func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return nil, xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims.Pretty {
u.Logger.V(1).Infof("the ID token has the claim: %s=%v", k, v)
return nil, xerrors.Errorf("authentication error: %w", err)
}
u.Logger.V(1).Infof("you got a token: %s", out.IDTokenClaims.Pretty)
if out.AlreadyHasValidIDToken {
u.Logger.V(1).Infof("you already have a valid token until %s", out.IDTokenClaims.Expiry)
return out, nil

View File

@@ -10,20 +10,20 @@ import (
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/adaptors/tokencache/mock_tokencache"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
"golang.org/x/xerrors"
)
func TestGetToken_Do(t *testing.T) {
dummyTokenClaims := oidc.Claims{
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
Pretty: "PRETTY_JSON",
}
t.Run("FullOptions", func(t *testing.T) {
@@ -98,7 +98,7 @@ func TestGetToken_Do(t *testing.T) {
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Writer: credentialPluginWriter,
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -151,7 +151,7 @@ func TestGetToken_Do(t *testing.T) {
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Writer: credentialPluginWriter,
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -191,7 +191,7 @@ func TestGetToken_Do(t *testing.T) {
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Writer: mock_credentialpluginwriter.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")

View File

@@ -14,25 +14,13 @@ var stage2Tpl = template.Must(template.New("").Parse(`
You got a token with the following claims:
{{- range $k, $v := .Claims }}
{{ $k }}={{ $v }}
{{- end }}
{{ .IDTokenPrettyJSON }}
## 3. Bind a cluster role
Apply the following manifest:
Run the following command:
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: {{ .IssuerURL }}#{{ .Subject }}
kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='{{ .IssuerURL }}#{{ .Subject }}'
## 4. Set up the Kubernetes API server
@@ -51,7 +39,7 @@ Run the following command:
--exec-arg=oidc-login \
--exec-arg=get-token \
{{- range .Args }}
--exec-arg={{ . }}
--exec-arg={{ . }} \
{{- end }}
## 6. Verify cluster access
@@ -68,11 +56,11 @@ You can share the kubeconfig to your team members for on-boarding.
`))
type stage2Vars struct {
Claims map[string]string
IssuerURL string
ClientID string
Args []string
Subject string
IDTokenPrettyJSON string
IssuerURL string
ClientID string
Args []string
Subject string
}
// Stage2Input represents an input DTO of the stage2.
@@ -111,15 +99,15 @@ func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
return xerrors.Errorf("authentication error: %w", err)
}
v := stage2Vars{
Claims: out.IDTokenClaims.Pretty,
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
Args: makeCredentialPluginArgs(in),
Subject: out.IDTokenClaims.Subject,
IDTokenPrettyJSON: out.IDTokenClaims.Pretty,
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
Args: makeCredentialPluginArgs(in),
Subject: out.IDTokenClaims.Subject,
}
var b strings.Builder
if err := stage2Tpl.Execute(&b, &v); err != nil {

View File

@@ -8,18 +8,13 @@ import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
)
func TestSetup_DoStage2(t *testing.T) {
dummyTokenClaims := oidc.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
}
var grantOptionSet authentication.GrantOptionSet
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -50,14 +45,18 @@ func TestSetup_DoStage2(t *testing.T) {
GrantOptionSet: grantOptionSet,
}).
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: "PRETTY_JSON",
},
}, nil)
u := Setup{
Authentication: mockAuthentication,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.DoStage2(ctx, in); err != nil {
t.Errorf("DoStage2 returned error: %+v", err)

View File

@@ -93,11 +93,9 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims.Pretty {
u.Logger.V(1).Infof("the ID token has the claim: %s=%v", k, v)
return xerrors.Errorf("authentication error: %w", err)
}
u.Logger.V(1).Infof("you got a token: %s", out.IDTokenClaims.Pretty)
if out.AlreadyHasValidIDToken {
u.Logger.Printf("You already have a valid token until %s", out.IDTokenClaims.Expiry)
return nil
@@ -108,7 +106,7 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
authProvider.RefreshToken = out.RefreshToken
u.Logger.V(1).Infof("writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
return xerrors.Errorf("could not update the kubeconfig: %w", err)
}
return nil
}
@@ -168,7 +166,7 @@ func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error
}
var b strings.Builder
if err := deprecationTpl.Execute(&b, &v); err != nil {
return xerrors.Errorf("could not render the template: %w", err)
return xerrors.Errorf("template error: %w", err)
}
u.Logger.Printf("%s", b.String())
return nil

View File

@@ -10,18 +10,18 @@ import (
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig/mock_kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
"golang.org/x/xerrors"
)
func TestStandalone_Do(t *testing.T) {
dummyTokenClaims := oidc.Claims{
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: map[string]string{"sub": "YOUR_SUBJECT"},
Pretty: "PRETTY_JSON",
}
t.Run("FullOptions", func(t *testing.T) {
@@ -88,7 +88,7 @@ func TestStandalone_Do(t *testing.T) {
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -131,7 +131,7 @@ func TestStandalone_Do(t *testing.T) {
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -151,7 +151,7 @@ func TestStandalone_Do(t *testing.T) {
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
@@ -188,7 +188,7 @@ func TestStandalone_Do(t *testing.T) {
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
@@ -240,7 +240,7 @@ func TestStandalone_Do(t *testing.T) {
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")

View File

@@ -1,19 +0,0 @@
name: kubelogin
version: git
summary: Log in to the OpenID Connect provider
description: |
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
confinement: strict
base: core18
parts:
kubelogin:
plugin: nil
source: .
source-type: git
build-snaps: [go]
override-build: |
make CIRCLE_TAG=$SNAPCRAFT_PROJECT_VERSION
cp -av kubelogin $SNAPCRAFT_PART_INSTALL/bin
apps:
kubelogin:
command: bin/kubelogin

109
system_test/Makefile Normal file
View File

@@ -0,0 +1,109 @@
CLUSTER_NAME := kubelogin-system-test
OUTPUT_DIR := $(CURDIR)/output
PATH := $(PATH):$(OUTPUT_DIR)/bin
export PATH
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG
# run the login script instead of opening chrome
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
export BROWSER
.PHONY: test
test: build
# see the setup instruction
kubectl oidc-login setup \
--oidc-issuer-url=https://dex-server:10443/dex \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET \
--oidc-extra-scope=email \
--certificate-authority=$(OUTPUT_DIR)/ca.crt
# set up the kubeconfig
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=https://dex-server:10443/dex \
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
--exec-arg=--oidc-extra-scope=email \
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
# make sure we can access the cluster
kubectl --user=oidc cluster-info
# switch the current context
kubectl config set-context --current --user=oidc
# make sure we can access the cluster
kubectl cluster-info
.PHONY: setup
setup: build dex cluster setup-chrome
.PHONY: setup-chrome
setup-chrome: $(OUTPUT_DIR)/ca.crt
# add the dex server certificate to the trust store
mkdir -p ~/.pki/nssdb
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
# build binaries
.PHONY: build
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
go build -o $@ ..
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
go build -o $@ ./chromelogin
# create a Dex server
.PHONY: dex
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
docker create --name dex-server -p 10443:10443 --network kind quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
docker cp $(OUTPUT_DIR)/server.key dex-server:/
docker cp dex.yaml dex-server:/
docker start dex-server
docker logs dex-server
$(OUTPUT_DIR)/ca.key:
mkdir -p $(OUTPUT_DIR)
openssl genrsa -out $@ 2048
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
$(OUTPUT_DIR)/server.key:
mkdir -p $(OUTPUT_DIR)
openssl genrsa -out $@ 2048
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
# create a Kubernetes cluster
.PHONY: cluster
cluster: dex create-cluster
# add the Dex container IP to /etc/hosts of kube-apiserver
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
# wait for kube-apiserver oidc initialization
# (oidc authenticator will retry oidc discovery every 10s)
sleep 10
.PHONY: create-cluster
create-cluster: $(OUTPUT_DIR)/ca.crt
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-system-test-dex-ca.crt
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=admin@example.com
# clean up the resources
.PHONY: clean
clean:
-rm -r $(OUTPUT_DIR)
.PHONY: delete-cluster
delete-cluster:
kind delete cluster --name $(CLUSTER_NAME)
.PHONY: delete-dex
delete-dex:
docker stop dex-server
docker rm dex-server

112
system_test/README.md Normal file
View File

@@ -0,0 +1,112 @@
# kubelogin/system_test
This is an automated test for verifying behavior of the plugin with a real Kubernetes cluster and OIDC provider.
## Purpose
This test checks the following points:
1. User can set up Kubernetes OIDC authentication using [setup guide](../docs/setup.md).
1. User can log in to an OIDC provider on a browser.
1. User can access the cluster using a token returned from the plugin.
It depends on the following components:
- Kubernetes cluster (Kind)
- OIDC provider (Dex)
- Browser (Chrome)
- kubectl command
## How it works
Let's take a look at the diagram.
![diagram](../docs/system-test-diagram.svg)
It prepares the following resources:
1. Generate a pair of CA certificate and TLS server certificate for Dex.
1. Run Dex on a container.
1. Create a Kubernetes cluster using Kind.
1. Mutate `/etc/hosts` of the CI machine to access Dex.
1. Mutate `/etc/hosts` of the kube-apiserver pod to access Dex.
It performs the test by the following steps:
1. Run kubectl.
1. kubectl automatically runs kubelogin.
1. kubelogin automatically runs [chromelogin](chromelogin).
1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
1. kubelogin gets an authorization code from the browser.
1. kubelogin gets a token.
1. kubectl accesses an API with the token.
1. kube-apiserver verifies the token by Dex.
1. Check if kubectl exited with code 0.
## Run locally
You need to set up the following components:
- Docker
- Kind
- Chrome or Chromium
You need to add the following line to `/etc/hosts` so that the browser can access the Dex.
```
127.0.0.1 dex-server
```
Run the test.
```shell script
# run the test
make
# clean up
make delete-cluster
make delete-dex
```
## Technical consideration
### Network and DNS
Consider the following issues:
- kube-apiserver runs on the host network of the kind container.
- kube-apiserver cannot resolve a service name by kube-dns.
- kube-apiserver cannot access a cluster IP.
- kube-apiserver can access another container via the Docker network.
- Chrome requires exactly match of domain name between Dex URL and a server certificate.
Consequently,
- kube-apiserver accesses Dex by resolving `/etc/hosts` and via the Docker network.
- kubelogin and Chrome accesses Dex by resolving `/etc/hosts` and via the Docker network.
### TLS server certificate
Consider the following issues:
- kube-apiserver requires `--oidc-issuer` is HTTPS URL.
- kube-apiserver requires a CA certificate at startup, if `--oidc-ca-file` is given.
- kube-apiserver mounts `/usr/local/share/ca-certificates` from the kind container.
- It is possible to mount a file from the CI machine.
- It is not possible to issue a certificate using Let's Encrypt in runtime.
- Chrome requires a valid certificate in `~/.pki/nssdb`.
As a result,
- kube-apiserver uses the CA certificate of `/usr/local/share/ca-certificates/dex-ca.crt`. See the `extraMounts` section of [`cluster.yaml`](cluster.yaml).
- kubelogin uses the CA certificate in `output/ca.crt`.
- Chrome uses the CA certificate in `~/.pki/nssdb`.
### Test environment
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.

20
system_test/cluster.yaml Normal file
View File

@@ -0,0 +1,20 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
# https://github.com/dexidp/dex/blob/master/Documentation/kubernetes.md
kubeadmConfigPatches:
- |
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
metadata:
name: config
apiServer:
extraArgs:
oidc-issuer-url: https://dex-server:10443/dex
oidc-client-id: YOUR_CLIENT_ID
oidc-username-claim: email
oidc-ca-file: /usr/local/share/ca-certificates/dex-ca.crt
nodes:
- role: control-plane
extraMounts:
- hostPath: /tmp/kubelogin-system-test-dex-ca.crt
containerPath: /usr/local/share/ca-certificates/dex-ca.crt

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