Compare commits

...

60 Commits

Author SHA1 Message Date
Hidetake Iwata
fe2fbcbc53 Refactor: use ghcp for release assets and PR (#219) 2020-01-24 10:59:08 +09:00
Hidetake Iwata
812a965739 go mod tidy 2020-01-24 10:51:45 +09:00
dependabot-preview[bot]
6de1fca64c Build(deps): bump gopkg.in/yaml.v2 from 2.2.7 to 2.2.8 (#217)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.7 to 2.2.8.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.7...v2.2.8)

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

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

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

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

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

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

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

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

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

* Refactor: extract assertCredentialPluginOutput()

* Refactor: add credential plugin test with TLS

* Refactor: extract helpers

* Refactor: rewrite TLS test cases

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

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

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

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-19 13:45:08 -08:00
Hidetake Iwata
8a4d1f5169 Add LICENSE to distribution (#189) 2019-11-15 10:52:12 +09:00
Hidetake Iwata
6f417cd30c Add screencast (#186)
* Update README.md

* Update README.md

* Update README.md
2019-11-08 10:13:19 +09:00
dependabot-preview[bot]
7ba08f4254 Build(deps): bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#184)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.4...v2.2.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-05 21:37:20 +09:00
Hidetake Iwata
e778bbdadc Release v1.15.0 2019-11-01 11:45:25 +09:00
Hidetake Iwata
74108adf00 Update setup.md 2019-11-01 11:37:19 +09:00
Hidetake Iwata
0257b24156 Update README.md 2019-11-01 11:31:16 +09:00
Hidetake Iwata
b8c29985e7 Refactor (#183)
* Refactor: split authentication types

* Refactor: reduce responsibility of oidcclient package
2019-11-01 11:27:28 +09:00
Hidetake Iwata
4683a005c7 Add authorization code flow with keyboard interactive (#182) 2019-11-01 11:01:43 +09:00
Hidetake Iwata
cc48fb4cf7 Refactor: regenerate mocks with newer mockgen (#181) 2019-10-31 11:17:53 +09:00
Hidetake Iwata
ec7f7a062a Refactor: extract GrantOptionSet (#180) 2019-10-31 11:02:03 +09:00
Hidetake Iwata
e9ae98dfaf Fix nonce verification (#179)
fixup cf4e310b2e (#175)
2019-10-31 10:03:29 +09:00
Hidetake Iwata
0c582e97ad Add --grant-type option and username prompt for ROPC (#178) 2019-10-31 00:36:40 +09:00
Hidetake Iwata
5a71247214 Refactor: extract authentication options (#177)
* Refactor: extract authentication options

* Refactor: make subtests
2019-10-30 21:32:51 +09:00
Hidetake Iwata
4a084756c3 Add OAuth 2.0 PKCE support (#176) 2019-10-30 20:47:58 +09:00
Hidetake Iwata
cf4e310b2e Refactor: rename to oidcclient package and extract method (#175)
* Refactor: rename oidc package to oidcclient

* Refactor: extract parseToken method
2019-10-29 10:18:24 +09:00
Hidetake Iwata
4007e7f61a Refactor: extract jwtdecoder package (#174) 2019-10-29 09:55:29 +09:00
Hidetake Iwata
2700e439b9 Refactor: remove kubeconfig.OIDCConfig for single responsibility (#173)
* Refactor: remove kubeconfig.OIDCConfig for single responsibility

* fixup: add comments and rename methods

* fixup: fix methods name

* fixup: replace GetX509OrNil with SetRootCAs
2019-10-28 23:45:17 +09:00
Hidetake Iwata
dbf6238029 Refactor: rename auth package (#172) 2019-10-28 20:02:59 +09:00
Hidetake Iwata
93e893bc36 Refactor: replace ListenPort with BindAddress option (#171) 2019-10-28 19:59:45 +09:00
dependabot-preview[bot]
5dc06ae574 Bump github.com/int128/oauth2cli from 1.7.0 to 1.8.1 (#169)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.7.0 to 1.8.1.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.7.0...v1.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-26 14:04:23 +09:00
Hidetake Iwata
ab1023757b Add single page setup guide (#168) 2019-10-25 22:06:08 +09:00
Hidetake Iwata
e26dbd118e Add diagram of the credential plugin (#145)
* Update README.md

* Added credential-plugin-diagram.svg

* Update credential-plugin-diagram.svg
2019-10-24 20:26:08 +09:00
Hidetake Iwata
fccef52a73 Bump to Go 1.13.3 (#166)
* Bump to Go 1.13.3 and golangci-lint 1.21.0

* go mod tidy
2019-10-22 11:24:46 +09:00
Hidetake Iwata
581284c626 Suppress success log to prevent screen disturbance (#165) 2019-10-19 15:36:47 +09:00
Hidetake Iwata
b5922f9419 Refactor: fix error handling and improve stability (#163)
* Fix ReadPassword() does not respect argument

* Do not ignore error when context has been cancelled

* Use longer timeout to reveal concurrency design failure

* Refactor: use context.TODO in test
2019-10-04 22:28:09 +09:00
Hidetake Iwata
7a0ca206d1 Bump Go 1.13 and dependencies (#162)
* Bump Go 1.13 and dependencies

* Fix lint error
2019-10-04 21:26:26 +09:00
dependabot-preview[bot]
0bca9ef54b Bump gopkg.in/yaml.v2 from 2.2.2 to 2.2.4 (#161)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.2 to 2.2.4.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.2...v2.2.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-04 21:08:52 +09:00
dependabot-preview[bot]
2fb551bf1b Bump github.com/int128/oauth2cli from 1.6.0 to 1.7.0 (#160)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-04 10:50:08 +09:00
Hidetake Iwata
0bc117ddc7 Refactor (#158)
* Refactor: template rendering

* Refactor: rename DecodedIDToken fields

* Refactor: expand command options

* Refactor: improve help messages
2019-09-30 18:27:23 +09:00
Hidetake Iwata
8c640f6c73 Add setup command (#157)
* Add setup command

* Refactor: extract IDTokenSubject instead of sub claims
2019-09-29 18:34:59 +09:00
Hidetake Iwata
8a5efac337 Add deprecation message of standalone mode (#155) 2019-09-28 11:03:03 +09:00
dependabot-preview[bot]
d6e0c761ac Bump github.com/int128/oauth2cli from 1.5.0 to 1.6.0 (#153)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.5.0...v1.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-26 13:07:01 +09:00
dependabot-preview[bot]
8925226afe Bump github.com/go-test/deep from 1.0.3 to 1.0.4 (#150)
Bumps [github.com/go-test/deep](https://github.com/go-test/deep) from 1.0.3 to 1.0.4.
- [Release notes](https://github.com/go-test/deep/releases)
- [Changelog](https://github.com/go-test/deep/blob/master/CHANGES.md)
- [Commits](https://github.com/go-test/deep/compare/v1.0.3...v1.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-26 12:32:20 +09:00
dependabot-preview[bot]
89a0f9a79e Bump github.com/spf13/pflag from 1.0.3 to 1.0.5 (#152)
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.3 to 1.0.5.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.3...v1.0.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-26 12:18:22 +09:00
Hidetake Iwata
74bb4c62c5 Fix expiration of CA certificate for E2E tests (#154) 2019-09-25 23:56:24 +09:00
94 changed files with 4397 additions and 2761 deletions

View File

@@ -2,29 +2,19 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.12.3
- image: circleci/golang:1.13.4
steps:
- run: |
mkdir -p ~/bin
echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
- run: |
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
chmod +x ~/bin/kubectl
- run: |
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.5.0/ghcp_linux_amd64
chmod +x ~/bin/ghcp
- run: |
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.16.0
- run: go get github.com/int128/goxzst
- run: go get github.com/tcnksm/ghr
- run: mkdir -p ~/bin
- run: echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
- checkout
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make run
- run: make ci-setup-linux-amd64
- run: make VERSION=$CIRCLE_TAG ci
- run: |
if [ "$CIRCLE_TAG" ]; then
make release
make VERSION=$CIRCLE_TAG GITHUB_USERNAME=$CIRCLE_PROJECT_USERNAME GITHUB_REPONAME=$CIRCLE_PROJECT_REPONAME release
fi
- store_artifacts:
path: gotest.log
workflows:
version: 2

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

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

3
.gitignore vendored
View File

@@ -2,8 +2,9 @@
/.kubeconfig*
/dist
/dist/output
/coverage.out
/gotest.log
/kubelogin
/kubectl-oidc_login

View File

@@ -1,39 +1,66 @@
# CI must provide the following variables (on tag push)
# VERSION
# GITHUB_USERNAME
# GITHUB_REPONAME
TARGET := kubelogin
TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
VERSION ?= latest
LDFLAGS := -X main.version=$(VERSION)
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
go test -v -race -cover -coverprofile=coverage.out ./...
go test -v -race -cover -coverprofile=coverage.out ./... > gotest.log
$(TARGET): $(wildcard *.go)
go build -o $@ -ldflags "$(LDFLAGS)"
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
.PHONY: run
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
dist:
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
mv dist/gh/kubelogin.rb dist/
mkdir -p dist/plugins
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
.PHONY: dist
dist: dist/output
dist/output:
# make the zip files for GitHub Releases
VERSION=$(VERSION) CGO_ENABLED=0 goxzst -d dist/output -i "LICENSE" -o "$(TARGET)" -t "dist/kubelogin.rb dist/oidc-login.yaml dist/Dockerfile" -- -ldflags "$(LDFLAGS)"
# test the zip file
zipinfo dist/output/kubelogin_linux_amd64.zip
# make the krew yaml structure
mkdir -p dist/output/plugins
mv dist/output/oidc-login.yaml dist/output/plugins/oidc-login.yaml
.PHONY: release
release: dist
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(CIRCLE_TAG)" -m "Bump oidc-login to $(CIRCLE_TAG)" -C dist/ plugins/oidc-login.yaml
# publish the binaries
ghcp release -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)" -t "$(VERSION)" dist/output/
# publish the Homebrew formula
ghcp commit -u "$(GITHUB_USERNAME)" -r "homebrew-$(GITHUB_REPONAME)" -b "bump-$(VERSION)" -m "Bump the version to $(VERSION)" -C dist/output/ kubelogin.rb
ghcp pull-request -u "$(GITHUB_USERNAME)" -r "homebrew-$(GITHUB_REPONAME)" -b "bump-$(VERSION)" --title "Bump the version to $(VERSION)"
# publish the Dockerfile
ghcp commit -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)-docker" -b "bump-$(VERSION)" -m "Bump the version to $(VERSION)" -C dist/output/ Dockerfile
ghcp pull-request -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)-docker" -b "bump-$(VERSION)" --title "Bump the version to $(VERSION)"
# publish the Krew manifest
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(VERSION)" -m "Bump oidc-login to $(VERSION)" -C dist/output/ plugins/oidc-login.yaml
.PHONY: clean
clean:
-rm $(TARGET)
-rm $(TARGET_PLUGIN)
-rm -r dist/
-rm -r dist/output/
-rm coverage.out gotest.log
.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

124
README.md
View File

@@ -2,9 +2,16 @@
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`.
This is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
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">
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.
Then kubelogin gets a token from the provider and kubectl access Kubernetes APIs with the token.
Take a look at the diagram:
![Diagram of the credential plugin](docs/credential-plugin-diagram.svg)
## Getting Started
@@ -21,25 +28,17 @@ brew install int128/kubelogin/kubelogin
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.2/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.15.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
You need to configure the OIDC provider, Kubernetes API server and role binding.
See the following documents for more:
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with dex and GitHub](docs/dex.md)
- [Getting Started with Google Identity Platform](docs/google.md)
Configure the kubeconfig to run kubelogin as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
It provides transparent login without manually running kubelogin command.
For example,
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
The kubeconfig looks like:
```yaml
users:
- name: keycloak
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
@@ -47,11 +46,14 @@ users:
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://issuer.example.com
- --oidc-issuer-url=ISSUER_URL
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
See [the setup guide](docs/setup.md) for more.
### Run
Run kubectl.
@@ -70,7 +72,6 @@ After authentication, kubelogin returns the credentials to kubectl and finally k
```
% kubectl get pods
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
@@ -81,13 +82,24 @@ If the cached ID token is valid, kubelogin just returns it.
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
If the refresh token has expired, kubelogin will perform reauthentication.
### Troubleshoot
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
Kubelogin will perform authentication if the token cache file does not exist.
### Standalone mode
You can dump the claims of token by passing `-v1` option.
As well as you can update the ID token in the kubeconfig by running the command.
See [standalone mode](docs/standalone-mode.md) for more.
```
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
```
## Usage
@@ -98,17 +110,13 @@ If you are looking for a specific version, see [the release tags](https://github
Kubelogin supports the following options:
```
% kubelogin get-token -h
% kubectl oidc-login get-token -h
Run as a kubectl credential plugin
Usage:
kubelogin get-token [flags]
Flags:
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--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
@@ -116,6 +124,12 @@ Flags:
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-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
Global Flags:
@@ -133,7 +147,7 @@ Global Flags:
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
See also the options in [standalone mode](docs/standalone-mode.md).
See also the options of [standalone mode](docs/standalone-mode.md).
### Extra scopes
@@ -170,30 +184,68 @@ You need to register the following redirect URIs to the provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the ports by the option:
You can change the listening address.
```yaml
- --listen-port 12345
- --listen-port 23456
- --listen-address=127.0.0.1:12345
- --listen-address=127.0.0.1:23456
```
#### Authorization code flow with keyboard interactive
If you cannot access the browser, instead use the authorization code flow with keyboard interactive.
```yaml
- --grant-type=authcode-keyboard
```
Kubelogin will show the URL and prompt.
Open the URL in the browser and then copy the code shown.
```
% kubectl get pods
Open https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=...
Enter code: YOUR_CODE
```
Note that this flow uses the redirect URI `urn:ietf:wg:oauth:2.0:oob` and
some OIDC providers do not support it.
#### Resource owner password credentials grant flow
As well as you can use the resource owner password credentials grant flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
Most OIDC providers do not support this flow.
Kubelogin performs the resource owner password credentials grant flow
when `--grant-type=password` or `--username` is set.
You can pass the username and password:
Note that most OIDC providers do not support this flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
You can set the username and password.
```yaml
- --username USERNAME
- --password PASSWORD
- --username=USERNAME
- --password=PASSWORD
```
If the password is not set, kubelogin will show the prompt.
If the password is not set, kubelogin will show the prompt for the password.
```yaml
- --username=USERNAME
```
```
% kubelogin --username USER
% kubectl get pods
Password:
```
If the username is not set, kubelogin will show the prompt for the username and password.
```yaml
- --grant-type=password
```
```
% kubectl get pods
Username: foo
Password:
```
@@ -212,7 +264,7 @@ Feel free to open issues and pull requests for improving code and documents.
### Development
Go 1.12 or later is required.
Go 1.13 or later is required.
```sh
# Run lint and tests

13
dist/Dockerfile vendored Normal file
View File

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

View File

View File

@@ -30,8 +30,10 @@ spec:
sha256: "{{ sha256 .linux_amd64_archive }}"
bin: kubelogin
files:
- from: "kubelogin"
to: "."
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
@@ -40,8 +42,10 @@ spec:
sha256: "{{ sha256 .darwin_amd64_archive }}"
bin: kubelogin
files:
- from: "kubelogin"
to: "."
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: darwin
@@ -50,8 +54,10 @@ spec:
sha256: "{{ sha256 .windows_amd64_archive }}"
bin: kubelogin.exe
files:
- from: "kubelogin.exe"
to: "."
- from: kubelogin.exe
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: windows

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,141 +0,0 @@
# Getting Started with dex and GitHub
## Prerequisite
- You have a GitHub account.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed.
## 1. Setup GitHub OAuth
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
- Application name: (any)
- Homepage URL: `https://dex.example.com`
- Authorization callback URL: `https://dex.example.com/callback`
## 2. Setup dex
Configure the dex with the following config:
```yaml
issuer: https://dex.example.com
connectors:
- type: github
id: github
name: GitHub
config:
clientID: YOUR_GITHUB_CLIENT_ID
clientSecret: YOUR_GITHUB_CLIENT_SECRET
redirectURI: https://dex.example.com/callback
staticClients:
- id: kubernetes
name: Kubernetes
redirectURIs:
- http://localhost:8000
- http://localhost:18000
secret: YOUR_DEX_CLIENT_SECRET
```
Now test authentication with the dex.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://dex.example.com \
--oidc-client-id=kubernetes \
--oidc-client-secret=YOUR_DEX_CLIENT_SECRET
```
You should get claims like:
```
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=kubernetes
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://dex.example.com
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=YOUR_SUBJECT
```
## 3. Setup Kubernetes API server
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
```
--oidc-issuer-url=https://dex.example.com
--oidc-client-id=kubernetes
```
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://dex.example.com
oidcClientID: kubernetes
```
## 4. Create a role binding
Here assign the `cluster-admin` role to your subject.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: YOUR_SUBJECT
```
You can create a custom role and assign it as well.
## 5. Setup kubeconfig
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: dex
name: dex@example.k8s.local
current-context: dex@example.k8s.local
kind: Config
preferences: {}
users:
- name: dex
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://dex.example.com
- --oidc-client-id=kubernetes
- --oidc-client-secret=YOUR_DEX_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Run kubectl
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -1,116 +0,0 @@
# Getting Started with Google Identity Platform
## Prerequisite
- You have a Google account.
- You have the Cluster Admin role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
Now test authentication with Google Identity Platform.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://accounts.google.com \
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
You should get claims like:
```
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=YOUR_CLIENT_ID.apps.googleusercontent.com
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://accounts.google.com
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=YOUR_SUBJECT
```
## 2. Setup Kubernetes API server
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
```
--oidc-issuer-url=https://accounts.google.com
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com
```
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://accounts.google.com
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to your subject.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: YOUR_SUBJECT
```
You can create a custom role and assign it as well.
## 4. Setup kubeconfig
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: google
name: google@example.k8s.local
current-context: google@example.k8s.local
kind: Config
preferences: {}
users:
- name: google
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubelogin
args:
- get-token
- --oidc-issuer-url=https://accounts.google.com
- --oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 5. Run kubectl
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -1,134 +0,0 @@
# Getting Started with Keycloak
## Prerequisite
- You have an administrator role of the Keycloak realm.
- You have an administrator role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed.
## 1. Setup Keycloak
Open the Keycloak and create an OIDC client as follows:
- Client ID: `kubernetes`
- Valid Redirect URLs:
- `http://localhost:8000`
- `http://localhost:18000` (used if the port 8000 is already in use)
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
You can associate client roles by adding the following mapper:
- Name: `groups`
- Mapper Type: `User Client Role`
- Client ID: `kubernetes`
- Client Role prefix: `kubernetes:`
- Token Claim Name: `groups`
- Add to ID token: on
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
Now test authentication with the Keycloak.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--oidc-client-id=kubernetes \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
You should get claims like:
```
I0827 12:29:03.086476 23722 get_token.go:59] the ID token has the claim: groups=[kubernetes:admin]
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=kubernetes
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://keycloak.example.com/auth/realms/YOUR_REALM
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=f08655e2-901f-48e5-8c64-bb9f7784d5df
```
## 2. Setup Kubernetes API server
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
```
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
--oidc-client-id=kubernetes
--oidc-groups-claim=groups
```
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
oidcClientID: kubernetes
oidcGroupsClaim: groups
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: kubernetes:admin
```
You can create a custom role and assign it as well.
## 4. Setup kubeconfig
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: keycloak
name: keycloak@example.k8s.local
current-context: keycloak@example.k8s.local
kind: Config
preferences: {}
users:
- name: keycloak
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubelogin
args:
- get-token
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
- --oidc-client-id=kubernetes
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 5. Run kubectl
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

234
docs/setup.md Normal file
View File

@@ -0,0 +1,234 @@
# Kubernetes OpenID Connection authentication
This document guides how to set up the Kubernetes OpenID Connect (OIDC) authentication.
Let's see the following steps:
1. Set up the OIDC provider
1. Verify authentication
1. Bind a cluster role
1. Set up the Kubernetes API server
1. Set up the kubeconfig
1. Verify cluster access
## 1. Set up the OIDC provider
### Google Identity Platform
You can log in with a Google account.
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
Check the client ID and secret.
Replace the following variables in the later sections.
Variable | Value
------------------------|------
`ISSUER_URL` | `https://accounts.google.com`
`YOUR_CLIENT_ID` | `xxx.apps.googleusercontent.com`
`YOUR_CLIENT_SECRET` | random string
### Keycloak
You can log in with a user of Keycloak.
Make sure you have an administrator role of the Keycloak realm.
Open the Keycloak and create an OIDC client as follows:
- Client ID: `YOUR_CLIENT_ID`
- Valid Redirect URLs:
- `http://localhost:8000`
- `http://localhost:18000` (used if the port 8000 is already in use)
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
You can associate client roles by adding the following mapper:
- Name: `groups`
- Mapper Type: `User Client Role`
- Client ID: `YOUR_CLIENT_ID`
- Client Role prefix: `kubernetes:`
- Token Claim Name: `groups`
- Add to ID token: on
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
Replace the following variables in the later sections.
Variable | Value
------------------------|------
`ISSUER_URL` | `https://keycloak.example.com/auth/realms/YOUR_REALM`
`YOUR_CLIENT_ID` | `YOUR_CLIENT_ID`
`YOUR_CLIENT_SECRET` | random string
### Dex with GitHub
You can log in with a GitHub account.
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
- Application name: (any)
- Homepage URL: `https://dex.example.com`
- Authorization callback URL: `https://dex.example.com/callback`
Deploy the [dex](https://github.com/dexidp/dex) with the following config:
```yaml
issuer: https://dex.example.com
connectors:
- type: github
id: github
name: GitHub
config:
clientID: YOUR_GITHUB_CLIENT_ID
clientSecret: YOUR_GITHUB_CLIENT_SECRET
redirectURI: https://dex.example.com/callback
staticClients:
- id: YOUR_CLIENT_ID
name: Kubernetes
redirectURIs:
- http://localhost:8000
- http://localhost:18000
secret: YOUR_DEX_CLIENT_SECRET
```
Replace the following variables in the later sections.
Variable | Value
------------------------|------
`ISSUER_URL` | `https://dex.example.com`
`YOUR_CLIENT_ID` | `YOUR_CLIENT_ID`
`YOUR_CLIENT_SECRET` | `YOUR_DEX_CLIENT_SECRET`
### Okta
You can log in with an Okta user.
Okta supports [the authorization code flow with PKCE](https://developer.okta.com/docs/guides/implement-auth-code-pkce/overview/)
and this section explains how to set up it.
Open your Okta organization and create an application with the following options:
- Application type: Native
- Initiate login URI: `http://localhost:8000`
- Login redirect URIs:
- `http://localhost:8000`
- `http://localhost:18000` (used if the port 8000 is already in use)
- Allowed grant types: Authorization Code
- Client authentication: Use PKCE (for public clients)
Replace the following variables in the later sections.
Variable | Value
------------------------|------
`ISSUER_URL` | `https://YOUR_ORGANIZATION.okta.com`
`YOUR_CLIENT_ID` | random string
You do not need to set `YOUR_CLIENT_SECRET`.
## 2. Verify authentication
Run the following command:
```sh
kubectl oidc-login setup \
--oidc-issuer-url=ISSUER_URL \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
It will open the browser and you can log in to the provider.
Then it will show the instruction.
## 3. Bind a cluster role
In this tutorial, bind the `cluster-admin` role to you.
Apply the following manifest:
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: ISSUER_URL#YOUR_SUBJECT
```
```sh
kubectl apply -f oidc-cluster-admin.yaml
```
As well as you can create a custom cluster role and bind it.
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
```
--oidc-issuer-url=ISSUER_URL
--oidc-client-id=YOUR_CLIENT_ID
```
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: ISSUER_URL
oidcClientID: YOUR_CLIENT_ID
```
If you are using [kube-aws](https://github.com/kubernetes-incubator/kube-aws), append the following settings to the `cluster.yaml`:
```yaml
oidc:
enabled: true
issuerUrl: ISSUER_URL
clientId: YOUR_CLIENT_ID
```
## 5. Set up the kubeconfig
Add the following user to the kubeconfig:
```yaml
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=ISSUER_URL
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -78,26 +78,24 @@ If the refresh token has expired, kubelogin will proceed the authentication.
Kubelogin supports the following options:
```
% kubelogin -h
Login to the OpenID Connect provider and update the kubeconfig
% kubectl oidc-login -h
Login to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
Usage:
kubelogin [flags]
kubelogin [command]
Examples:
# Login to the provider using the authorization code flow.
kubelogin
# Login to the provider using the resource owner password credentials flow.
kubelogin --username USERNAME --password PASSWORD
# Run as a credential plugin.
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
main [flags]
main [command]
Available Commands:
get-token Run as a kubectl credential plugin
help Help about any command
setup Show the setup instruction
version Print the version information
Flags:
@@ -106,7 +104,9 @@ Flags:
--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
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--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
@@ -122,8 +122,8 @@ Flags:
--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
-h, --help help for main
--version version for main
```
### Kubeconfig

View File

@@ -8,14 +8,17 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
// Run the integration tests of the credential plugin use-case.
@@ -25,8 +28,7 @@ import (
// 3. Open a request for the local server.
// 4. Verify the output.
//
func TestCmd_Run_CredentialPlugin(t *testing.T) {
timeout := 1 * time.Second
func TestCredentialPlugin(t *testing.T) {
cacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
@@ -37,48 +39,273 @@ func TestCmd_Run_CredentialPlugin(t *testing.T) {
}
}()
t.Run("NoTLS", func(t *testing.T) {
testCredentialPlugin(t, cacheDir, keys.None, nil)
})
t.Run("TLS", func(t *testing.T) {
testCredentialPlugin(t, cacheDir, keys.Server, []string{"--certificate-authority", keys.Server.CACertPath})
})
}
func testCredentialPlugin(t *testing.T, cacheDir string, idpTLS keys.Keys, extraArgs []string) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(gomock.Any()).
Do(func(out credentialplugin.Output) {
if out.Token != idToken {
t.Errorf("Token wants %s but %s", idToken, out.Token)
}
if out.Expiry != tokenExpiryFuture {
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
}
})
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
runGetTokenCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
credentialPluginInteraction,
"--skip-open-browser",
"--listen-port", "0",
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
)
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, 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()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
setupMockIDPForROPC(service, serverURL, "openid", "USER", "PASS", idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
"--username", "USER",
"--password", "PASS",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, 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()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
setupTokenCache(t, cacheDir,
tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
CACertFilename: idpTLS.CACertPath,
}, tokencache.Value{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
assertTokenCache(t, cacheDir,
tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
CACertFilename: idpTLS.CACertPath,
}, 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()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
setupMockIDPForDiscovery(service, serverURL)
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(validIDToken, "NEW_REFRESH_TOKEN"), nil)
setupTokenCache(t, cacheDir,
tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
CACertFilename: idpTLS.CACertPath,
}, tokencache.Value{
IDToken: expiredIDToken,
RefreshToken: "VALID_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &validIDToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
assertTokenCache(t, cacheDir,
tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
CACertFilename: idpTLS.CACertPath,
}, 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()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &validIDToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
setupTokenCache(t, cacheDir,
tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
CACertFilename: idpTLS.CACertPath,
}, tokencache.Value{
IDToken: expiredIDToken,
RefreshToken: "EXPIRED_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &validIDToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
assertTokenCache(t, cacheDir,
tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
CACertFilename: idpTLS.CACertPath,
}, tokencache.Value{
IDToken: validIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "email profile openid", &idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
})
}
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc auth.LocalServerReadyFunc, interaction credentialplugin.Interface, args ...string) {
func assertCredentialPluginOutput(t *testing.T, credentialPluginInteraction *mock_credentialplugin.MockInterface, idToken *string) {
credentialPluginInteraction.EXPECT().
Write(gomock.Any()).
Do(func(out credentialplugin.Output) {
if out.Token != *idToken {
t.Errorf("Token wants %s but %s", *idToken, out.Token)
}
if out.Expiry != tokenExpiryFuture {
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
}
})
}
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, interaction credentialplugin.Interface, args []string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, interaction)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "get-token", "--v=1"}, args...), "HEAD")
exitCode := cmd.Run(ctx, append([]string{
"kubelogin", "get-token",
"--v=1",
"--skip-open-browser",
"--listen-address", "127.0.0.1:0",
}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func setupTokenCache(t *testing.T, cacheDir string, k tokencache.Key, v tokencache.Value) {
var r tokencache.Repository
err := r.Save(cacheDir, k, v)
if err != nil {
t.Errorf("could not set up the token cache: %s", err)
}
}
func assertTokenCache(t *testing.T, cacheDir string, k tokencache.Key, want tokencache.Value) {
var r tokencache.Repository
got, err := r.FindByKey(cacheDir, 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)
}
}

91
e2e_test/helpers_test.go Normal file
View File

@@ -0,0 +1,91 @@
package e2e_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
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 setupMockIDPForDiscovery(service *mock_idp.MockService, serverURL string) {
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
}
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
var nonce string
setupMockIDPForDiscovery(service, serverURL)
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
service.EXPECT().Exchange("YOUR_AUTH_CODE").
DoAndReturn(func(string) (*idp.TokenResponse, error) {
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
})
}
func setupMockIDPForROPC(service *mock_idp.MockService, serverURL, scope, username, password, idToken string) {
setupMockIDPForDiscovery(service, serverURL)
service.EXPECT().AuthenticatePassword(username, password, scope).
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
}
func openBrowserOnReadyFunc(t *testing.T, ctx context.Context, k keys.Keys) authentication.LocalServerReadyFunc {
return 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)
}
}
}

View File

@@ -35,6 +35,7 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder {
// AuthenticateCode mocks base method
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
@@ -43,11 +44,13 @@ func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
// AuthenticateCode indicates an expected call of AuthenticateCode
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
}
// AuthenticatePassword mocks base method
func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
@@ -56,11 +59,13 @@ func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenR
// AuthenticatePassword indicates an expected call of AuthenticatePassword
func (mr *MockServiceMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockService)(nil).AuthenticatePassword), arg0, arg1, arg2)
}
// Discovery mocks base method
func (m *MockService) Discovery() *idp.DiscoveryResponse {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Discovery")
ret0, _ := ret[0].(*idp.DiscoveryResponse)
return ret0
@@ -68,11 +73,13 @@ func (m *MockService) Discovery() *idp.DiscoveryResponse {
// Discovery indicates an expected call of Discovery
func (mr *MockServiceMockRecorder) Discovery() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockService)(nil).Discovery))
}
// Exchange mocks base method
func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exchange", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
@@ -81,11 +88,13 @@ func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
// Exchange indicates an expected call of Exchange
func (mr *MockServiceMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockService)(nil).Exchange), arg0)
}
// GetCertificates mocks base method
func (m *MockService) GetCertificates() *idp.CertificatesResponse {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCertificates")
ret0, _ := ret[0].(*idp.CertificatesResponse)
return ret0
@@ -93,11 +102,13 @@ func (m *MockService) GetCertificates() *idp.CertificatesResponse {
// GetCertificates indicates an expected call of GetCertificates
func (mr *MockServiceMockRecorder) GetCertificates() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockService)(nil).GetCertificates))
}
// Refresh mocks base method
func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
@@ -106,5 +117,6 @@ func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
// Refresh indicates an expected call of Refresh
func (mr *MockServiceMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockService)(nil).Refresh), arg0)
}

View File

@@ -4,68 +4,64 @@ import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"golang.org/x/xerrors"
)
// TLSCACert is path to the CA certificate.
// This should be generated by Makefile before test.
const TLSCACert = "keys/testdata/ca.crt"
// Keys represents a pair of certificate and key.
type Keys struct {
CertPath string
KeyPath string
CACertPath string
TLSConfig *tls.Config
}
// TLSCACertAsBase64 is a base64 encoded string of TLSCACert.
var TLSCACertAsBase64 string
// None represents non-TLS.
var None Keys
// TLSCACertAsConfig is a TLS config including TLSCACert.
var TLSCACertAsConfig = &tls.Config{RootCAs: x509.NewCertPool()}
// TLSServerCert is path to the server certificate.
// This should be generated by Makefile before test.
const TLSServerCert = "keys/testdata/server.crt"
// TLSServerKey is path to the server key.
// This should be generated by Makefile before test.
const TLSServerKey = "keys/testdata/server.key"
// 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",
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 *rsa.PrivateKey
var JWSKeyPair = readPrivateKey(JWSKey)
func init() {
var err error
JWSKeyPair, err = readPrivateKey(JWSKey)
if err != nil {
panic(err)
}
b, err := ioutil.ReadFile(TLSCACert)
if err != nil {
panic(err)
}
TLSCACertAsBase64 = base64.StdEncoding.EncodeToString(b)
if !TLSCACertAsConfig.RootCAs.AppendCertsFromPEM(b) {
panic("could not append the CA cert")
}
}
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
func newTLSConfig(name string) *tls.Config {
b, err := ioutil.ReadFile(name)
if err != nil {
return nil, xerrors.Errorf("could not read JWSKey: %w", err)
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 {
return nil, xerrors.New("could not decode PEM")
panic("could not decode PEM")
}
if len(rest) > 0 {
return nil, xerrors.New("PEM should contain single key but multiple keys")
panic("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Errorf("could not parse the key: %w", err)
panic(err)
}
return k, nil
return k
}

View File

@@ -1,3 +1,5 @@
EXPIRY := 3650
all: ca.key ca.crt server.key server.crt jws.key
.PHONY: clean
@@ -17,7 +19,9 @@ ca.csr: openssl.cnf ca.key
openssl req -noout -text -in $@
ca.crt: ca.csr ca.key
openssl x509 -req \
openssl x509 \
-req \
-days $(EXPIRY) \
-signkey ca.key \
-in ca.csr \
-out $@
@@ -42,6 +46,7 @@ server.crt: openssl.cnf server.csr ca.key ca.crt
touch CA/index.txt.attr
echo 00 > CA/serial
openssl ca -config openssl.cnf \
-days $(EXPIRY) \
-extensions v3_req \
-batch \
-cert ca.crt \

View File

@@ -1,11 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBnTCCAQYCCQCuPrhkr+BvGzANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
ZWxsbyBDQTAeFw0xOTA4MTgwNjAwMDZaFw0xOTA5MTcwNjAwMDZaMBMxETAPBgNV
MIIBnTCCAQYCCQC/aR7GRyndljANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
ZWxsbyBDQTAeFw0xOTA5MjUxNDQ0NTFaFw0yOTA5MjIxNDQ0NTFaMBMxETAPBgNV
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
ACfgNePlOLnLz1zJrWN6RZ6q0a+SSK8HdgSiKSF66SBIRILFoQmapBLXRY9YyATt
cdgg7pOd1WGCMqlOnhL56c8X5n+j/LGM5hc9PaEJA5vru7EBrnbxCkg0n8yp4Swc
8KFV5IiZ5D8t03AHjrXLQg8/HRzTuFRJJ1nJmc+FbnjT
AE/gsgTC4jzYC3icZdhALJTe3JsZ7geN702dE95zSI5LXAzzHJ/j8wGmorQjrMs2
iNPjVOdTU6cVWa1Ba29wWakVyVCUqDmDiWHaVhM/Qyyxo6mVlZGFwSnto3zq/h4y
KMFJ8lUtFCYMrzo5wqgj2xOjVrN77F6F4XWZbMufh50G
-----END CERTIFICATE-----

View File

@@ -10,7 +10,6 @@ new_certs_dir = $dir
default_md = sha256
policy = policy_match
serial = $dir/serial
default_days = 3650
[ policy_match ]
countryName = optional

View File

@@ -8,6 +8,8 @@ import (
"net"
"net/http"
"testing"
"github.com/int128/kubelogin/e2e_test/keys"
)
type Shutdowner interface {
@@ -28,7 +30,15 @@ func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
}
// Start starts an authentication server.
func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
// 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 {
return startNoTLS(t, h)
}
return startTLS(t, h, k)
}
func startNoTLS(t *testing.T, h http.Handler) (string, Shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
@@ -44,8 +54,7 @@ func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
return url, &shutdowner{l, s}
}
// Start starts an authentication server with TLS.
func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Shutdowner) {
func startTLS(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
@@ -53,7 +62,7 @@ func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Sh
Handler: h,
}
go func() {
err := s.ServeTLS(l, cert, key)
err := s.ServeTLS(l, k.CertPath, k.KeyPath)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}

View File

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

28
go.mod
View File

@@ -4,22 +4,22 @@ go 1.12
require (
github.com/coreos/go-oidc v2.1.0+incompatible
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda
github.com/go-test/deep v1.0.3
github.com/golang/mock v1.3.1
github.com/google/wire v0.3.0
github.com/int128/oauth2cli v1.5.0
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/google/wire v0.4.0
github.com/int128/oauth2cli v1.8.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/pflag v1.0.3
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
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/xerrors v0.0.0-20191204190536-9bdfabe68543
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
k8s.io/klog v0.4.0
gopkg.in/yaml.v2 v2.2.8
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
k8s.io/klog v1.0.0
)

234
go.sum
View File

@@ -1,135 +1,230 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
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-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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda h1:NyywMz59neOoVRFDz+ccfKWxn784fiHMDnZSy6T+JXY=
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/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/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/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/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/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/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
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/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/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
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=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/go-cmp v0.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/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.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=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60=
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/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/oauth2cli v1.4.1 h1:IsaYMafEDS1jyArxYdmksw+nMsNxiYCQzdkPj3QF9BY=
github.com/int128/oauth2cli v1.4.1/go.mod h1:CMJjyUSgKiobye1M/9byFACOjtB2LRo2mo7boklEKlI=
github.com/int128/oauth2cli v1.5.0 h1:EOBMCWfroql1hPqPhP+EtDhgO7y6ClFZ/NwJEpBCo1s=
github.com/int128/oauth2cli v1.5.0/go.mod h1:ivzuzt+k+bpwLI1Mb1bRq8PiBvwLBsO8L7tX2F9iKKA=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/int128/listener v1.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/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/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/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=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/ginkgo v1.10.1/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/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/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/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/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/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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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/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/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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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-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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/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-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-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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/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=
@@ -137,18 +232,29 @@ 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=
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9L4wrirlwYJ6w88jUQxTUw=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
gopkg.in/yaml.v2 v2.2.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=
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/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=
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/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View File

@@ -0,0 +1,71 @@
// Package certpool provides loading certificates from files or base64 encoded string.
package certpool
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Value(NewFunc(New)),
wire.Struct(new(CertPool), "*"),
wire.Bind(new(Interface), new(*CertPool)),
)
type NewFunc func() Interface
// New returns an instance which implements the Interface.
func New() Interface {
return &CertPool{pool: x509.NewCertPool()}
}
type Interface interface {
AddFile(filename string) error
AddBase64Encoded(s string) error
SetRootCAs(cfg *tls.Config)
}
// CertPool represents a pool of certificates.
type CertPool struct {
pool *x509.CertPool
}
// SetRootCAs sets cfg.RootCAs if it has any certificate.
// Otherwise do nothing.
func (p *CertPool) SetRootCAs(cfg *tls.Config) {
if len(p.pool.Subjects()) > 0 {
cfg.RootCAs = p.pool
}
}
// AddFile loads the certificate from the file.
func (p *CertPool) AddFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
// AddBase64Encoded loads the certificate from the base64 encoded string.
func (p *CertPool) AddBase64Encoded(s string) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,58 @@
package certpool
import (
"crypto/tls"
"io/ioutil"
"testing"
)
func TestCertPool_AddFile(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
p := New()
if err := p.AddFile("testdata/ca1.crt"); err != nil {
t.Errorf("AddFile error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
})
t.Run("Invalid", func(t *testing.T) {
p := New()
err := p.AddFile("testdata/Makefile")
if err == nil {
t.Errorf("AddFile wants an error but was nil")
}
})
}
func TestCertPool_AddBase64Encoded(t *testing.T) {
p := New()
if err := p.AddBase64Encoded(readFile(t, "testdata/ca2.crt.base64")); err != nil {
t.Errorf("AddBase64Encoded error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
}
func TestCertPool_SetRootCAs(t *testing.T) {
p := New()
var cfg tls.Config
p.SetRootCAs(&cfg)
if cfg.RootCAs != nil {
t.Errorf("cfg.RootCAs wants nil but was %+v", cfg.RootCAs)
}
}
func readFile(t *testing.T, filename string) string {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return string(b)
}

View File

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

View File

@@ -16,28 +16,21 @@ var Set = wire.NewSet(
wire.Bind(new(Interface), new(*Cmd)),
wire.Struct(new(Root), "*"),
wire.Struct(new(GetToken), "*"),
wire.Struct(new(Setup), "*"),
)
type Interface interface {
Run(ctx context.Context, args []string, version string) int
}
const examples = ` # Login to the provider using the authorization code flow.
%[1]s
# Login to the provider using the resource owner password credentials flow.
%[1]s --username USERNAME --password PASSWORD
# Run as a credential plugin.
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
var defaultListenPort = []int{8000, 18000}
var defaultListenAddress = []string{"127.0.0.1:8000", "127.0.0.1:18000"}
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Root *Root
GetToken *GetToken
Setup *Setup
Logger logger.Interface
}
@@ -54,6 +47,9 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
getTokenCmd := cmd.GetToken.New(ctx)
rootCmd.AddCommand(getTokenCmd)
setupCmd := cmd.Setup.New(ctx)
rootCmd.AddCommand(setupCmd)
versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version information",

View File

@@ -6,6 +6,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
@@ -16,213 +17,344 @@ func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("login/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
ListenPort: defaultListenPort,
t.Run("root", func(t *testing.T) {
tests := map[string]struct {
args []string
in standalone.Input
}{
"Defaults": {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: defaultListenAddress,
},
},
},
},
"when --listen-port is set, it should convert the port to address": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
},
},
},
},
"when --listen-port is set, it should ignore --listen-address flags": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:30080",
"--listen-address", "127.0.0.1:40080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
},
},
},
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"--grant-type", "authcode-keyboard",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
t.Run("login/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
t.Run("get-token", func(t *testing.T) {
tests := map[string]struct {
args []string
in credentialplugin.Input
}{
"Defaults": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
},
},
},
},
"FullOptions": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "authcode-keyboard",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
ListenPort: defaultListenPort,
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
})
cmd := Cmd{
Root: &Root{
t.Run("MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
})
cmd := Cmd{
Root: &Root{
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("get-token/MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
}

View File

@@ -12,20 +12,18 @@ import (
// getTokenOptions represents the options for get-token command.
type getTokenOptions struct {
loginOptions
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
TokenCacheDir string
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
TokenCacheDir string
authenticationOptions authenticationOptions
}
func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
o.loginOptions.register(f)
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
@@ -33,6 +31,7 @@ func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
o.authenticationOptions.register(f)
}
type GetToken struct {
@@ -58,18 +57,19 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
return nil
},
RunE: func(*cobra.Command, []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("error: %w", err)
}
in := credentialplugin.Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
TokenCacheDir: o.TokenCacheDir,
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
}
if err := cmd.GetToken.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)

View File

@@ -3,75 +3,132 @@ package cmd
import (
"context"
"fmt"
"strings"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// kubectlOptions represents kubectl specific options.
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
const longDescription = `Login to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
`
// rootOptions represents the options for the root command.
type rootOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
authenticationOptions authenticationOptions
}
func (o *kubectlOptions) register(f *pflag.FlagSet) {
func (o *rootOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
}
// loginOptions represents the options for Login use-case.
type loginOptions struct {
ListenPort []int
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
SkipOpenBrowser bool
Username string
Password string
}
func (o *loginOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
// determineListenAddress returns the addresses from the flags.
// Note that --listen-address is always given due to the default value.
// If --listen-port is not set, it returns --listen-address.
// If --listen-port is set, it returns the strings of --listen-port.
func (o *authenticationOptions) determineListenAddress() []string {
if len(o.ListenPort) == 0 {
return o.ListenAddress
}
var a []string
for _, p := range o.ListenPort {
a = append(a, fmt.Sprintf("127.0.0.1:%d", p))
}
return a
}
var allGrantType = strings.Join([]string{
"auto",
"authcode",
"authcode-keyboard",
"password",
}, "|")
func (o *authenticationOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("The authorization grant type to use. One of (%s)", allGrantType))
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "Address to bind to the local server. If multiple addresses are given, it will try binding in order")
//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.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeOption = &authentication.AuthCodeOption{
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &authentication.ROPCOption{
Username: o.Username,
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}
type Root struct {
Standalone standalone.Interface
Logger logger.Interface
}
func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
var o struct {
kubectlOptions
loginOptions
}
var o rootOptions
rootCmd := &cobra.Command{
Use: executable,
Short: "Login to the OpenID Connect provider and update the kubeconfig",
Example: fmt.Sprintf(examples, executable),
Args: cobra.NoArgs,
Use: executable,
Short: "Login to the OpenID Connect provider",
Long: longDescription,
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("invalid option: %w", err)
}
in := standalone.Input{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
GrantOptionSet: grantOptionSet,
}
if err := cmd.Standalone.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
@@ -79,8 +136,7 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
return nil
},
}
o.kubectlOptions.register(rootCmd.Flags())
o.loginOptions.register(rootCmd.Flags())
o.register(rootCmd.Flags())
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
return rootCmd
}

73
pkg/adaptors/cmd/setup.go Normal file
View File

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

View File

@@ -35,6 +35,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// Write mocks base method
func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -42,5 +43,6 @@ func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
// Write indicates an expected call of Write
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)
}

View File

@@ -1,9 +1,13 @@
// Package env provides environment dependent facilities.
package env
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"time"
"github.com/google/wire"
"github.com/pkg/browser"
@@ -27,21 +31,37 @@ var Set = wire.NewSet(
)
type Interface interface {
ReadString(prompt string) (string, error)
ReadPassword(prompt string) (string, error)
OpenBrowser(url 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, "Password: "); err != nil {
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: %w", err)
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)
@@ -56,3 +76,8 @@ func (env *Env) OpenBrowser(url string) error {
}
return nil
}
// Now returns the current time.
func (*Env) Now() time.Time {
return time.Now()
}

View File

@@ -7,6 +7,7 @@ package mock_env
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
time "time"
)
// MockInterface is a mock of Interface interface
@@ -32,8 +33,23 @@ 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))
}
// OpenBrowser mocks base method
func (m *MockInterface) OpenBrowser(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -41,11 +57,13 @@ func (m *MockInterface) OpenBrowser(arg0 string) error {
// OpenBrowser indicates an expected call of OpenBrowser
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
}
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
@@ -54,5 +72,21 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}
// ReadString mocks base method
func (m *MockInterface) ReadString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadString", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadString indicates an expected call of ReadString
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)
}

View File

@@ -1,4 +1,5 @@
package oidc
// Package jwtdecoder provides decoding a JWT.
package jwtdecoder
import (
"bytes"
@@ -8,24 +9,29 @@ import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/domain/oidc"
"golang.org/x/xerrors"
)
type DecoderInterface interface {
DecodeIDToken(t string) (*DecodedIDToken, error)
}
//go:generate mockgen -destination mock_jwtdecoder/mock_jwtdecoder.go github.com/int128/kubelogin/pkg/adaptors/jwtdecoder Interface
type DecodedIDToken struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
// 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{}
// DecodeIDToken returns the claims of the ID token.
// Decode returns the claims of the JWT.
// Note that this method does not verify the signature and always trust it.
func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
parts := strings.Split(t, ".")
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")
}
@@ -41,18 +47,19 @@ func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
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 &DecodedIDToken{
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
IDTokenClaims: dumpRawClaims(rawClaims),
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.(type) {
switch v := v.(type) {
case float64:
claims[k] = fmt.Sprintf("%.f", v.(float64))
claims[k] = fmt.Sprintf("%.f", v)
default:
claims[k] = fmt.Sprintf("%v", v)
}

View File

@@ -1,4 +1,4 @@
package oidc
package jwtdecoder
import (
"crypto/rsa"
@@ -11,23 +11,23 @@ import (
"github.com/dgrijalva/jwt-go"
)
func TestDecoder_DecodeIDToken(t *testing.T) {
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.DecodeIDToken(idToken)
decodedToken, err := decoder.Decode(idToken)
if err != nil {
t.Fatalf("DecodeIDToken error: %s", err)
t.Fatalf("Decode error: %s", err)
}
if decodedToken.IDTokenExpiry != expiry {
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry)
if decodedToken.Expiry != expiry {
t.Errorf("Expiry wants %s but %s", expiry, decodedToken.Expiry)
}
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims)
t.Logf("Pretty=%+v", decodedToken.Pretty)
})
t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
decodedToken, err := decoder.Decode("HEADER.INVALID_TOKEN.SIGNATURE")
if err == nil {
t.Errorf("error wants non-nil but nil")
} else {

View File

@@ -0,0 +1,49 @@
// 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

@@ -2,6 +2,7 @@ package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
//go:generate mockgen -destination mock_kubeconfig/mock_kubeconfig.go github.com/int128/kubelogin/pkg/adaptors/kubeconfig Interface
@@ -26,22 +27,19 @@ type UserName string
// AuthProvider represents the authentication provider,
// i.e. context, user and auth-provider in a kubeconfig.
type AuthProvider struct {
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // Context name (optional)
OIDCConfig OIDCConfig
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // (optional) Context name
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id
ClientSecret string // (optional) client-secret
IDPCertificateAuthority string // (optional) idp-certificate-authority
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
ExtraScopes []string // (optional) extra-scopes
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
}
// OIDCConfig represents a configuration of an OIDC provider.
type OIDCConfig struct {
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id
ClientSecret string // client-secret
IDPCertificateAuthority string // (optional) idp-certificate-authority
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
ExtraScopes []string // (optional) extra-scopes
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
type Kubeconfig struct {
Logger logger.Interface
}
type Kubeconfig struct{}

View File

@@ -58,20 +58,16 @@ func findCurrentAuthProvider(config *api.Config, contextName ContextName, userNa
if userNode.AuthProvider.Config == nil {
return nil, xerrors.New("auth-provider.config is missing")
}
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
OIDCConfig: makeOIDCConfig(userNode.AuthProvider.Config),
}, nil
}
func makeOIDCConfig(m map[string]string) OIDCConfig {
m := userNode.AuthProvider.Config
var extraScopes []string
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return OIDCConfig{
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
IDPIssuerURL: m["idp-issuer-url"],
ClientID: m["client-id"],
ClientSecret: m["client-secret"],
@@ -80,5 +76,5 @@ func makeOIDCConfig(m map[string]string) OIDCConfig {
ExtraScopes: extraScopes,
IDToken: m["id-token"],
RefreshToken: m["refresh-token"],
}
}, nil
}

View File

@@ -4,7 +4,7 @@ import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
"k8s.io/client-go/tools/clientcmd/api"
)
@@ -76,7 +76,7 @@ func unsetenv(t *testing.T, key string) {
func Test_findCurrentAuthProvider(t *testing.T) {
t.Run("CurrentContext", func(t *testing.T) {
auth, err := findCurrentAuthProvider(&api.Config{
got, err := findCurrentAuthProvider(&api.Config{
CurrentContext: "theContext",
Contexts: map[string]*api.Context{
"theContext": {
@@ -106,27 +106,25 @@ func Test_findCurrentAuthProvider(t *testing.T) {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("ByContextName", func(t *testing.T) {
auth, err := findCurrentAuthProvider(&api.Config{
got, err := findCurrentAuthProvider(&api.Config{
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
@@ -151,17 +149,15 @@ func Test_findCurrentAuthProvider(t *testing.T) {
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
IDPIssuerURL: "https://accounts.google.com",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("ByUserName", func(t *testing.T) {
auth, err := findCurrentAuthProvider(&api.Config{
got, err := findCurrentAuthProvider(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
@@ -180,12 +176,10 @@ func Test_findCurrentAuthProvider(t *testing.T) {
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
IDPIssuerURL: "https://accounts.google.com",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})

View File

@@ -35,6 +35,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// GetCurrentAuthProvider mocks base method
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
ret1, _ := ret[1].(error)
@@ -43,11 +44,13 @@ func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.Cont
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuthProvider mocks base method
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -55,5 +58,6 @@ func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
}

View File

@@ -7,14 +7,14 @@ import (
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuthProvider(auth *AuthProvider) error {
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
func (*Kubeconfig) UpdateAuthProvider(p *AuthProvider) error {
config, err := clientcmd.LoadFromFile(p.LocationOfOrigin)
if err != nil {
return xerrors.Errorf("could not load %s: %w", auth.LocationOfOrigin, err)
return xerrors.Errorf("could not load %s: %w", p.LocationOfOrigin, err)
}
userNode, ok := config.AuthInfos[string(auth.UserName)]
userNode, ok := config.AuthInfos[string(p.UserName)]
if !ok {
return xerrors.Errorf("user %s does not exist", auth.UserName)
return xerrors.Errorf("user %s does not exist", p.UserName)
}
if userNode.AuthProvider == nil {
return xerrors.Errorf("auth-provider is missing")
@@ -22,23 +22,23 @@ func (*Kubeconfig) UpdateAuthProvider(auth *AuthProvider) error {
if userNode.AuthProvider.Name != "oidc" {
return xerrors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
}
copyOIDCConfig(auth.OIDCConfig, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, auth.LocationOfOrigin); err != nil {
return xerrors.Errorf("could not update %s: %w", auth.LocationOfOrigin, err)
copyAuthProviderConfig(p, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, p.LocationOfOrigin); err != nil {
return xerrors.Errorf("could not update %s: %w", p.LocationOfOrigin, err)
}
return nil
}
func copyOIDCConfig(config OIDCConfig, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", config.IDPIssuerURL)
setOrDeleteKey(m, "client-id", config.ClientID)
setOrDeleteKey(m, "client-secret", config.ClientSecret)
setOrDeleteKey(m, "idp-certificate-authority", config.IDPCertificateAuthority)
setOrDeleteKey(m, "idp-certificate-authority-data", config.IDPCertificateAuthorityData)
extraScopes := strings.Join(config.ExtraScopes, ",")
func copyAuthProviderConfig(p *AuthProvider, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", p.IDPIssuerURL)
setOrDeleteKey(m, "client-id", p.ClientID)
setOrDeleteKey(m, "client-secret", p.ClientSecret)
setOrDeleteKey(m, "idp-certificate-authority", p.IDPCertificateAuthority)
setOrDeleteKey(m, "idp-certificate-authority-data", p.IDPCertificateAuthorityData)
extraScopes := strings.Join(p.ExtraScopes, ",")
setOrDeleteKey(m, "extra-scopes", extraScopes)
setOrDeleteKey(m, "id-token", config.IDToken)
setOrDeleteKey(m, "refresh-token", config.RefreshToken)
setOrDeleteKey(m, "id-token", p.IDToken)
setOrDeleteKey(m, "refresh-token", p.RefreshToken)
}
func setOrDeleteKey(m map[string]string, key, value string) {

View File

@@ -4,6 +4,8 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
@@ -19,13 +21,11 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
@@ -34,9 +34,10 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
t.Fatalf("Could not read kubeconfig: %s", err)
}
got := string(b)
want := `apiVersion: v1
clusters: []
contexts: []
clusters: null
contexts: null
current-context: ""
kind: Config
preferences: {}
@@ -52,8 +53,8 @@ users:
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
}
})
@@ -65,18 +66,16 @@ users:
}
}()
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
LocationOfOrigin: f.Name(),
UserName: "google",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
@@ -85,9 +84,10 @@ users:
t.Fatalf("Could not read kubeconfig: %s", err)
}
got := string(b)
want := `apiVersion: v1
clusters: []
contexts: []
clusters: null
contexts: null
current-context: ""
kind: Config
preferences: {}
@@ -106,8 +106,8 @@ users:
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
}
})
}

View File

@@ -1,161 +0,0 @@
package oidc
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type Interface interface {
AuthenticateByCode(ctx context.Context, localServerPort []int, localServerReadyChan chan<- string) (*TokenSet, error)
AuthenticateByPassword(ctx context.Context, username, password string) (*TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
}
// TokenSet represents an output DTO of
// Interface.AuthenticateByCode, Interface.AuthenticateByPassword and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
}
func (c *client) wrapContext(ctx context.Context) context.Context {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
return ctx
}
// AuthenticateByCode performs the authorization code flow.
func (c *client) AuthenticateByCode(ctx context.Context, localServerPort []int, localServerReadyChan chan<- string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
nonce, err := newNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce parameter")
}
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
LocalServerPort: localServerPort,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
LocalServerReadyChan: localServerReadyChan,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
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})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
if verifiedIDToken.Nonce != nonce {
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func newNonce() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", xerrors.Errorf("error while reading random: %w", err)
}
return fmt.Sprintf("%x", n), nil
}
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(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)
}
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})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: refreshToken,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
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})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
return dumpRawClaims(rawClaims), err
}

View File

@@ -1,122 +0,0 @@
package oidc
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type FactoryInterface interface {
New(ctx context.Context, config ClientConfig) (Interface, error)
}
// ClientConfig represents a configuration of an Interface to create.
type ClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
SkipTLSVerify bool
}
type Factory struct {
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, config ClientConfig) (Interface, error) {
tlsConfig, err := f.tlsConfigFor(config)
if err != nil {
return nil, xerrors.Errorf("could not initialize TLS config: %w", err)
}
baseTransport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.Config.IDPIssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDCFactory issuer: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.Config.ClientID,
ClientSecret: config.Config.ClientSecret,
Scopes: append(config.Config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
}, nil
}
func (f *Factory) tlsConfigFor(config ClientConfig) (*tls.Config, error) {
pool := x509.NewCertPool()
if config.Config.IDPCertificateAuthority != "" {
f.Logger.V(1).Infof("loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if config.Config.IDPCertificateAuthorityData != "" {
f.Logger.V(1).Infof("loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if config.CACertFilename != "" {
f.Logger.V(1).Infof("loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
c := &tls.Config{
InsecureSkipVerify: config.SkipTLSVerify,
}
if len(pool.Subjects()) > 0 {
c.RootCAs = pool
}
return c, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -1,88 +0,0 @@
package oidc
import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
)
func TestFactory_tlsConfigFor(t *testing.T) {
testingLogger := mock_logger.New(t)
factory := &Factory{Logger: testingLogger}
t.Run("Defaults", func(t *testing.T) {
c, err := factory.tlsConfigFor(ClientConfig{})
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants false but true")
}
if c.RootCAs != nil {
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
config := ClientConfig{
SkipTLSVerify: true,
}
c, err := factory.tlsConfigFor(config)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if !c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants true but false")
}
if c.RootCAs != nil {
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
}
})
t.Run("AllCertificates", func(t *testing.T) {
config := ClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/tls/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/tls/ca2.crt.base64")),
},
CACertFilename: "testdata/tls/ca3.crt",
}
c, err := factory.tlsConfigFor(config)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants false but true")
}
if c.RootCAs == nil {
t.Fatalf("RootCAs wants non-nil but nil")
}
subjects := c.RootCAs.Subjects()
if len(subjects) != 3 {
t.Errorf("len(subjects) wants 3 but %d", len(subjects))
}
})
t.Run("InvalidCertificate", func(t *testing.T) {
config := ClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/tls/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/tls/ca2.crt.base64")),
},
CACertFilename: "testdata/Makefile", // invalid cert
}
_, err := factory.tlsConfigFor(config)
if err == nil {
t.Fatalf("NewConfig wants non-nil but nil")
}
t.Logf("expected error: %+v", err)
})
}
func readFile(t *testing.T, filename string) []byte {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return b
}

View File

@@ -1,146 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidc (interfaces: FactoryInterface,Interface,DecoderInterface)
// Package mock_oidc is a generated GoMock package.
package mock_oidc
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidc "github.com/int128/kubelogin/pkg/adaptors/oidc"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New(arg0 context.Context, arg1 oidc.ClientConfig) (oidc.Interface, error) {
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(oidc.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New), arg0, arg1)
}
// 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
}
// AuthenticateByCode mocks base method
func (m *MockInterface) AuthenticateByCode(arg0 context.Context, arg1 []int, arg2 chan<- string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByCode indicates an expected call of AuthenticateByCode
func (mr *MockInterfaceMockRecorder) AuthenticateByCode(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockInterface)(nil).AuthenticateByCode), arg0, arg1, arg2)
}
// AuthenticateByPassword mocks base method
func (m *MockInterface) AuthenticateByPassword(arg0 context.Context, arg1, arg2 string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
func (mr *MockInterfaceMockRecorder) AuthenticateByPassword(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockInterface)(nil).AuthenticateByPassword), arg0, arg1, arg2)
}
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}
// MockDecoderInterface is a mock of DecoderInterface interface
type MockDecoderInterface struct {
ctrl *gomock.Controller
recorder *MockDecoderInterfaceMockRecorder
}
// MockDecoderInterfaceMockRecorder is the mock recorder for MockDecoderInterface
type MockDecoderInterfaceMockRecorder struct {
mock *MockDecoderInterface
}
// NewMockDecoderInterface creates a new mock instance
func NewMockDecoderInterface(ctrl *gomock.Controller) *MockDecoderInterface {
mock := &MockDecoderInterface{ctrl: ctrl}
mock.recorder = &MockDecoderInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDecoderInterface) EXPECT() *MockDecoderInterfaceMockRecorder {
return m.recorder
}
// DecodeIDToken mocks base method
func (m *MockDecoderInterface) DecodeIDToken(arg0 string) (*oidc.DecodedIDToken, error) {
ret := m.ctrl.Call(m, "DecodeIDToken", arg0)
ret0, _ := ret[0].(*oidc.DecodedIDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DecodeIDToken indicates an expected call of DecodeIDToken
func (mr *MockDecoderInterfaceMockRecorder) DecodeIDToken(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeIDToken", reflect.TypeOf((*MockDecoderInterface)(nil).DecodeIDToken), arg0)
}

View File

@@ -1,15 +0,0 @@
package oidc
import (
"github.com/google/wire"
)
//go:generate mockgen -destination mock_oidc/mock_oidc.go github.com/int128/kubelogin/pkg/adaptors/oidc FactoryInterface,Interface,DecoderInterface
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
wire.Struct(new(Decoder)),
wire.Bind(new(DecoderInterface), new(*Decoder)),
)

View File

@@ -0,0 +1,63 @@
// Package oidcclient provides a client of OpenID Connect.
package oidcclient
import (
"context"
"crypto/tls"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"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)
// Config represents a configuration of OpenID Connect client.
type Config struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func New(ctx context.Context, config Config) (Interface, error) {
var tlsConfig tls.Config
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
config.CertPool.SetRootCAs(&tlsConfig)
baseTransport := &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: config.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
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 &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
},
logger: config.Logger,
}, nil
}

View File

@@ -0,0 +1,109 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidcclient (interfaces: Interface)
// Package mock_oidcclient is a generated GoMock package.
package mock_oidcclient
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidcclient "github.com/int128/kubelogin/pkg/adaptors/oidcclient"
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
}
// 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)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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
func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthCodeURL", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL
func (mr *MockInterfaceMockRecorder) GetAuthCodeURL(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthCodeURL", reflect.TypeOf((*MockInterface)(nil).GetAuthCodeURL), arg0)
}
// GetTokenByAuthCode mocks base method
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode
func (mr *MockInterfaceMockRecorder) GetTokenByAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByAuthCode", reflect.TypeOf((*MockInterface)(nil).GetTokenByAuthCode), arg0, arg1, arg2)
}
// GetTokenByROPC mocks base method
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByROPC indicates an expected call of GetTokenByROPC
func (mr *MockInterfaceMockRecorder) GetTokenByROPC(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByROPC", reflect.TypeOf((*MockInterface)(nil).GetTokenByROPC), arg0, arg1, arg2)
}
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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)
}

View File

@@ -0,0 +1,198 @@
package oidcclient
import (
"context"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
oidcModel "github.com/int128/kubelogin/pkg/domain/oidc"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient 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)
}
type AuthCodeURLInput struct {
State string
Nonce string
CodeChallenge string
CodeChallengeMethod string
RedirectURI string
}
type ExchangeAuthCodeInput struct {
Code string
CodeVerifier string
Nonce string
RedirectURI string
}
type GetTokenByAuthCodeInput struct {
BindAddress []string
Nonce string
CodeChallenge string
CodeChallengeMethod string
CodeVerifier string
}
// TokenSet represents an output DTO of
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenClaims oidcModel.Claims
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
}
func (c *client) wrapContext(ctx context.Context) context.Context {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
return ctx
}
// GetTokenByAuthCode performs the authorization code flow.
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),
},
LocalServerBindAddress: in.BindAddress,
LocalServerReadyChan: localServerReadyChan,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
// GetAuthCodeURL returns the URL of authentication request for the authorization code flow.
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),
)
}
// ExchangeAuthCode exchanges the authorization code and token.
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
cfg := c.oauth2Config
cfg.RedirectURL = in.RedirectURI
token, err := cfg.Exchange(ctx, in.Code, oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier))
if err != nil {
return nil, xerrors.Errorf("could not exchange the authorization code: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
// 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 c.verifyToken(ctx, token, "")
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: refreshToken,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
return c.verifyToken(ctx, token, "")
}
// 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)
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)
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)
}
claims, err := dumpClaims(idToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idTokenString,
IDTokenClaims: claims,
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

@@ -34,20 +34,23 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
}
// FindByKey mocks base method
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.TokenCache, error) {
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.Value, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*tokencache.TokenCache)
ret0, _ := ret[0].(*tokencache.Value)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByKey indicates an expected call of FindByKey
func (mr *MockInterfaceMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockInterface)(nil).FindByKey), arg0, arg1)
}
// Save mocks base method
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.TokenCache) error {
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.Value) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
@@ -55,5 +58,6 @@ func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.T
// Save indicates an expected call of Save
func (mr *MockInterfaceMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInterface)(nil).Save), arg0, arg1, arg2)
}

View File

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

View File

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

View File

@@ -5,50 +5,51 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() cmd.Interface {
wire.Build(
NewCmdForHeadless,
// dependencies for production
logger.Set,
wire.Value(authentication.DefaultLocalServerReadyFunc),
credentialPluginAdaptor.Set,
)
return nil
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(logger.Interface, authentication.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
wire.Build(
// use-cases
auth.Set,
wire.Value(auth.DefaultLocalServerReadyFunc),
authentication.Set,
standalone.Set,
credentialPluginUseCase.Set,
setup.Set,
// adaptors
cmd.Set,
env.Set,
kubeconfig.Set,
tokencache.Set,
credentialPluginAdaptor.Set,
oidc.Set,
logger.Set,
)
return nil
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(logger.Interface, auth.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
wire.Build(
auth.Set,
standalone.Set,
credentialPluginUseCase.Set,
cmd.Set,
env.Set,
kubeconfig.Set,
tokencache.Set,
oidc.Set,
oidcclient.Set,
jwtdecoder.Set,
certpool.Set,
)
return nil
}

View File

@@ -6,15 +6,18 @@
package di
import (
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
@@ -22,70 +25,50 @@ import (
func NewCmd() cmd.Interface {
loggerInterface := logger.New()
factory := &oidc.Factory{
Logger: loggerInterface,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
localServerReadyFunc := _wireLocalServerReadyFuncValue
authentication := &auth.Authentication{
OIDCFactory: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
standaloneStandalone := &standalone.Standalone{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
interaction := &credentialplugin.Interaction{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
TokenCacheRepository: repository,
Interaction: interaction,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
GetToken: getToken,
Logger: loggerInterface,
}
cmdCmd := &cmd.Cmd{
Root: root,
GetToken: cmdGetToken,
Logger: loggerInterface,
}
return cmdCmd
cmdInterface := NewCmdForHeadless(loggerInterface, localServerReadyFunc, interaction)
return cmdInterface
}
var (
_wireLocalServerReadyFuncValue = auth.DefaultLocalServerReadyFunc
_wireLocalServerReadyFuncValue = authentication.DefaultLocalServerReadyFunc
)
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc auth.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
factory := &oidc.Factory{
Logger: loggerInterface,
}
decoder := &oidc.Decoder{}
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc authentication.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
newFunc := _wireNewFuncValue
decoder := &jwtdecoder.Decoder{}
envEnv := &env.Env{}
authentication := &auth.Authentication{
OIDCFactory: factory,
OIDCDecoder: decoder,
authCode := &authentication.AuthCode{
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Env: envEnv,
Logger: loggerInterface,
}
ropc := &authentication.ROPC{
Env: envEnv,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
NewOIDCClient: newFunc,
JWTDecoder: decoder,
Logger: loggerInterface,
Env: envEnv,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
certpoolNewFunc := _wireCertpoolNewFuncValue
standaloneStandalone := &standalone.Standalone{
Authentication: authentication,
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
NewCertPool: certpoolNewFunc,
Logger: loggerInterface,
}
root := &cmd.Root{
@@ -94,8 +77,9 @@ func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc au
}
repository := &tokencache.Repository{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
NewCertPool: certpoolNewFunc,
Interaction: credentialpluginInterface,
Logger: loggerInterface,
}
@@ -103,10 +87,24 @@ func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc au
GetToken: getToken,
Logger: loggerInterface,
}
setupSetup := &setup.Setup{
Authentication: authenticationAuthentication,
NewCertPool: certpoolNewFunc,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
Setup: setupSetup,
}
cmdCmd := &cmd.Cmd{
Root: root,
GetToken: cmdGetToken,
Setup: cmdSetup,
Logger: loggerInterface,
}
return cmdCmd
}
var (
_wireNewFuncValue = oidcclient.NewFunc(oidcclient.New)
_wireCertpoolNewFuncValue = certpool.NewFunc(certpool.New)
)

65
pkg/domain/oidc/oidc.go Normal file
View File

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

View File

@@ -0,0 +1,25 @@
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)
}
}

20
pkg/domain/oidc/token.go Normal file
View File

@@ -0,0 +1,20 @@
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())
}

View File

@@ -0,0 +1,36 @@
package oidc_test
import (
"testing"
"time"
"github.com/int128/kubelogin/pkg/domain/oidc"
)
type timeProvider time.Time
func (tp timeProvider) Now() time.Time {
return time.Time(tp)
}
func TestClaims_IsExpired(t *testing.T) {
claims := oidc.Claims{
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
}
t.Run("Expired", func(t *testing.T) {
tp := timeProvider(time.Date(2019, 1, 2, 4, 0, 0, 0, time.UTC))
got := claims.IsExpired(tp)
if got != true {
t.Errorf("IsExpired() wants true but false")
}
})
t.Run("NotExpired", func(t *testing.T) {
tp := timeProvider(time.Date(2019, 1, 2, 0, 0, 0, 0, time.UTC))
got := claims.IsExpired(tp)
if got != false {
t.Errorf("IsExpired() wants false but true")
}
})
}

View File

@@ -1,195 +0,0 @@
package auth
import (
"context"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_auth/mock_auth.go github.com/int128/kubelogin/pkg/usecases/auth Interface
// Set provides the use-case of Authentication.
var Set = wire.NewSet(
wire.Struct(new(Authentication), "*"),
wire.Bind(new(Interface), new(*Authentication)),
)
// LocalServerReadyFunc provides an extension point for e2e tests.
type LocalServerReadyFunc func(url string)
// DefaultLocalServerReadyFunc is the default noop function.
var DefaultLocalServerReadyFunc = LocalServerReadyFunc(nil)
type Interface interface {
Do(ctx context.Context, in Input) (*Output, error)
}
// Input represents an input DTO of the Authentication use-case.
type Input struct {
OIDCConfig kubeconfig.OIDCConfig
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
}
// Output represents an output DTO of the Authentication use-case.
type Output struct {
AlreadyHasValidIDToken bool
IDTokenExpiry time.Time
IDTokenClaims map[string]string
IDToken string
RefreshToken string
}
const passwordPrompt = "Password: "
// Authentication provides the internal use-case of authentication.
//
// If the IDToken is not set, it performs the authentication flow.
// If the IDToken is valid, it does nothing.
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
// If the RefreshToken has expired, it performs the authentication flow.
//
// The authentication flow is determined as:
//
// If the Username is not set, it performs the authorization code flow.
// Otherwise, it performs the resource owner password credentials flow.
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
OIDCFactory oidc.FactoryInterface
OIDCDecoder oidc.DecoderInterface
Env env.Interface
Logger logger.Interface
LocalServerReadyFunc LocalServerReadyFunc // only for e2e tests
}
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if in.OIDCConfig.IDToken != "" {
u.Logger.V(1).Infof("checking expiration of the existing token")
// 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.
token, err := u.OIDCDecoder.DecodeIDToken(in.OIDCConfig.IDToken)
if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
}
if token.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
u.Logger.V(1).Infof("you already have a valid token until %s", token.IDTokenExpiry)
return &Output{
AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken,
IDTokenExpiry: token.IDTokenExpiry,
IDTokenClaims: token.IDTokenClaims,
}, nil
}
u.Logger.V(1).Infof("you have an expired token at %s", token.IDTokenExpiry)
}
u.Logger.V(1).Infof("initializing an OIDCFactory client")
client, err := u.OIDCFactory.New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return nil, xerrors.Errorf("could not create an OIDCFactory client: %w", err)
}
if in.OIDCConfig.RefreshToken != "" {
u.Logger.V(1).Infof("refreshing the token")
out, err := client.Refresh(ctx, in.OIDCConfig.RefreshToken)
if err == nil {
return &Output{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.V(1).Infof("could not refresh the token: %s", err)
}
if in.Username == "" {
return u.doAuthCodeFlow(ctx, in, client)
}
return u.doPasswordCredentialsFlow(ctx, in, client)
}
func (u *Authentication) doAuthCodeFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authentication code flow")
readyChan := make(chan string, 1)
var out Output
var eg errgroup.Group
eg.Go(func() error {
select {
case url, ok := <-readyChan:
if !ok {
return nil
}
u.Logger.Printf("Open %s for authentication", url)
if u.LocalServerReadyFunc != nil {
u.LocalServerReadyFunc(url)
}
if in.SkipOpenBrowser {
return nil
}
if err := u.Env.OpenBrowser(url); err != nil {
u.Logger.V(1).Infof("could not open the browser: %s", err)
}
return nil
case <-ctx.Done():
return nil
}
})
eg.Go(func() error {
defer close(readyChan)
tokenSet, err := client.AuthenticateByCode(ctx, in.ListenPort, readyChan)
if err != nil {
return xerrors.Errorf("error while the authorization code flow: %w", err)
}
out = Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &out, nil
}
func (u *Authentication) doPasswordCredentialsFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
if in.Password == "" {
var err error
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
}
}
tokenSet, err := client.AuthenticateByPassword(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
}
return &Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}, nil
}

View File

@@ -1,403 +0,0 @@
package auth
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidc/mock_oidc"
"golang.org/x/xerrors"
)
func TestAuthentication_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
pastTime := time.Now().Add(-time.Hour) //TODO: inject time service
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("AuthorizationCodeFlow", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
ListenPort: []int{10000},
SkipOpenBrowser: true,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AuthorizationCodeFlow/OpenBrowser", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
ListenPort: []int{10000},
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{Config: in.OIDCConfig}).
Return(mockOIDCClient, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
OpenBrowser("LOCAL_SERVER_URL")
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
Env: mockEnv,
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/UsePassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
Username: "USER",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
Username: "USER",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mock_oidc.NewMockInterface(ctrl), nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := Authentication{
OIDCFactory: mockOIDCFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
}
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("VALID_ID_TOKEN").
Return(&oidc.DecodedIDToken{
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Authentication{
OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl),
OIDCDecoder: mockOIDCDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "VALID_REFRESH_TOKEN",
},
}
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&oidc.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "VALID_REFRESH_TOKEN").
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
ListenPort: []int{10000},
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "EXPIRED_REFRESH_TOKEN",
},
}
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&oidc.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
}

View File

@@ -0,0 +1,79 @@
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/domain/oidc"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
// AuthCode provides the authentication code flow.
type AuthCode struct {
Env env.Interface
Logger logger.Interface
LocalServerReadyFunc LocalServerReadyFunc // only for e2e tests
}
func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authentication code flow")
nonce, err := oidc.NewNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
}
p, err := oidc.NewPKCEParams()
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,
}
readyChan := make(chan string, 1)
defer close(readyChan)
var out Output
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
select {
case url, ok := <-readyChan:
if !ok {
return nil
}
u.Logger.Printf("Open %s for authentication", url)
if u.LocalServerReadyFunc != nil {
u.LocalServerReadyFunc(url)
}
if o.SkipOpenBrowser {
return nil
}
if err := u.Env.OpenBrowser(url); err != nil {
u.Logger.V(1).Infof("could not open the browser: %s", err)
}
return nil
case <-ctx.Done():
return xerrors.Errorf("context cancelled while waiting for the local server: %w", ctx.Err())
}
})
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)
}
out = Output{
IDToken: tokenSet.IDToken,
IDTokenClaims: tokenSet.IDTokenClaims,
RefreshToken: tokenSet.RefreshToken,
}
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &out, nil
}

View File

@@ -0,0 +1,63 @@
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/domain/oidc"
"golang.org/x/xerrors"
)
const authCodeKeyboardPrompt = "Enter code: "
const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
// AuthCodeKeyboard provides the authorization code flow with keyboard interactive.
type AuthCodeKeyboard struct {
Env env.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")
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()
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,
})
u.Logger.Printf("Open %s", authCodeURL)
code, err := u.Env.ReadString(authCodeKeyboardPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read the authorization code: %w", err)
}
tokenSet, err := client.ExchangeAuthCode(ctx, oidcclient.ExchangeAuthCodeInput{
Code: code,
CodeVerifier: p.CodeVerifier,
Nonce: nonce,
RedirectURI: oobRedirectURI,
})
if err != nil {
return nil, xerrors.Errorf("could not get the token: %w", err)
}
return &Output{
IDToken: tokenSet.IDToken,
IDTokenClaims: tokenSet.IDTokenClaims,
RefreshToken: tokenSet.RefreshToken,
}, nil
}

View File

@@ -0,0 +1,69 @@
package authentication
import (
"context"
"testing"
"time"
"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"
)
var nonNil = gomock.Not(gomock.Nil())
func TestAuthCodeKeyboard_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"},
}
timeout := 5 * time.Second
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetAuthCodeURL(nonNil).
Return("https://issuer.example.com/auth")
mockOIDCClient.EXPECT().
ExchangeAuthCode(nonNil, nonNil).
Do(func(_ context.Context, in oidcclient.ExchangeAuthCodeInput) {
if in.Code != "YOUR_AUTH_CODE" {
t.Errorf("Code wants YOUR_AUTH_CODE but was %s", in.Code)
}
}).
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
ReadString(authCodeKeyboardPrompt).
Return("YOUR_AUTH_CODE", nil)
u := AuthCodeKeyboard{
Env: mockEnv,
Logger: mock_logger.New(t),
}
got, err := u.Do(ctx, nil, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}

View File

@@ -0,0 +1,101 @@
package authentication
import (
"context"
"testing"
"time"
"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"
)
func TestAuthCode_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"},
}
timeout := 5 * time.Second
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
u := AuthCode{
Logger: mock_logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("OpenBrowser", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
OpenBrowser("LOCAL_SERVER_URL")
u := AuthCode{
Logger: mock_logger.New(t),
Env: mockEnv,
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}

View File

@@ -0,0 +1,161 @@
package authentication
import (
"context"
"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/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_authentication/mock_authentication.go github.com/int128/kubelogin/pkg/usecases/authentication Interface
// Set provides the use-case of Authentication.
var Set = wire.NewSet(
wire.Struct(new(Authentication), "*"),
wire.Bind(new(Interface), new(*Authentication)),
wire.Struct(new(AuthCode), "*"),
wire.Struct(new(AuthCodeKeyboard), "*"),
wire.Struct(new(ROPC), "*"),
)
// LocalServerReadyFunc provides an extension point for e2e tests.
type LocalServerReadyFunc func(url string)
// DefaultLocalServerReadyFunc is the default noop function.
var DefaultLocalServerReadyFunc = LocalServerReadyFunc(nil)
type Interface interface {
Do(ctx context.Context, in Input) (*Output, error)
}
// Input represents an input DTO of the Authentication use-case.
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
IDToken string // optional
RefreshToken string // optional
GrantOptionSet GrantOptionSet
}
type GrantOptionSet struct {
AuthCodeOption *AuthCodeOption
AuthCodeKeyboardOption *AuthCodeKeyboardOption
ROPCOption *ROPCOption
}
type AuthCodeOption struct {
SkipOpenBrowser bool
BindAddress []string
}
type AuthCodeKeyboardOption struct{}
type ROPCOption struct {
Username string
Password string // If empty, read a password using Env.ReadPassword()
}
// Output represents an output DTO of the Authentication use-case.
type Output struct {
AlreadyHasValidIDToken bool
IDToken string
IDTokenClaims oidc.Claims
RefreshToken string
}
const usernamePrompt = "Username: "
const passwordPrompt = "Password: "
// Authentication provides the internal use-case of authentication.
//
// If the IDToken is not set, it performs the authentication flow.
// If the IDToken is valid, it does nothing.
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
// If the RefreshToken has expired, it performs the authentication flow.
//
// The authentication flow is determined as:
//
// If the Username is not set, it performs the authorization code flow.
// Otherwise, it performs the resource owner password credentials flow.
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
NewOIDCClient oidcclient.NewFunc
JWTDecoder jwtdecoder.Interface
Logger logger.Interface
Env env.Interface
AuthCode *AuthCode
AuthCodeKeyboard *AuthCodeKeyboard
ROPC *ROPC
}
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if in.IDToken != "" {
u.Logger.V(1).Infof("checking expiration of the existing token")
// 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)
if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
}
if !claims.IsExpired(u.Env) {
u.Logger.V(1).Infof("you already have a valid token until %s", claims.Expiry)
return &Output{
AlreadyHasValidIDToken: true,
IDToken: in.IDToken,
RefreshToken: in.RefreshToken,
IDTokenClaims: *claims,
}, nil
}
u.Logger.V(1).Infof("you have an expired token at %s", claims.Expiry)
}
u.Logger.V(1).Infof("initializing an OpenID Connect client")
client, err := u.NewOIDCClient(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)
}
if in.RefreshToken != "" {
u.Logger.V(1).Infof("refreshing the token")
out, err := client.Refresh(ctx, in.RefreshToken)
if err == nil {
return &Output{
IDToken: out.IDToken,
IDTokenClaims: out.IDTokenClaims,
RefreshToken: out.RefreshToken,
}, nil
}
u.Logger.V(1).Infof("could not refresh the token: %s", err)
}
if in.GrantOptionSet.AuthCodeOption != nil {
return u.AuthCode.Do(ctx, in.GrantOptionSet.AuthCodeOption, client)
}
if in.GrantOptionSet.AuthCodeKeyboardOption != nil {
return u.AuthCodeKeyboard.Do(ctx, in.GrantOptionSet.AuthCodeKeyboardOption, client)
}
if in.GrantOptionSet.ROPCOption != nil {
return u.ROPC.Do(ctx, in.GrantOptionSet.ROPCOption, client)
}
return nil, xerrors.Errorf("any authorization grant must be set")
}

View File

@@ -0,0 +1,257 @@
package authentication
import (
"context"
"testing"
"time"
"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"
"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)
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
}
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,
}
got, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
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,
}, nil)
u := Authentication{
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
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,
}
got, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("HasExpiredRefreshToken/AuthCode", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
GrantOptionSet: GrantOptionSet{
AuthCodeOption: &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
},
},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
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().
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Authentication{
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
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,
AuthCode: &AuthCode{
Logger: testingLogger,
},
}
got, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("NoToken/ROPC", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
GrantOptionSet: GrantOptionSet{
ROPCOption: &ROPCOption{
Username: "USER",
Password: "PASS",
},
},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Authentication{
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
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,
ROPC: &ROPC{
Logger: testingLogger,
},
}
got, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}

View File

@@ -1,13 +1,13 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/usecases/auth (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/usecases/authentication (interfaces: Interface)
// Package mock_auth is a generated GoMock package.
package mock_auth
// Package mock_authentication is a generated GoMock package.
package mock_authentication
import (
context "context"
gomock "github.com/golang/mock/gomock"
auth "github.com/int128/kubelogin/pkg/usecases/auth"
authentication "github.com/int128/kubelogin/pkg/usecases/authentication"
reflect "reflect"
)
@@ -35,14 +35,16 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
}
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 auth.Input) (*auth.Output, error) {
func (m *MockInterface) Do(arg0 context.Context, arg1 authentication.Input) (*authentication.Output, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*auth.Output)
ret0, _ := ret[0].(*authentication.Output)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -0,0 +1,43 @@
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"
"golang.org/x/xerrors"
)
// ROPC provides the resource owner password credentials flow.
type ROPC struct {
Env env.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")
if in.Username == "" {
var err error
in.Username, err = u.Env.ReadString(usernamePrompt)
if err != nil {
return nil, xerrors.Errorf("could not get the username: %w", err)
}
}
if in.Password == "" {
var err error
in.Password, err = u.Env.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 &Output{
IDToken: tokenSet.IDToken,
IDTokenClaims: tokenSet.IDTokenClaims,
RefreshToken: tokenSet.RefreshToken,
}, nil
}

View File

@@ -0,0 +1,153 @@
package authentication
import (
"context"
"testing"
"time"
"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"
"golang.org/x/xerrors"
)
func TestROPC_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"},
}
timeout := 5 * time.Second
t.Run("AskUsernameAndPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
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)
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("UsePassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{
Username: "USER",
Password: "PASS",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
u := ROPC{
Logger: mock_logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{
Username: "USER",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{
Username: "USER",
}
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, o, mock_oidcclient.NewMockInterface(ctrl))
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
}

View File

@@ -7,11 +7,11 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
@@ -28,72 +28,87 @@ type Interface interface {
// Input represents an input DTO of the GetToken use-case.
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
TokenCacheDir string
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
TokenCacheDir string
GrantOptionSet authentication.GrantOptionSet
}
type GetToken struct {
Authentication auth.Interface
Authentication authentication.Interface
TokenCacheRepository tokencache.Interface
NewCertPool certpool.NewFunc
Interaction credentialplugin.Interface
Logger logger.Interface
}
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")
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
cacheKey := tokencache.Key{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
out, err := u.getTokenFromCacheOrProvider(ctx, in)
if err != nil {
u.Logger.V(1).Infof("could not find a token cache: %s", err)
cache = &tokencache.TokenCache{}
return xerrors.Errorf("could not get a token from the cache or provider: %w", err)
}
out, err := u.Authentication.Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
IDToken: cache.IDToken,
RefreshToken: cache.RefreshToken,
},
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.V(1).Infof("the ID token has the claim: %s=%v", k, v)
}
if !out.AlreadyHasValidIDToken {
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
cache := tokencache.TokenCache{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
}
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, cacheKey, cache); err != nil {
return xerrors.Errorf("could not write the token cache: %w", err)
}
}
u.Logger.V(1).Infof("writing the token to client-go")
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenExpiry}); err != nil {
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenClaims.Expiry}); err != nil {
return xerrors.Errorf("could not write the token to client-go: %w", err)
}
return nil
}
func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*authentication.Output, error) {
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
tokenCacheKey := tokencache.Key{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
}
tokenCacheValue, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, tokenCacheKey)
if err != nil {
u.Logger.V(1).Infof("could not find a token cache: %s", err)
tokenCacheValue = &tokencache.Value{}
}
certPool := u.NewCertPool()
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
IDToken: tokenCacheValue.IDToken,
RefreshToken: tokenCacheValue.RefreshToken,
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)
}
if out.AlreadyHasValidIDToken {
u.Logger.V(1).Infof("you already have a valid token until %s", out.IDTokenClaims.Expiry)
return out, nil
}
u.Logger.V(1).Infof("you got a valid token until %s", out.IDTokenClaims.Expiry)
newTokenCacheValue := tokencache.Value{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
}
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, tokenCacheKey, newTokenCacheValue); err != nil {
return nil, xerrors.Errorf("could not write the token cache: %w", err)
}
return out, nil
}

View File

@@ -6,72 +6,79 @@ import (
"time"
"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/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"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/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"github.com/int128/kubelogin/pkg/domain/oidc"
"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 := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
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"},
}
t.Run("FullOptions", func(t *testing.T) {
var grantOptionSet authentication.GrantOptionSet
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert")
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}).
Return(&auth.Output{
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
FindByKey("/path/to/token-cache",
tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(nil, xerrors.New("file not found"))
tokenCacheRepository.EXPECT().
Save("/path/to/token-cache",
tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
},
tokencache.TokenCache{
tokencache.Value{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
@@ -79,11 +86,12 @@ func TestGetToken_Do(t *testing.T) {
credentialPluginInteraction.EXPECT().
Write(credentialplugin.Output{
Token: "YOUR_ID_TOKEN",
Expiry: futureTime,
Expiry: dummyTokenClaims.Expiry,
})
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Interaction: credentialPluginInteraction,
Logger: mock_logger.New(t),
}
@@ -102,40 +110,41 @@ func TestGetToken_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
CertPool: mockCertPool,
}).
Return(&auth.Output{
Return(&authentication.Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(&tokencache.TokenCache{
Return(&tokencache.Value{
IDToken: "VALID_ID_TOKEN",
}, nil)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(credentialplugin.Output{
Token: "VALID_ID_TOKEN",
Expiry: futureTime,
Expiry: dummyTokenClaims.Expiry,
})
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Interaction: credentialPluginInteraction,
Logger: mock_logger.New(t),
}
@@ -154,26 +163,28 @@ func TestGetToken_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
}).
Return(nil, xerrors.New("authentication error"))
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(nil, xerrors.New("file not found"))
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Interaction: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
}

View File

@@ -36,6 +36,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
@@ -43,5 +44,6 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) er
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -0,0 +1,27 @@
// Package setup provides the use case of setting up environment.
package setup
import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
var Set = wire.NewSet(
wire.Struct(new(Setup), "*"),
wire.Bind(new(Interface), new(*Setup)),
)
type Interface interface {
DoStage1()
DoStage2(ctx context.Context, in Stage2Input) error
}
type Setup struct {
Authentication authentication.Interface
NewCertPool certpool.NewFunc
Logger logger.Interface
}

View File

@@ -0,0 +1,28 @@
package setup
const stage1 = `This setup shows the instruction of Kubernetes OpenID Connect authentication.
See also https://github.com/int128/kubelogin.
## 1. Set up the OpenID Connect Provider
Open the OpenID Connect Provider and create a client.
For example, Google Identity Platform:
Open https://console.developers.google.com/apis/credentials and create an OAuth client of "Other" type.
ISSUER is https://accounts.google.com
## 2. Verify authentication
Run the following command to proceed.
kubectl oidc-login setup \
--oidc-issuer-url=ISSUER \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET
You can set your CA certificate. See also the options by --help.
`
func (u *Setup) DoStage1() {
u.Logger.Printf(stage1)
}

View File

@@ -0,0 +1,145 @@
package setup
import (
"context"
"strings"
"text/template"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
var stage2Tpl = template.Must(template.New("").Parse(`
## 3. Bind a role
Run the following command:
kubectl apply -f - <<-EOF
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 }}
EOF
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
--oidc-issuer-url={{ .IssuerURL }}
--oidc-client-id={{ .ClientID }}
## 5. Set up the kubeconfig
Add the following user to the kubeconfig:
users:
- name: google
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
{{- range .Args }}
- {{ . }}
{{- end }}
Run kubectl and verify cluster access.
`))
type stage2Vars struct {
IssuerURL string
ClientID string
Args []string
Subject string
}
// Stage2Input represents an input DTO of the stage2.
type Stage2Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
ListenAddressArgs []string // non-nil if set by the command arg
GrantOptionSet authentication.GrantOptionSet
}
func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
u.Logger.Printf(`## 2. Verify authentication`)
certPool := u.NewCertPool()
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return xerrors.Errorf("could not load the certificate: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
}
u.Logger.Printf("You got the following claims in the token:")
for k, v := range out.IDTokenClaims.Pretty {
u.Logger.Printf("\t%s=%s", k, v)
}
v := stage2Vars{
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 {
return xerrors.Errorf("could not render the template: %w", err)
}
u.Logger.Printf(b.String())
return nil
}
func makeCredentialPluginArgs(in Stage2Input) []string {
var args []string
args = append(args, "--oidc-issuer-url="+in.IssuerURL)
args = append(args, "--oidc-client-id="+in.ClientID)
if in.ClientSecret != "" {
args = append(args, "--oidc-client-secret="+in.ClientSecret)
}
for _, extraScope := range in.ExtraScopes {
args = append(args, "--oidc-extra-scope="+extraScope)
}
if in.CACertFilename != "" {
args = append(args, "--certificate-authority="+in.CACertFilename)
}
if in.SkipTLSVerify {
args = append(args, "--insecure-skip-tls-verify")
}
if in.GrantOptionSet.AuthCodeOption != nil {
if in.GrantOptionSet.AuthCodeOption.SkipOpenBrowser {
args = append(args, "--skip-open-browser")
}
}
args = append(args, in.ListenAddressArgs...)
if in.GrantOptionSet.ROPCOption != nil {
if in.GrantOptionSet.ROPCOption.Username != "" {
args = append(args, "--username="+in.GrantOptionSet.ROPCOption.Username)
}
}
return args
}

View File

@@ -0,0 +1,65 @@
package setup
import (
"context"
"testing"
"time"
"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/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()
ctx := context.Background()
in := Stage2Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert")
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}).
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Setup{
Authentication: mockAuthentication,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
}
if err := u.DoStage2(ctx, in); err != nil {
t.Errorf("DoStage2 returned error: %+v", err)
}
}

View File

@@ -36,6 +36,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
@@ -43,5 +44,6 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -2,11 +2,14 @@ package standalone
import (
"context"
"strings"
"text/template"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
@@ -27,20 +30,14 @@ type Input struct {
KubeconfigFilename string // Default to the environment variable or global config as kubectl
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
KubeconfigUser kubeconfig.UserName // Default to the user of the context
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
GrantOptionSet authentication.GrantOptionSet
}
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials CONTEXT_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
const oidcConfigErrorMessage = `You need to set up the kubeconfig for OpenID Connect authentication.
See https://github.com/int128/kubelogin for more.
`
// Standalone provides the use case of explicit login.
//
@@ -49,8 +46,9 @@ const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubec
// Otherwise, update the kubeconfig.
//
type Standalone struct {
Authentication auth.Interface
Authentication authentication.Interface
Kubeconfig kubeconfig.Interface
NewCertPool certpool.NewFunc
Logger logger.Interface
}
@@ -62,35 +60,119 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
u.Logger.Printf(oidcConfigErrorMessage)
return xerrors.Errorf("could not find the current authentication provider: %w", err)
}
if err := u.showDeprecation(in, authProvider); err != nil {
return xerrors.Errorf("could not show deprecation message: %w", err)
}
u.Logger.V(1).Infof("using the authentication provider of the user %s", authProvider.UserName)
u.Logger.V(1).Infof("a token will be written to %s", authProvider.LocationOfOrigin)
out, err := u.Authentication.Do(ctx, auth.Input{
OIDCConfig: authProvider.OIDCConfig,
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
certPool := u.NewCertPool()
if authProvider.IDPCertificateAuthority != "" {
if err := certPool.AddFile(authProvider.IDPCertificateAuthority); err != nil {
return xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if authProvider.IDPCertificateAuthorityData != "" {
if err := certPool.AddBase64Encoded(authProvider.IDPCertificateAuthorityData); err != nil {
return xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return xerrors.Errorf("could not load the certificate: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: authProvider.IDPIssuerURL,
ClientID: authProvider.ClientID,
ClientSecret: authProvider.ClientSecret,
ExtraScopes: authProvider.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
IDToken: authProvider.IDToken,
RefreshToken: authProvider.RefreshToken,
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
for k, v := range out.IDTokenClaims.Pretty {
u.Logger.V(1).Infof("the ID token has the claim: %s=%v", k, v)
}
if out.AlreadyHasValidIDToken {
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
u.Logger.Printf("You already have a valid token until %s", out.IDTokenClaims.Expiry)
return nil
}
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
authProvider.OIDCConfig.IDToken = out.IDToken
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
u.Logger.Printf("You got a valid token until %s", out.IDTokenClaims.Expiry)
authProvider.IDToken = out.IDToken
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 nil
}
var deprecationTpl = template.Must(template.New("").Parse(
`IMPORTANT NOTICE:
The credential plugin mode is available since v1.14.0.
Kubectl will automatically run kubelogin and you do not need to run kubelogin explicitly.
You can switch to the credential plugin mode by setting the following user to
{{ .Kubeconfig }}.
---
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
{{- range .Args }}
- {{ . }}
{{- end }}
---
See https://github.com/int128/kubelogin for more.
`))
type deprecationVars struct {
Kubeconfig string
Args []string
}
func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error {
var args []string
args = append(args, "--oidc-issuer-url="+p.IDPIssuerURL)
args = append(args, "--oidc-client-id="+p.ClientID)
if p.ClientSecret != "" {
args = append(args, "--oidc-client-secret="+p.ClientSecret)
}
for _, extraScope := range p.ExtraScopes {
args = append(args, "--oidc-extra-scope="+extraScope)
}
if p.IDPCertificateAuthority != "" {
args = append(args, "--certificate-authority="+p.IDPCertificateAuthority)
}
if in.CACertFilename != "" {
args = append(args, "--certificate-authority="+in.CACertFilename)
}
if in.GrantOptionSet.ROPCOption != nil {
if in.GrantOptionSet.ROPCOption.Username != "" {
args = append(args, "--username="+in.GrantOptionSet.ROPCOption.Username)
}
}
v := deprecationVars{
Kubeconfig: p.LocationOfOrigin,
Args: args,
}
var b strings.Builder
if err := deprecationTpl.Execute(&b, &v); err != nil {
return xerrors.Errorf("could not render the template: %w", err)
}
u.Logger.Printf("%s", b.String())
return nil
}

View File

@@ -6,19 +6,26 @@ import (
"time"
"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/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig/mock_kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"github.com/int128/kubelogin/pkg/domain/oidc"
"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 := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
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"},
}
t.Run("FullOptions", func(t *testing.T) {
var grantOptionSet authentication.GrantOptionSet
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
@@ -26,58 +33,61 @@ func TestStandalone_Do(t *testing.T) {
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
CACertFilename: "/path/to/cert1",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert2",
IDPCertificateAuthorityData: "BASE64ENCODED",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert1")
mockCertPool.EXPECT().
AddFile("/path/to/cert2")
mockCertPool.EXPECT().
AddBase64Encoded("BASE64ENCODED")
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert2",
IDPCertificateAuthorityData: "BASE64ENCODED",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: currentAuthProvider.OIDCConfig,
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}).
Return(&auth.Output{
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
@@ -93,28 +103,34 @@ func TestStandalone_Do(t *testing.T) {
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&auth.Output{
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
CertPool: mockCertPool,
}).
Return(&authentication.Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
@@ -131,7 +147,7 @@ func TestStandalone_Do(t *testing.T) {
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, xerrors.New("no oidc config"))
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
@@ -150,23 +166,28 @@ func TestStandalone_Do(t *testing.T) {
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
}).
Return(nil, xerrors.New("authentication error"))
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
@@ -182,12 +203,11 @@ func TestStandalone_Do(t *testing.T) {
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
@@ -196,27 +216,30 @@ func TestStandalone_Do(t *testing.T) {
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}).
Return(xerrors.New("I/O error"))
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&auth.Output{
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
}).
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {