Compare commits

..

120 Commits
1.3 ... v1.12.0

Author SHA1 Message Date
Hidetake Iwata
97b0bdd53c Fix build error of windows_amd64 binary (#97) 2019-06-06 09:30:41 +09:00
Hidetake Iwata
ebe5feaed2 Transparently login feature (#95)
* Introduce spf13/cobra

* Add login transparently feature
2019-06-05 22:01:53 +09:00
flarno11
4427bc7985 fixed typo in kubectl plugin call (#94) 2019-06-04 08:15:00 +09:00
Hidetake Iwata
8643d695fe Refactor (#92)
* Refactor test

* Refactor: rename and add comments for ShowLocalServerURL
2019-06-03 20:01:51 +09:00
Hidetake Iwata
f0cff5a54b Add password prompt (#91) 2019-06-01 19:57:11 +09:00
Hidetake Iwata
8237928af3 Refactor (#90)
* Refactor: move HTTP adaptor to internal

* Refactor: extract models/kubeconfig package

* Refactor: rename to Kubeconfig from KubeConfig

* Refactor: simplify use-case
2019-05-29 15:31:52 +09:00
Hidetake Iwata
01b270755b Refactor: use a dynamic port in the integration tests (#89)
* Refactor: use dynamic port for auth server in integration tests

* Refactor: use dynamic port for local server in integration tests
2019-05-28 09:05:15 +09:00
Hidetake Iwata
e97b4de40b Refactor packages structure (#88)
* Refactor: move usecases implementation to dedicated packages

* Refactor: move adaptors implementation to dedicated packages

* Refactor: rename and move packages

* Refactor: split to OIDC and OIDCClient
2019-05-24 11:41:09 +09:00
Hidetake Iwata
5063550468 Add resource owner password credentials grant support (#87) 2019-05-23 09:37:47 +09:00
Hidetake Iwata
d5989ca256 Move to google/wire (#86) 2019-05-20 10:55:02 +09:00
Hidetake Iwata
c508a1b717 Refactor docs (#84) 2019-05-19 16:07:57 +09:00
Hidetake Iwata
e133ea8541 Add codecov (#83) 2019-05-18 09:45:37 +09:00
Hidetake Iwata
3f2e84a1ea Update README.md 2019-05-17 09:24:08 +09:00
Hidetake Iwata
7011f03094 Release v1.11.0 2019-05-16 21:59:50 +09:00
Hidetake Iwata
6aef98cef7 Bump to int128/oauth2cli:v1.4.0 (#82) 2019-05-16 21:55:56 +09:00
Hidetake Iwata
93bb1d39b9 Add fallback ports for local server (#79) 2019-05-16 20:38:45 +09:00
Hidetake Iwata
c8116e2eae Update keycloak.md 2019-05-14 15:42:05 +09:00
Hidetake Iwata
f2de8dd987 Refactor log messages and etc. (#77)
* Refactor log messages

* Refactor token verification
2019-05-14 13:49:43 +09:00
Hidetake Iwata
915fb35bc8 Raise error on invalid certificate (as kubectl) (#73) 2019-05-14 11:32:11 +09:00
Hidetake Iwata
51ccd70af3 go mod tidy 2019-05-14 11:26:06 +09:00
dependabot[bot]
c6df597fb0 Bump github.com/golang/mock from 1.3.0 to 1.3.1 (#76)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/golang/mock/releases)
- [Commits](https://github.com/golang/mock/compare/v1.3.0...1.3.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-14 11:24:03 +09:00
Hidetake Iwata
ee78f6f735 Make integration tests in parallel (#75) 2019-05-13 21:25:20 +09:00
Hidetake Iwata
6e484a2b89 Add golangci-lint (#74)
* Add golangci-lint

* Fix lint errors
2019-05-13 10:19:25 +09:00
Hidetake Iwata
8050db7e05 go mod tidy 2019-05-13 09:26:28 +09:00
dependabot[bot]
cd54ca0df0 Bump github.com/golang/mock from 1.2.0 to 1.3.0 (#71)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/golang/mock/releases)
- [Commits](https://github.com/golang/mock/compare/v1.2.0...v1.3.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-04 17:58:42 +09:00
Hidetake Iwata
45f83b0b0e Add multiple kubeconfig support (#70) 2019-04-29 14:34:56 +09:00
Hidetake Iwata
51b7ca1600 Fix templates for the latest goxzst 2019-04-22 09:42:01 +09:00
Hidetake Iwata
83f85a9b53 Dump all claims of ID token to debug log (#68)
* Dump all claims of ID token to debug log

* Add dump when a user already has a token
2019-04-19 10:29:56 +09:00
Hidetake Iwata
d82c8a2dd1 Fix error of go get and remove golint (#69)
* Remove golint

* Fix error of go get github.com/int128/ghcp
2019-04-19 10:26:03 +09:00
Hidetake Iwata
072bee6992 Add --certificate-authority option (#67) 2019-04-18 10:50:38 +09:00
Hidetake Iwata
5c07850a68 Add --user option (#66) 2019-04-18 10:38:23 +09:00
Hidetake Iwata
5c8c80f055 Add --context option (#65) 2019-04-18 10:12:48 +09:00
Hidetake Iwata
bc7bfabfb2 Move to pflags (#62) 2019-04-17 16:58:17 +09:00
Hidetake Iwata
73112546de Update README.md 2019-04-17 16:56:44 +09:00
Hidetake Iwata
356f0d519d Add debug log feature (#58) 2019-04-16 09:27:38 +09:00
Hidetake Iwata
e84d29bc6b Fix flaky test (#61) 2019-04-16 09:13:39 +09:00
Hidetake Iwata
ae80ebf148 Refactor: use dedicated Logger on authentication callback (#59) 2019-04-14 14:49:12 +09:00
Nathaniel Irons
5942c82b5f Change expected binary name to kubelogin (#57) 2019-04-12 10:16:18 +09:00
Hidetake Iwata
a5f9c698ea Bump Go to 1.12.3 (#56) 2019-04-11 12:52:41 +09:00
Hidetake Iwata
40ef2c25b8 Update README.md 2019-04-11 10:48:19 +09:00
Hidetake Iwata
2422c46271 Refactor integration tests (#54)
* Refactor integration tests

* Refactor: move keys

* Refactor authserver

* Refactor: do not generate key on test runtime

* Refactor: package keys in integration tests

* Refactor: use context on sending browser request

* Refactor: use testing.T logger on integration tests
2019-04-11 10:43:05 +09:00
Hidetake Iwata
6ca0ee8013 Skip login if kubeconfig has valid ID token (#52)
* Skip login if kubeconfig has valid ID token

* Add integration test for skip login feature
2019-04-10 10:20:13 +09:00
Hidetake Iwata
9ac252667a Update README.md 2019-04-09 13:34:47 +09:00
Hidetake Iwata
000711f52e Add support of HTTP_PROXY, HTTPS_PROXY and NO_PROXY (#51) 2019-04-09 13:17:12 +09:00
Hidetake Iwata
e465c4852b Introduce dig container (#50) 2019-04-09 12:37:44 +09:00
Hidetake Iwata
003badb0bc Fix nil pointer error (#49) 2019-04-09 10:19:25 +09:00
Hidetake Iwata
0873a193a5 Refactor: extract adaptors.Logger 2019-04-08 16:07:26 +09:00
Hidetake Iwata
5e80b1858e Refactor: logs 2019-04-08 16:07:26 +09:00
Hidetake Iwata
c816281657 Refactor: add test of usecases.Login 2019-04-08 13:35:01 +09:00
Hidetake Iwata
0db49860f9 Refactor: extract adaptors.HTTPClientConfig 2019-04-08 13:35:01 +09:00
Hidetake Iwata
d70c9db036 Refactor: extract adaptors.HTTP 2019-04-08 13:35:01 +09:00
Hidetake Iwata
675b5e5fff Refactor: extract adaptors.OIDC 2019-04-08 13:35:01 +09:00
Hidetake Iwata
e3bfc321a2 Refactor: extract adaptors.KubeConfig 2019-04-08 13:35:01 +09:00
Hidetake Iwata
b34d0fb32f Refactor: add di package 2019-04-08 13:35:01 +09:00
Hidetake Iwata
4c61a71ed4 Add test of adaptors.Cmd (#45) 2019-04-07 19:33:53 +09:00
Hidetake Iwata
8a02ed0fb0 Split cli package into adaptors and use-cases (#44) 2019-04-05 14:52:15 +09:00
Hidetake Iwata
3485c5408e Replace with errors.Errorf() and errors.Wrapf() (#43) 2019-04-05 12:12:00 +09:00
Hidetake Iwata
fb99977e98 Update README.md 2019-04-05 10:53:32 +09:00
Hidetake Iwata
39b441a7c2 Run go get at once 2019-04-05 10:40:23 +09:00
Hidetake Iwata
cde5becf67 Fix release in Makefile 2019-04-05 10:37:02 +09:00
Hidetake Iwata
460b14a159 Use CI context 2019-04-05 10:36:12 +09:00
Hidetake Iwata
8436fe3494 Merge pull request #42 from int128/krew
Add yaml for Krew installation
2019-04-05 10:28:23 +09:00
Hidetake Iwata
9d2319ee2f Do not set change log on release 2019-04-05 10:17:42 +09:00
Hidetake Iwata
75277378fc Add yaml for krew installation 2019-04-05 10:17:42 +09:00
Hidetake Iwata
89a1046ce3 Use goxzst for release 2019-04-05 09:58:34 +09:00
Baykonur
15d40413e4 HTTPS_PROXY support for kubelogin (#41)
adding HTTPS_PROXY support to kubelogin
2019-04-04 15:45:47 +09:00
Hidetake Iwata
8525ba5142 Merge pull request #40 from int128/refactor
Rename plugin to kubectl oidc-login
2019-03-29 16:59:55 +09:00
Hidetake Iwata
dbddd6a07f Rename plugin to kubectl oidc-login
See the naming guide:
https://github.com/GoogleContainerTools/krew/blob/master/docs/NAMING_GUIDE.md
2019-03-29 16:43:53 +09:00
Hidetake Iwata
839877b45e Merge pull request #39 from int128/refactor
Move to gox/ghr/ghcp to fix version of k8s modules
2019-03-29 16:29:48 +09:00
Hidetake Iwata
99ed86e22e Move to gox/ghr/ghcp to fix version of k8s modules 2019-03-29 16:27:26 +09:00
Hidetake Iwata
a78b746c29 Exclude /.idea 2019-02-27 10:54:07 +09:00
Hidetake Iwata
187bbc203c Merge pull request #32 from int128/dependabot/go_modules/github.com/mitchellh/go-homedir-1.1.0
Bump github.com/mitchellh/go-homedir from 1.0.0 to 1.1.0
2019-02-27 10:46:11 +09:00
dependabot[bot]
d4b5e511bb Bump github.com/mitchellh/go-homedir from 1.0.0 to 1.1.0
Bumps [github.com/mitchellh/go-homedir](https://github.com/mitchellh/go-homedir) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/mitchellh/go-homedir/releases)
- [Commits](https://github.com/mitchellh/go-homedir/compare/v1.0.0...v1.1.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-01-28 20:20:06 +00:00
Hidetake Iwata
33241b8721 Update README.md 2018-11-20 16:46:02 +09:00
Hidetake Iwata
b72cb63826 Fix up #25 2018-11-09 13:26:35 +09:00
Hidetake Iwata
63fda1db0f Merge pull request #25 from int128/kubectl-plugin
Add kubectl plugin support
2018-11-09 11:39:52 +09:00
Hidetake Iwata
da95fe470f Add kubectl plugin support 2018-11-09 11:37:38 +09:00
Hidetake Iwata
4b08a49a51 Add workaround for Go Modules issue 2018-10-31 14:21:33 +09:00
Hidetake Iwata
9c74f3748b Merge pull request #23 from int128/oauth2cli
Import github.com/int128/oauth2cli
2018-10-31 14:16:25 +09:00
Hidetake Iwata
17e03f2abc Import github.com/int128/oauth2cli 2018-10-31 14:13:09 +09:00
Hidetake Iwata
ebef81f9d7 Merge pull request #22 from int128/ignore-cert-errors
Skip invalid CA certs instead of raising error
2018-10-30 09:56:22 +09:00
Hidetake Iwata
8d0d82fb71 Skip invalid CA certs instead of raising error 2018-10-30 09:55:12 +09:00
Hidetake Iwata
b600e54a12 Refactor e2e test 2018-10-29 14:04:10 +09:00
Hidetake Iwata
75317f88a1 Merge pull request #21 from int128/go111
Move to Go Modules
2018-10-29 13:50:01 +09:00
Hidetake Iwata
5a794e8ceb Move to Go Modules 2018-10-29 13:45:32 +09:00
Hidetake Iwata
1fe1ec4c20 Update README.md 2018-10-16 09:47:50 +09:00
Hidetake Iwata
7676ffbfab Fix golint 2018-10-16 09:25:38 +09:00
Hidetake Iwata
7e1e6a096b Refactor docs 2018-10-15 15:52:52 +09:00
Hidetake Iwata
4d3d1c3b78 Add explanation about extra scopes 2018-10-04 16:05:36 +09:00
Hidetake Iwata
1ebdfc0e4f Use "Application Type: Other" on Google IdP 2018-10-03 15:33:56 +09:00
Hidetake Iwata
9c67c52b34 Fix test for #17 2018-09-14 15:06:05 +09:00
Hidetake Iwata
550396e1dd Merge pull request #17 from stang/allow-to-change-listening-port
Allow to change listening port
2018-09-14 11:20:08 +09:00
Stephane Tang
34f0578b59 Allow to change listening port
It's using port 8000 by default, which is identical as the original behavior.

Signed-off-by: Stephane Tang <hi@stang.sh>
2018-09-14 00:03:18 +01:00
Hidetake Iwata
604d118b68 Update README.md 2018-09-07 10:56:26 +09:00
Hidetake Iwata
91959e8a56 Update README.md 2018-09-07 10:56:21 +09:00
Hidetake Iwata
9b325a66a9 Show version on help 2018-09-05 12:54:00 +09:00
Hidetake Iwata
8b6257d60b Introduce goreleaser 2018-09-05 12:54:00 +09:00
Hidetake Iwata
d469df4978 Fix cli.Parse does not respect argument 2018-09-05 11:34:40 +09:00
Hidetake Iwata
3ae68df848 Merge pull request #14 from int128/ux
Improve error messages
2018-09-04 06:52:24 +09:00
Hidetake Iwata
e8805f7a94 Improve error messages 2018-09-04 06:49:49 +09:00
Hidetake Iwata
717da9d442 Merge pull request #15 from int128/refresh-token
Fix refresh token is not set with Google IdP
2018-09-04 06:41:58 +09:00
Hidetake Iwata
de176cfbaa Fix refresh token is not set with Google IdP 2018-09-03 14:42:00 +09:00
Hidetake Iwata
9bf8a89577 Merge pull request #13 from int128/extra-scopes
Add extra-scopes support
2018-09-02 14:20:35 +09:00
Hidetake Iwata
a91c020f46 Update README.md 2018-09-02 14:19:23 +09:00
Hidetake Iwata
d4fb49613d Add extra-scopes support 2018-08-31 21:02:34 +09:00
Hidetake Iwata
64b1d52208 Fix test says message if CLI returns error 2018-08-31 15:19:58 +09:00
Hidetake Iwata
a298058e3f Refactor test 2018-08-31 14:59:50 +09:00
Hidetake Iwata
309e73d8c0 Merge pull request #12 from int128/browser-delay
Add delay before opening browser
2018-08-31 09:30:05 +09:00
Hidetake Iwata
857d5dad88 Add delay before opening browser 2018-08-30 21:28:17 +09:00
Hidetake Iwata
455c920b65 Refactor e2e test 2018-08-30 14:47:03 +09:00
Hidetake Iwata
afad46817a Update README.md 2018-08-28 12:28:59 +09:00
Hidetake Iwata
4f506b9f62 Update README.md 2018-08-28 09:53:16 +09:00
Hidetake Iwata
72bc19bc10 Rename 2018-08-28 09:30:11 +09:00
Hidetake Iwata
69bcb16e26 Update README.md 2018-08-27 22:27:11 +09:00
Hidetake Iwata
978a45bcf1 Refactor 2018-08-27 14:49:25 +09:00
Hidetake Iwata
62b9a2158d Refactor 2018-08-26 12:33:35 +09:00
Hidetake Iwata
974fc5c526 Merge pull request #10 from int128/oidc-browser
Open browser automatically on authentication
2018-08-26 11:07:07 +09:00
Hidetake Iwata
2c7d958efd Close browser automatically 2018-08-25 21:53:41 +09:00
Hidetake Iwata
16b15cd21b Open browser automatically on authentication 2018-08-25 21:53:41 +09:00
Hidetake Iwata
3213572180 Polish 2018-08-24 15:22:09 +09:00
66 changed files with 4366 additions and 929 deletions

View File

@@ -2,37 +2,38 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/int128/kubelogin
- image: circleci/golang:1.12.3
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -v
- run: make -C integration-test/testdata
- run: go test -v ./...
release:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/int128/kubelogin
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/mitchellh/gox
- 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.3.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: gox --osarch 'darwin/amd64 linux/amd64 windows/amd64 windows/386' -output 'dist/{{.Dir}}_{{.OS}}_{{.Arch}}'
- run: ghr -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" "$CIRCLE_TAG" dist
- checkout
# workaround for https://github.com/golang/go/issues/27925
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make run
- run: |
if [ "$CIRCLE_TAG" ]; then
make release
fi
workflows:
version: 2
build:
all:
jobs:
- build
- release:
- build:
context: open-source
filters:
branches:
ignore: /.*/
tags:
only: /.*/

View File

@@ -1,15 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.go]
indent_style = tab
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 4

9
.gitignore vendored
View File

@@ -1,2 +1,9 @@
/.idea
/.kubeconfig*
/dist
/coverage.out
/kubelogin
/.kubeconfig
/kubectl-oidc_login

35
Makefile Normal file
View File

@@ -0,0 +1,35 @@
TARGET := kubelogin
TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
.PHONY: check run release clean
all: $(TARGET)
check:
golangci-lint run
$(MAKE) -C adaptors_test/keys/testdata
go test -v -race -cover -coverprofile=coverage.out ./...
$(TARGET): $(wildcard *.go)
go build -o $@ -ldflags "$(LDFLAGS)"
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
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/
release: dist
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
ghcp -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
clean:
-rm $(TARGET)
-rm $(TARGET_PLUGIN)
-rm -r dist/

405
README.md
View File

@@ -1,234 +1,271 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin)
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
`kubelogin` is a command to get an OpenID Connect (OIDC) token for `kubectl` authentication.
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`.
It gets a token from the OIDC provider and writes it to the kubeconfig.
## TL;DR
## Getting Started
1. Setup your OpenID Connect provider, e.g. Google Identity Platform or Keycloak.
1. Setup your Kubernetes cluster.
1. Setup your `kubectl`.
```
% kubelogin --help
2018/08/15 19:08:58 Usage:
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
Help Options:
-h, --help Show this help message
```
## Getting Started with Google Account
### 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client as follows:
- Application Type: Web application
- Redirect URL: `http://localhost:8000/`
### 2. Setup Kubernetes cluster
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
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
```
Here assign the `cluster-admin` role to your user.
```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: https://accounts.google.com#1234567890
```
### 3. Setup kubectl and kubelogin
Setup `kubectl` to authenticate with your identity provider.
You can install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
# Homebrew
brew tap int128/kubelogin
brew install kubelogin
# Krew
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.11.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
You need to setup the following components:
Run `kubelogin` and open http://localhost:8000 in your browser.
- OIDC provider
- Kubernetes API server
- kubectl authentication
- Role binding
For more, see the following documents:
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
### Login manually
Just run:
```sh
kubelogin
# or run as a kubectl plugin
kubectl oidc-login
```
It automatically opens the browser and you can log in to the provider.
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
After authentication, it writes an ID token and refresh token to the kubeconfig.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
Updated ~/.kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
Now you can access to the cluster.
```
% kubectl get nodes
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
% kubectl get pods
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the token is valid, kubelogin does nothing.
```
% kubelogin
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
```
## Getting Started with Keycloak
### Login transparently
### 1. Setup Keycloak
Create an OIDC client as follows:
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
- Client ID: `kubernetes`
- Groups claim: `groups`
Then create a group `kubernetes:admin` and join to it.
### 2. Setup Kubernetes cluster
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
oidcClientID: kubernetes
oidcGroupsClaim: groups
```
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
```
### 3. Setup kubectl and kubelogin
Setup `kubectl` to authenticate with your identity provider.
You can wrap kubectl to transparently login to the provider.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
alias kubectl='kubelogin exec -- kubectl'
# or run as a kubectl plugin
alias kubectl='kubectl oidc-login exec -- kubectl'
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
If the token expired, it updates the kubeconfig and executes kubectl.
Run `kubelogin` and make sure you can access to the cluster.
See the previous section for details.
```
% kubectl get pods
Open http://localhost:8000 for authentication
You got a valid token until 2019-06-05 19:05:34 +0900 JST
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the token is valid, it just executes kubectl.
```
% kubectl get pods
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
It respects kubectl options passed to the extra arguments.
For example, if you run `kubectl --kubeconfig .kubeconfig`,
it will update `.kubeconfig` and execute kubectl.
If the current auth provider is not `oidc`, it just executes kubectl.
## Configuration
This document is for the development version.
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
Kubelogin supports the following options:
```
Usage:
kubelogin [flags]
kubelogin [command]
Examples:
# Login to the provider using authorization code grant.
kubelogin
# Login to the provider using resource owner password credentials grant.
kubelogin --username USERNAME --password PASSWORD
# Wrap kubectl and login transparently
alias kubectl='kubelogin exec -- kubectl'
Available Commands:
exec Login transparently and execute the kubectl command
help Help about any command
version Print the version information
Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-v, --v int If set to 1 or greater, it shows debug log
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
-h, --help help for kubelogin
```
It supports the following keys of `auth-provider` in a kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
Key | Direction | Value
----|-----------|------
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
`client-id` | Read (Mandatory) | Client ID of the provider.
`client-secret` | Read (Mandatory) | Client Secret of the provider.
`idp-certificate-authority` | Read | CA certificate path of the provider.
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
`id-token` | Write | ID token got from the provider.
`refresh-token` | Write | Refresh token got from the provider.
### Kubeconfig
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
It defaults to `~/.kube/config`.
```sh
export KUBECONFIG="$PWD/.kubeconfig"
# by the option
kubelogin --kubeconfig /path/to/kubeconfig
# by the environment variable
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
```
### OpenID Connect Provider CA Certificate
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
You can specify the CA certificate of your OpenID Connect provider by [`idp-certificate-authority` or `idp-certificate-authority-data` in the kubeconfig](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
### Authentication flows
#### Authorization code flow
Kubelogin performs the authorization code flow by default.
It starts the local server at port 8000 or 18000 by default.
You need to register the following redirect URIs to the OIDC provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the ports by the option:
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
kubelogin --listen-port 12345 --listen-port 23456
```
### Setup script
In actual team operation, you can share the following script to your team members for easy setup.
#### Resource owner password credentials grant flow
You can use the resource owner password credentials grant flow.
Note that not all providers support this flow as:
> The resource owner password credentials grant type is suitable in
> cases where the resource owner has a trust relationship with the
> client, such as the device operating system or a highly privileged
> application.
> The authorization server should take special care when
> enabling this grant type and only allow it when other flows are not
> viable.
>
> <https://tools.ietf.org/html/rfc6749#section-4.3>
You can pass the username and password:
```
% kubelogin --username USER --password PASS
```
or ask the password:
```
% kubelogin --username USER
Password:
```
### Extra scopes
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
#!/bin/sh -xe
CLUSTER_NAME="hello.k8s.local"
export KUBECONFIG="$PWD/.kubeconfig"
kubectl config set-cluster "$CLUSTER_NAME" \
--server https://api-xxx.xxx.elb.amazonaws.com \
--certificate-authority "$PWD/cluster.crt"
kubectl config set-credentials "$CLUSTER_NAME" \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
kubectl config use-context "$CLUSTER_NAME"
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
Note that kubectl does not accept multiple scopes and you need to edit the kubeconfig as like:
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
### CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by kubeconfig or option.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```
### HTTP Proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
## Contributions
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests.
### Build
```sh
go get github.com/int128/kubelogin
```
### Release
CircleCI publishes the build to GitHub. See [.circleci/config.yml](.circleci/config.yml).
Feel free to open issues and pull requests for improving code and documents.

79
adaptors/adaptors.go Normal file
View File

@@ -0,0 +1,79 @@
package adaptors
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/models/kubeconfig"
)
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,OIDC,OIDCClient,Env,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type Kubeconfig interface {
GetCurrentAuth(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error)
UpdateAuth(auth *kubeconfig.Auth) error
}
type OIDC interface {
New(config OIDCClientConfig) (OIDCClient, error)
}
type OIDCClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
SkipTLSVerify bool
}
type OIDCClient interface {
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*oidc.IDToken, error)
}
type OIDCAuthenticateByCodeIn struct {
Config kubeconfig.OIDCConfig
LocalServerPort []int // HTTP server port candidates
SkipOpenBrowser bool // skip opening browser if true
ShowLocalServerURL interface{ ShowLocalServerURL(url string) }
}
type OIDCAuthenticateByPasswordIn struct {
Config kubeconfig.OIDCConfig
Username string
Password string
}
type OIDCAuthenticateOut struct {
VerifiedIDToken *oidc.IDToken
IDToken string
RefreshToken string
}
type OIDCVerifyIn struct {
Config kubeconfig.OIDCConfig
}
type Env interface {
ReadPassword(prompt string) (string, error)
Exec(ctx context.Context, executable string, args []string) (int, error)
}
type Logger interface {
Printf(format string, v ...interface{})
Debugf(level LogLevel, format string, v ...interface{})
SetLevel(level LogLevel)
IsEnabled(level LogLevel) bool
}
// LogLevel represents a log level for debug.
//
// 0 = None
// 1 = Including in/out
// 2 = Including transport headers
// 3 = Including transport body
//
type LogLevel int

168
adaptors/cmd/cmd.go Normal file
View File

@@ -0,0 +1,168 @@
package cmd
import (
"context"
"fmt"
"path/filepath"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const examples = ` # Login to the provider using authorization code grant.
%[1]s
# Login to the provider using resource owner password credentials grant.
%[1]s --username USERNAME --password PASSWORD
# Wrap kubectl and login transparently
alias kubectl='%[1]s exec -- kubectl'`
var defaultListenPort = []int{8000, 18000}
type Cmd struct {
Login usecases.Login
LoginAndExec usecases.LoginAndExec
Logger adaptors.Logger
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var exitCode int
executable := filepath.Base(args[0])
var o struct {
kubectlOptions
kubeloginOptions
}
rootCmd := cobra.Command{
Use: executable,
Short: "Login to the OpenID Connect provider and update the kubeconfig",
Example: fmt.Sprintf(examples, executable),
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginIn{
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,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("error: %s", err)
exitCode = 1
return
}
},
}
o.kubectlOptions.register(rootCmd.Flags())
o.kubeloginOptions.register(rootCmd.Flags())
execCmd := cobra.Command{
Use: "exec [flags] -- kubectl [args]",
Short: "Login transparently and execute the kubectl command",
Args: func(execCmd *cobra.Command, args []string) error {
if execCmd.ArgsLenAtDash() == -1 {
return errors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
}
if len(args) < 1 {
return errors.New("too few arguments")
}
return nil
},
Run: func(execCmd *cobra.Command, args []string) {
// parse the extra args and override the kubectl options
f := pflag.NewFlagSet(execCmd.Name(), pflag.ContinueOnError)
o.kubectlOptions.register(f)
// ignore unknown flags and help flags (-h/--help)
f.ParseErrorsWhitelist.UnknownFlags = true
f.BoolP("help", "h", false, "ignore help flags")
if err := f.Parse(args); err != nil {
cmd.Logger.Debugf(1, "error while parsing the extra arguments: %s", err)
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
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,
},
Executable: args[0],
Args: args[1:],
}
out, err := cmd.LoginAndExec.Do(ctx, in)
if err != nil {
cmd.Logger.Printf("error: %s", err)
exitCode = 1
return
}
exitCode = out.ExitCode
},
}
o.kubeloginOptions.register(execCmd.Flags())
rootCmd.AddCommand(&execCmd)
versionCmd := cobra.Command{
Use: "version",
Short: "Print the version information",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.Printf("%s version %s", executable, version)
},
}
rootCmd.AddCommand(&versionCmd)
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
cmd.Logger.Debugf(1, "error while parsing the arguments: %s", err)
return 1
}
return exitCode
}
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
}
func (o *kubectlOptions) 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")
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
}
type kubeloginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
func (o *kubeloginOptions) 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")
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")
}

268
adaptors/cmd/cmd_test.go Normal file
View File

@@ -0,0 +1,268 @@
package cmd
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
)
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()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
ListenPort: defaultListenPort,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
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",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
}
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{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("loginAndExec/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: defaultListenPort,
},
Executable: "kubectl",
Args: []string{"dummy"},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable, "exec", "--", "kubectl", "dummy"}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("loginAndExec/OptionsInExtraArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig2",
KubeconfigContext: "hello2.k8s.local",
KubeconfigUser: "google2",
CACertFilename: "/path/to/cacert2",
SkipTLSVerify: true,
ListenPort: defaultListenPort,
},
Executable: "kubectl",
Args: []string{
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
"--help",
},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"exec",
"--",
"kubectl",
// kubectl options in the extra args should be mapped to the options
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
// kubelogin options in the extra args should not affect
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
"--help",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("loginAndExec/OverrideOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig2",
KubeconfigContext: "hello2.k8s.local",
KubeconfigUser: "google2",
CACertFilename: "/path/to/cacert2",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
},
Executable: "kubectl",
Args: []string{
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
// kubelogin options in the first args should be mapped to the options
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
"exec",
"--",
"kubectl",
// kubectl options in the extra args should be mapped to the options
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
// kubelogin options in the extra args should not affect
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}

44
adaptors/env/env.go vendored Normal file
View File

@@ -0,0 +1,44 @@
package env
import (
"context"
"fmt"
"os"
"os/exec"
"syscall"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh/terminal"
)
// Env provides environment specific facilities.
type Env struct{}
// 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 {
return "", errors.Wrapf(err, "could not write the prompt")
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", errors.Wrapf(err, "could not read")
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", errors.Wrapf(err, "could not write a new line")
}
return string(b), nil
}
func (*Env) Exec(ctx context.Context, executable string, args []string) (int, error) {
c := exec.CommandContext(ctx, executable, args...)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
if err, ok := err.(*exec.ExitError); ok {
return err.ExitCode(), nil
}
return 0, errors.Wrapf(err, "could not execute the command")
}
return 0, nil
}

View File

@@ -0,0 +1,3 @@
package kubeconfig
type Kubeconfig struct{}

View File

@@ -0,0 +1,85 @@
package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func (*Kubeconfig) GetCurrentAuth(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, errors.WithStack(err)
}
auth, err := findCurrentAuth(config, contextName, userName)
if err != nil {
return nil, errors.Wrapf(err, "could not find the current auth provider")
}
return auth, nil
}
func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, errors.Wrapf(err, "could not load the kubeconfig")
}
return config, err
}
// findCurrentAuth resolves the current auth provider.
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func findCurrentAuth(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error) {
if userName == "" {
if contextName == "" {
contextName = kubeconfig.ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, errors.Errorf("context %s does not exist", contextName)
}
userName = kubeconfig.UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
return nil, errors.Errorf("user %s does not exist", userName)
}
if userNode.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
}
if userNode.AuthProvider.Config == nil {
return nil, errors.Errorf("auth-provider.config is missing")
}
return &kubeconfig.Auth{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
OIDCConfig: makeOIDCConfig(userNode.AuthProvider.Config),
}, nil
}
func makeOIDCConfig(m map[string]string) kubeconfig.OIDCConfig {
var extraScopes []string
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return kubeconfig.OIDCConfig{
IDPIssuerURL: m["idp-issuer-url"],
ClientID: m["client-id"],
ClientSecret: m["client-secret"],
IDPCertificateAuthority: m["idp-certificate-authority"],
IDPCertificateAuthorityData: m["idp-certificate-authority-data"],
ExtraScopes: extraScopes,
IDToken: m["id-token"],
RefreshToken: m["refresh-token"],
}
}

View File

@@ -0,0 +1,225 @@
package kubeconfig
import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/models/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
func Test_loadByDefaultRules(t *testing.T) {
t.Run("google.yaml>keycloak.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := loadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "google@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
t.Run("keycloak.yaml>google.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := loadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "keycloak@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}
func Test_findCurrentAuth(t *testing.T) {
t.Run("CurrentContext", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
CurrentContext: "theContext",
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
},
},
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"idp-issuer-url": "https://accounts.google.com",
"client-id": "GOOGLE_CLIENT_ID",
"client-secret": "GOOGLE_CLIENT_SECRET",
"idp-certificate-authority": "/path/to/cert",
"idp-certificate-authority-data": "BASE64",
"extra-scopes": "email,profile",
"id-token": "YOUR_ID_TOKEN",
"refresh-token": "YOUR_REFRESH_TOKEN",
},
},
},
},
}, "", "")
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: kubeconfig.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",
},
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
}
})
t.Run("ByContextName", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
},
},
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"idp-issuer-url": "https://accounts.google.com",
},
},
},
},
}, "theContext", "")
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
}
})
t.Run("ByUserName", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"idp-issuer-url": "https://accounts.google.com",
},
},
},
},
}, "", "theUser")
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
}
})
t.Run("NoConfig", func(t *testing.T) {
_, err := findCurrentAuth(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
},
},
},
}, "", "theUser")
if err == nil {
t.Fatalf("wants error but nil")
}
})
t.Run("NotOIDC", func(t *testing.T) {
_, err := findCurrentAuth(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "some",
Config: map[string]string{"foo": "bar"},
},
},
},
}, "", "theUser")
if err == nil {
t.Fatalf("wants error but nil")
}
})
}

View File

@@ -0,0 +1,17 @@
apiVersion: v1
clusters: []
contexts:
- context:
cluster: hello.k8s.local
user: google
name: google@hello.k8s.local
current-context: google@hello.k8s.local
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
client-id: CLIENT_ID.apps.googleusercontent.com
name: oidc

View File

@@ -0,0 +1,16 @@
apiVersion: v1
contexts:
- context:
cluster: hello.k8s.local
user: keycloak
name: keycloak@hello.k8s.local
current-context: keycloak@hello.k8s.local
kind: Config
preferences: {}
users:
- name: keycloak
user:
auth-provider:
config:
client-id: kubernetes
name: oidc

View File

@@ -0,0 +1,51 @@
package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuth(auth *kubeconfig.Auth) error {
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
if err != nil {
return errors.Wrapf(err, "could not load %s", auth.LocationOfOrigin)
}
userNode, ok := config.AuthInfos[string(auth.UserName)]
if !ok {
return errors.Errorf("user %s does not exist", auth.UserName)
}
if userNode.AuthProvider == nil {
return errors.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return errors.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 errors.Wrapf(err, "could not update %s", auth.LocationOfOrigin)
}
return nil
}
func copyOIDCConfig(config kubeconfig.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, ",")
setOrDeleteKey(m, "extra-scopes", extraScopes)
setOrDeleteKey(m, "id-token", config.IDToken)
setOrDeleteKey(m, "refresh-token", config.RefreshToken)
}
func setOrDeleteKey(m map[string]string, key, value string) {
if value == "" {
delete(m, key)
return
}
m[key] = value
}

View File

@@ -0,0 +1,138 @@
package kubeconfig
import (
"io/ioutil"
"os"
"testing"
"github.com/int128/kubelogin/models/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
var k Kubeconfig
t.Run("MinimumKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
defer func() {
if err := os.Remove(f.Name()); err != nil {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuth(&kubeconfig.Auth{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
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)
}
b, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatalf("Could not read kubeconfig: %s", err)
}
want := `apiVersion: v1
clusters: []
contexts: []
current-context: ""
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
client-id: GOOGLE_CLIENT_ID
client-secret: GOOGLE_CLIENT_SECRET
id-token: YOUR_ID_TOKEN
idp-issuer-url: https://accounts.google.com
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
}
})
t.Run("FullKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
defer func() {
if err := os.Remove(f.Name()); err != nil {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuth(&kubeconfig.Auth{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.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",
},
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
b, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatalf("Could not read kubeconfig: %s", err)
}
want := `apiVersion: v1
clusters: []
contexts: []
current-context: ""
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
client-id: GOOGLE_CLIENT_ID
client-secret: GOOGLE_CLIENT_SECRET
extra-scopes: email,profile
id-token: YOUR_ID_TOKEN
idp-certificate-authority: /path/to/cert
idp-certificate-authority-data: BASE64
idp-issuer-url: https://accounts.google.com
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
}
})
}
func newKubeconfigFile(t *testing.T) *os.File {
content := `apiVersion: v1
clusters: []
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
name: oidc`
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatalf("Could not create a file: %s", err)
}
defer f.Close()
if _, err := f.Write([]byte(content)); err != nil {
t.Fatalf("Could not write kubeconfig: %s", err)
}
return f
}

49
adaptors/logger/logger.go Normal file
View File

@@ -0,0 +1,49 @@
package logger
import (
"log"
"os"
"github.com/int128/kubelogin/adaptors"
)
// New returns a Logger with the standard log.Logger for messages and debug.
func New() adaptors.Logger {
return &Logger{
stdLogger: log.New(os.Stderr, "", 0),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
}
}
// FromStdLogger returns a Logger with the given standard log.Logger.
func FromStdLogger(l stdLogger) *Logger {
return &Logger{
stdLogger: l,
debugLogger: l,
}
}
type stdLogger interface {
Printf(format string, v ...interface{})
}
// Logger wraps the standard log.Logger and just provides debug level.
type Logger struct {
stdLogger
debugLogger stdLogger
level adaptors.LogLevel
}
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
if l.IsEnabled(level) {
l.debugLogger.Printf(format, v...)
}
}
func (l *Logger) SetLevel(level adaptors.LogLevel) {
l.level = level
}
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
return level <= l.level
}

View File

@@ -0,0 +1,53 @@
package logger
import (
"fmt"
"testing"
"github.com/int128/kubelogin/adaptors"
)
type mockStdLogger struct {
count int
}
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
l.count++
}
func TestLogger_Debugf(t *testing.T) {
for _, c := range []struct {
loggerLevel adaptors.LogLevel
debugfLevel adaptors.LogLevel
count int
}{
{0, 0, 1},
{0, 1, 0},
{1, 0, 1},
{1, 1, 1},
{1, 2, 0},
{2, 1, 1},
{2, 2, 1},
{2, 3, 0},
} {
t.Run(fmt.Sprintf("%+v", c), func(t *testing.T) {
m := &mockStdLogger{}
l := &Logger{debugLogger: m, level: c.loggerLevel}
l.Debugf(c.debugfLevel, "hello")
if m.count != c.count {
t.Errorf("count wants %d but %d", c.count, m.count)
}
})
}
}
func TestLogger_Printf(t *testing.T) {
m := &mockStdLogger{}
l := &Logger{stdLogger: m}
l.Printf("hello")
if m.count != 1 {
t.Errorf("count wants %d but %d", 1, m.count)
}
}

View File

@@ -0,0 +1,31 @@
package mock_adaptors
import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
)
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {
return &Logger{
MockLogger: NewMockLogger(ctrl),
testingLogger: t,
}
}
type testingLogger interface {
Logf(format string, v ...interface{})
}
// Logger provides mock feature but overrides output methods with actual logging.
type Logger struct {
*MockLogger
testingLogger testingLogger
}
func (l *Logger) Printf(format string, v ...interface{}) {
l.testingLogger.Logf(format, v...)
}
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
l.testingLogger.Logf(format, v...)
}

View File

@@ -0,0 +1,284 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,OIDC,OIDCClient,Env,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
import (
context "context"
go_oidc "github.com/coreos/go-oidc"
gomock "github.com/golang/mock/gomock"
adaptors "github.com/int128/kubelogin/adaptors"
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
reflect "reflect"
)
// MockKubeconfig is a mock of Kubeconfig interface
type MockKubeconfig struct {
ctrl *gomock.Controller
recorder *MockKubeconfigMockRecorder
}
// MockKubeconfigMockRecorder is the mock recorder for MockKubeconfig
type MockKubeconfigMockRecorder struct {
mock *MockKubeconfig
}
// NewMockKubeconfig creates a new mock instance
func NewMockKubeconfig(ctrl *gomock.Controller) *MockKubeconfig {
mock := &MockKubeconfig{ctrl: ctrl}
mock.recorder = &MockKubeconfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockKubeconfig) EXPECT() *MockKubeconfigMockRecorder {
return m.recorder
}
// GetCurrentAuth mocks base method
func (m *MockKubeconfig) GetCurrentAuth(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.Auth, error) {
ret := m.ctrl.Call(m, "GetCurrentAuth", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCurrentAuth indicates an expected call of GetCurrentAuth
func (mr *MockKubeconfigMockRecorder) GetCurrentAuth(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuth", reflect.TypeOf((*MockKubeconfig)(nil).GetCurrentAuth), arg0, arg1, arg2)
}
// UpdateAuth mocks base method
func (m *MockKubeconfig) UpdateAuth(arg0 *kubeconfig.Auth) error {
ret := m.ctrl.Call(m, "UpdateAuth", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuth indicates an expected call of UpdateAuth
func (mr *MockKubeconfigMockRecorder) UpdateAuth(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuth", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuth), arg0)
}
// MockOIDC is a mock of OIDC interface
type MockOIDC struct {
ctrl *gomock.Controller
recorder *MockOIDCMockRecorder
}
// MockOIDCMockRecorder is the mock recorder for MockOIDC
type MockOIDCMockRecorder struct {
mock *MockOIDC
}
// NewMockOIDC creates a new mock instance
func NewMockOIDC(ctrl *gomock.Controller) *MockOIDC {
mock := &MockOIDC{ctrl: ctrl}
mock.recorder = &MockOIDCMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockOIDC) New(arg0 adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
ret := m.ctrl.Call(m, "New", arg0)
ret0, _ := ret[0].(adaptors.OIDCClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockOIDCMockRecorder) New(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOIDC)(nil).New), arg0)
}
// MockOIDCClient is a mock of OIDCClient interface
type MockOIDCClient struct {
ctrl *gomock.Controller
recorder *MockOIDCClientMockRecorder
}
// MockOIDCClientMockRecorder is the mock recorder for MockOIDCClient
type MockOIDCClientMockRecorder struct {
mock *MockOIDCClient
}
// NewMockOIDCClient creates a new mock instance
func NewMockOIDCClient(ctrl *gomock.Controller) *MockOIDCClient {
mock := &MockOIDCClient{ctrl: ctrl}
mock.recorder = &MockOIDCClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDCClient) EXPECT() *MockOIDCClientMockRecorder {
return m.recorder
}
// AuthenticateByCode mocks base method
func (m *MockOIDCClient) AuthenticateByCode(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByCode indicates an expected call of AuthenticateByCode
func (mr *MockOIDCClientMockRecorder) AuthenticateByCode(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByCode), arg0, arg1)
}
// AuthenticateByPassword mocks base method
func (m *MockOIDCClient) AuthenticateByPassword(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
func (mr *MockOIDCClientMockRecorder) AuthenticateByPassword(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByPassword), arg0, arg1)
}
// Verify mocks base method
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*go_oidc.IDToken, error) {
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
ret0, _ := ret[0].(*go_oidc.IDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify
func (mr *MockOIDCClientMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDCClient)(nil).Verify), arg0, arg1)
}
// MockEnv is a mock of Env interface
type MockEnv struct {
ctrl *gomock.Controller
recorder *MockEnvMockRecorder
}
// MockEnvMockRecorder is the mock recorder for MockEnv
type MockEnvMockRecorder struct {
mock *MockEnv
}
// NewMockEnv creates a new mock instance
func NewMockEnv(ctrl *gomock.Controller) *MockEnv {
mock := &MockEnv{ctrl: ctrl}
mock.recorder = &MockEnvMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockEnv) EXPECT() *MockEnvMockRecorder {
return m.recorder
}
// Exec mocks base method
func (m *MockEnv) Exec(arg0 context.Context, arg1 string, arg2 []string) (int, error) {
ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec
func (mr *MockEnvMockRecorder) Exec(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockEnv)(nil).Exec), arg0, arg1, arg2)
}
// ReadPassword mocks base method
func (m *MockEnv) ReadPassword(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockEnvMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockEnv)(nil).ReadPassword), arg0)
}
// MockLogger is a mock of Logger interface
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debugf mocks base method
func (m *MockLogger) Debugf(arg0 adaptors.LogLevel, arg1 string, arg2 ...interface{}) {
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf
func (mr *MockLoggerMockRecorder) Debugf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// IsEnabled mocks base method
func (m *MockLogger) IsEnabled(arg0 adaptors.LogLevel) bool {
ret := m.ctrl.Call(m, "IsEnabled", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsEnabled indicates an expected call of IsEnabled
func (mr *MockLoggerMockRecorder) IsEnabled(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockLogger)(nil).IsEnabled), arg0)
}
// Printf mocks base method
func (m *MockLogger) Printf(arg0 string, arg1 ...interface{}) {
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Printf", varargs...)
}
// Printf indicates an expected call of Printf
func (mr *MockLoggerMockRecorder) Printf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Printf", reflect.TypeOf((*MockLogger)(nil).Printf), varargs...)
}
// SetLevel mocks base method
func (m *MockLogger) SetLevel(arg0 adaptors.LogLevel) {
m.ctrl.Call(m, "SetLevel", arg0)
}
// SetLevel indicates an expected call of SetLevel
func (mr *MockLoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*MockLogger)(nil).SetLevel), arg0)
}

75
adaptors/oidc/http.go Normal file
View File

@@ -0,0 +1,75 @@
package oidc
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/pkg/errors"
)
func newHTTPClient(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*http.Client, error) {
pool := x509.NewCertPool()
if config.Config.IDPCertificateAuthority != "" {
logger.Debugf(1, "Loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority")
}
}
if config.Config.IDPCertificateAuthorityData != "" {
logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority-data")
}
}
if config.CACertFilename != "" {
logger.Debugf(1, "Loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate")
}
}
var tlsConfig tls.Config
if len(pool.Subjects()) > 0 {
tlsConfig.RootCAs = pool
}
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
return &http.Client{
Transport: &logging.Transport{
Base: &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
},
Logger: logger,
},
}, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "could not read %s", filename)
}
if !pool.AppendCertsFromPEM(b) {
return errors.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 errors.Wrapf(err, "could not decode base64")
}
if !pool.AppendCertsFromPEM(b) {
return errors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,50 @@
package logging
import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/adaptors"
)
const (
logLevelDumpHeaders = 2
logLevelDumpBody = 3
)
type Transport struct {
Base http.RoundTripper
Logger adaptors.Logger
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.IsDumpEnabled() {
return t.Base.RoundTrip(req)
}
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
return t.Base.RoundTrip(req)
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
resp, err := t.Base.RoundTrip(req)
if err != nil {
return resp, err
}
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
return resp, err
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
return resp, err
}
func (t *Transport) IsDumpEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpHeaders)
}
func (t *Transport) IsDumpBodyEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpBody)
}

View File

@@ -0,0 +1,90 @@
package logging
import (
"bufio"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
)
type mockTransport struct {
req *http.Request
resp *http.Response
}
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
t.req = req
return t.resp, nil
}
func TestLoggingTransport_RoundTrip(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(gomock.Any()).
Return(true).
AnyTimes()
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
Host: example.com
dummy`)), req)
if err != nil {
t.Errorf("could not create a response: %s", err)
}
defer resp.Body.Close()
transport := &Transport{
Base: &mockTransport{resp: resp},
Logger: logger,
}
gotResp, err := transport.RoundTrip(req)
if err != nil {
t.Errorf("RoundTrip error: %s", err)
}
if gotResp != resp {
t.Errorf("resp wants %v but %v", resp, gotResp)
}
}
func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
Return(true)
transport := &Transport{
Logger: logger,
}
if transport.IsDumpEnabled() != true {
t.Errorf("IsDumpEnabled wants true")
}
}
func TestLoggingTransport_IsDumpBodyEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(adaptors.LogLevel(logLevelDumpBody)).
Return(true)
transport := &Transport{
Logger: logger,
}
if transport.IsDumpBodyEnabled() != true {
t.Errorf("IsDumpBodyEnabled wants true")
}
}

118
adaptors/oidc/oidc.go Normal file
View File

@@ -0,0 +1,118 @@
package oidc
import (
"context"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
type Factory struct {
Logger adaptors.Logger
}
func (f *Factory) New(config adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
hc, err := newHTTPClient(config, f.Logger)
if err != nil {
return nil, errors.Wrapf(err, "could not create a HTTP client")
}
return &Client{hc}, nil
}
type Client struct {
hc *http.Client
}
func (c *Client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2cli.Config{
OAuth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID,
ClientSecret: in.Config.ClientSecret,
Scopes: append(in.Config.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
ShowLocalServerURL: in.ShowLocalServerURL.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (c *Client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID,
ClientSecret: in.Config.ClientSecret,
Scopes: append(in.Config.ExtraScopes, oidc.ScopeOpenID),
}
token, err := config.PasswordCredentialsToken(ctx, in.Username, in.Password)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (c *Client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*oidc.IDToken, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, in.Config.IDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return verifiedIDToken, nil
}

View File

@@ -0,0 +1,77 @@
// Package authserver provides an authentication server which supports
// Authorization Code Grant and Resource Owner Password Credentials Grant.
// This is only for testing.
//
package authserver
import (
"context"
"net"
"net/http"
"testing"
)
type Shutdowner interface {
Shutdown(t *testing.T, ctx context.Context)
}
type shutdowner struct {
l net.Listener
s *http.Server
}
func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
// s.Shutdown() closes the lister as well,
// so we do not need to call l.Close() explicitly
if err := s.s.Shutdown(ctx); err != nil {
t.Errorf("Could not shutdown the server: %s", err)
}
}
// Start starts an authentication server.
func Start(t *testing.T, h func(url string) http.Handler) Shutdowner {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
s := &http.Server{
Handler: h(url),
}
go func() {
err := s.Serve(l)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return &shutdowner{l, s}
}
// Start starts an authentication server with TLS.
func StartTLS(t *testing.T, cert string, key string, h func(url string) http.Handler) Shutdowner {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
s := &http.Server{
Handler: h(url),
}
go func() {
err := s.ServeTLS(l, cert, key)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return &shutdowner{l, s}
}
func newLocalhostListener(t *testing.T) (net.Listener, string) {
t.Helper()
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Could not create a listener: %s", err)
}
addr := l.Addr().String()
_, port, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("Could not parse the address %s: %s", addr, err)
}
return l, port
}

View File

@@ -0,0 +1,107 @@
package authserver
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
"net/http"
"testing"
"github.com/pkg/errors"
)
// CodeConfig represents a config for Authorization Code Grant.
type CodeConfig struct {
Issuer string
Scope string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
Code string
}
type codeHandler struct {
t *testing.T
c CodeConfig
templates templates
values templateValues
}
func NewCodeHandler(t *testing.T, c CodeConfig) *codeHandler {
if c.Scope == "" {
c.Scope = "openid"
}
if c.Code == "" {
c.Code = "3d24a8bd-35e6-457d-999e-e04bb1dfcec7"
}
h := codeHandler{
t: t,
c: c,
templates: parseTemplates(t),
values: templateValues{
Issuer: c.Issuer,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
},
}
if c.IDTokenKeyPair != nil {
h.values.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.values.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *codeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("authserver/codeHandler: Error: %s", err)
w.WriteHeader(500)
}
}
func (h *codeHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("authserver/codeHandler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.discovery.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
if h.c.Scope != q.Get("scope") {
return errors.Errorf("scope wants %s but %s", h.c.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.c.Code)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
if err := r.ParseForm(); err != nil {
return errors.Wrapf(err, "could not parse the form")
}
grantType, code := r.Form.Get("grant_type"), r.Form.Get("code")
if grantType != "authorization_code" {
return errors.Errorf("grant_type wants authorization_code but %s", grantType)
}
if h.c.Code != code {
return errors.Errorf("code wants %s but %s", h.c.Code, code)
}
w.Header().Add("Content-Type", "application/json")
if err := h.templates.token.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.jwks.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -0,0 +1,98 @@
package authserver
import (
"crypto/rsa"
"encoding/base64"
"math/big"
"net/http"
"testing"
"github.com/pkg/errors"
)
// PasswordConfig represents a config for Resource Owner Password Credentials Grant.
type PasswordConfig struct {
Issuer string
Scope string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
Username string
Password string
}
type passwordHandler struct {
t *testing.T
c PasswordConfig
templates templates
values templateValues
}
func NewPasswordHandler(t *testing.T, c PasswordConfig) *passwordHandler {
if c.Scope == "" {
c.Scope = "openid"
}
h := passwordHandler{
t: t,
c: c,
templates: parseTemplates(t),
values: templateValues{
Issuer: c.Issuer,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
},
}
if c.IDTokenKeyPair != nil {
h.values.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.values.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *passwordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("authserver/passwordHandler: Error: %s", err)
w.WriteHeader(500)
}
}
func (h *passwordHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("authserver/passwordHandler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.discovery.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// https://tools.ietf.org/html/rfc6749#section-4.3
if err := r.ParseForm(); err != nil {
return errors.Wrapf(err, "could not parse the form")
}
grantType, username, password := r.Form.Get("grant_type"), r.Form.Get("username"), r.Form.Get("password")
if grantType != "password" {
return errors.Errorf("grant_type wants password but %s", grantType)
}
if h.c.Username != username {
return errors.Errorf("username wants %s but %s", h.c.Username, username)
}
if h.c.Password != password {
return errors.Errorf("password wants %s but %s", h.c.Password, password)
}
w.Header().Add("Content-Type", "application/json")
if err := h.templates.token.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.jwks.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -0,0 +1,35 @@
package authserver
import (
"testing"
"text/template"
)
func parseTemplates(t *testing.T) templates {
tpl, err := template.ParseFiles(
"authserver/testdata/oidc-discovery.json",
"authserver/testdata/oidc-token.json",
"authserver/testdata/oidc-jwks.json",
)
if err != nil {
t.Fatalf("could not read the templates: %s", err)
}
return templates{
discovery: tpl.Lookup("oidc-discovery.json"),
token: tpl.Lookup("oidc-token.json"),
jwks: tpl.Lookup("oidc-jwks.json"),
}
}
type templates struct {
discovery *template.Template
token *template.Template
jwks *template.Template
}
type templateValues struct {
Issuer string
IDToken string
RefreshToken string
PrivateKey struct{ N, E string }
}

View File

@@ -1,7 +1,7 @@
{
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
"token_type": "Bearer",
"refresh_token": "44df4c82-5ce7-4260-b54d-1da0d396ef2a",
"refresh_token": "{{ .RefreshToken }}",
"expires_in": 3600,
"id_token": "{{ .IDToken }}"
}

343
adaptors_test/cmd_test.go Normal file
View File

@@ -0,0 +1,343 @@
package adaptors_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"sync"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/adaptors_test/authserver"
"github.com/int128/kubelogin/adaptors_test/keys"
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
"github.com/int128/kubelogin/adaptors_test/logger"
"github.com/int128/kubelogin/di"
)
// Run the integration tests.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the kuneconfig.
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var passwordConfig authserver.PasswordConfig
server := authserver.Start(t, func(url string) http.Handler {
passwordConfig = authserver.PasswordConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Username: "USER",
Password: "PASS",
}
return authserver.NewPasswordHandler(t, passwordConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: passwordConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, nil, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: passwordConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACert", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACertData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDTokenKeyPair: keys.JWSKeyPair,
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, codeConfig.Issuer)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
IDToken: idToken,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, nil, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
})
})
}
func newIDToken(t *testing.T, issuer string) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
}
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 runCmd(t *testing.T, ctx context.Context, br *browserRequest, args ...string) {
t.Helper()
cmd := di.NewCmdWith(logger.New(t), br)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
type browserRequest struct {
t *testing.T
urlCh chan<- string
wg *sync.WaitGroup
}
func (r *browserRequest) ShowLocalServerURL(url string) {
defer close(r.urlCh)
r.t.Logf("Open %s for authentication", url)
r.urlCh <- url
}
func (r *browserRequest) wait() {
r.wg.Wait()
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
t.Helper()
urlCh := make(chan string)
var wg sync.WaitGroup
go func() {
defer wg.Done()
select {
case url := <-urlCh:
client := http.Client{Transport: &http.Transport{TLSClientConfig: 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)
}
case err := <-ctx.Done():
t.Errorf("context done while waiting for URL prompt: %s", err)
}
}()
wg.Add(1)
return &browserRequest{t, urlCh, &wg}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

View File

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

View File

@@ -1,40 +1,40 @@
.PHONY: clean
all: authserver.crt authserver-ca.crt
all: server.crt ca.crt jws.key
clean:
rm -v authserver*
rm -v ca.* server.*
authserver-ca.key:
ca.key:
openssl genrsa -out $@ 1024
authserver-ca.csr: openssl.cnf authserver-ca.key
ca.csr: openssl.cnf ca.key
openssl req -config openssl.cnf \
-new \
-key authserver-ca.key \
-key ca.key \
-subj "/CN=Hello CA" \
-out $@
openssl req -noout -text -in $@
authserver-ca.crt: authserver-ca.csr authserver-ca.key
ca.crt: ca.csr ca.key
openssl x509 -req \
-signkey authserver-ca.key \
-in authserver-ca.csr \
-signkey ca.key \
-in ca.csr \
-out $@
openssl x509 -text -in $@
authserver.key:
server.key:
openssl genrsa -out $@ 1024
authserver.csr: openssl.cnf authserver.key
server.csr: openssl.cnf server.key
openssl req -config openssl.cnf \
-new \
-key authserver.key \
-key server.key \
-subj "/CN=localhost" \
-out $@
openssl req -noout -text -in $@
authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
server.crt: openssl.cnf server.csr ca.key ca.crt
rm -fr ./CA
mkdir -p ./CA
touch CA/index.txt
@@ -43,8 +43,11 @@ authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
openssl ca -config openssl.cnf \
-extensions v3_req \
-batch \
-cert authserver-ca.crt \
-keyfile authserver-ca.key \
-in authserver.csr \
-cert ca.crt \
-keyfile ca.key \
-in server.csr \
-out $@
openssl x509 -text -in $@
jws.key:
openssl genrsa -out $@ 1024

View File

@@ -0,0 +1,79 @@
package kubeconfig
import (
"html/template"
"io/ioutil"
"os"
"testing"
"gopkg.in/yaml.v2"
)
// Values represents values in .kubeconfig template.
type Values struct {
Issuer string
ExtraScopes string
IDPCertificateAuthority string
IDPCertificateAuthorityData string
IDToken string
}
// Create creates a kubeconfig file and returns path to it.
func Create(t *testing.T, v *Values) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tpl, err := template.ParseFiles("kubeconfig/testdata/kubeconfig.yaml")
if err != nil {
t.Fatal(err)
}
if err := tpl.Execute(f, v); err != nil {
t.Fatal(err)
}
return f.Name()
}
type AuthProviderConfig struct {
IDToken string `yaml:"id-token"`
RefreshToken string `yaml:"refresh-token"`
}
// Verify returns true if the kubeconfig has valid values.
func Verify(t *testing.T, kubeconfig string, want AuthProviderConfig) {
t.Helper()
f, err := os.Open(kubeconfig)
if err != nil {
t.Errorf("could not open kubeconfig: %s", err)
return
}
defer f.Close()
var y struct {
Users []struct {
User struct {
AuthProvider struct {
Config AuthProviderConfig `yaml:"config"`
} `yaml:"auth-provider"`
} `yaml:"user"`
} `yaml:"users"`
}
d := yaml.NewDecoder(f)
if err := d.Decode(&y); err != nil {
t.Errorf("could not decode YAML: %s", err)
return
}
if len(y.Users) != 1 {
t.Errorf("len(users) wants 1 but %d", len(y.Users))
return
}
currentConfig := y.Users[0].User.AuthProvider.Config
if currentConfig.IDToken != want.IDToken {
t.Errorf("id-token wants %s but %s", want.IDToken, currentConfig.IDToken)
}
if currentConfig.RefreshToken != want.RefreshToken {
t.Errorf("refresh-token wants %s but %s", want.RefreshToken, currentConfig.RefreshToken)
}
}

View File

@@ -0,0 +1,4 @@
apiVersion: v1
current-context: dummy
kind: Config
preferences: {}

View File

@@ -19,10 +19,16 @@ users:
client-id: kubernetes
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
idp-issuer-url: {{ .Issuer }}
#{{ if .ExtraScopes }}
extra-scopes: {{ .ExtraScopes }}
#{{ end }}
#{{ if .IDPCertificateAuthority }}
idp-certificate-authority: {{ .IDPCertificateAuthority }}
#{{ end }}
#{{ if .IDPCertificateAuthorityData }}
idp-certificate-authority-data: {{ .IDPCertificateAuthorityData }}
#{{ end }}
#{{ if .IDToken }}
id-token: {{ .IDToken }}
#{{ end }}
name: oidc

View File

@@ -0,0 +1,21 @@
package logger
import (
"github.com/int128/kubelogin/adaptors/logger"
)
func New(t testingLogger) *logger.Logger {
return logger.FromStdLogger(&bridge{t})
}
type testingLogger interface {
Logf(format string, v ...interface{})
}
type bridge struct {
t testingLogger
}
func (b *bridge) Printf(format string, v ...interface{}) {
b.t.Logf(format, v...)
}

View File

@@ -1,107 +0,0 @@
package auth
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"net/http"
"golang.org/x/oauth2"
)
// BrowserAuthCodeFlow is a flow to get a token by browser interaction.
type BrowserAuthCodeFlow struct {
oauth2.Config
Port int // HTTP server port
}
// GetToken returns a token.
func (f *BrowserAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
f.Config.RedirectURL = fmt.Sprintf("http://localhost:%d/", f.Port)
state, err := generateState()
if err != nil {
return nil, fmt.Errorf("Could not generate state parameter: %s", err)
}
log.Printf("Open http://localhost:%d for authorization", f.Port)
code, err := f.getCode(ctx, &f.Config, state)
if err != nil {
return nil, err
}
token, err := f.Config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
}
return token, nil
}
func generateState() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", err
}
return fmt.Sprintf("%x", n), nil
}
func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config, state string) (string, error) {
codeCh := make(chan string)
errCh := make(chan error)
server := http.Server{
Addr: fmt.Sprintf(":%d", f.Port),
Handler: &handler{
AuthCodeURL: config.AuthCodeURL(state),
Callback: func(code string, actualState string, err error) {
switch {
case err != nil:
errCh <- err
case actualState != state:
errCh <- fmt.Errorf("OAuth state did not match, should be %s but %s", state, actualState)
default:
codeCh <- code
}
},
},
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
server.Shutdown(ctx)
return "", err
case code := <-codeCh:
server.Shutdown(ctx)
return code, nil
}
}
type handler struct {
AuthCodeURL string
Callback func(code string, state string, err error)
}
func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.RequestURI)
switch r.URL.Path {
case "/":
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
errorCode := r.URL.Query().Get("error")
errorDescription := r.URL.Query().Get("error_description")
switch {
case code != "":
s.Callback(code, state, nil)
fmt.Fprintf(w, "Back to command line.")
case errorCode != "":
s.Callback("", "", fmt.Errorf("OAuth Error: %s %s", errorCode, errorDescription))
fmt.Fprintf(w, "Back to command line.")
default:
http.Redirect(w, r, s.AuthCodeURL, 302)
}
default:
http.Error(w, "Not Found", 404)
}
}

View File

@@ -1,60 +0,0 @@
package auth
import (
"context"
"fmt"
oidc "github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
// TokenSet is a set of tokens and claims.
type TokenSet struct {
IDToken string
RefreshToken string
Claims *Claims
}
// Claims represents properties in the ID token.
type Claims struct {
Email string `json:"email"`
}
// GetTokenSet retrieves a token from the OIDC provider.
func GetTokenSet(ctx context.Context, issuer string, clientID string, clientSecret string) (*TokenSet, error) {
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return nil, fmt.Errorf("Could not access OIDC issuer: %s", err)
}
flow := BrowserAuthCodeFlow{
Port: 8000,
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{oidc.ScopeOpenID, "email"},
},
}
token, err := flow.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("Could not get a token: %s", err)
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("Could not verify the id_token: %s", err)
}
var claims Claims
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("Could not extract claims from the token response: %s", err)
}
return &TokenSet{
IDToken: rawIDToken,
RefreshToken: token.RefreshToken,
Claims: &claims,
}, nil
}

View File

@@ -1,115 +0,0 @@
package cli
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/int128/kubelogin/auth"
"github.com/int128/kubelogin/kubeconfig"
flags "github.com/jessevdk/go-flags"
homedir "github.com/mitchellh/go-homedir"
"golang.org/x/oauth2"
)
// Parse parses command line arguments and returns a CLI instance.
func Parse(args []string) (*CLI, error) {
var cli CLI
parser := flags.NewParser(&cli, flags.HelpFlag)
args, err := parser.Parse()
if err != nil {
return nil, err
}
if len(args) > 0 {
return nil, fmt.Errorf("Too many argument")
}
return &cli, nil
}
// CLI represents an interface of this command.
type CLI struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
// CertificateAuthority string `long:"certificate-authority" env:"KUBELOGIN_CERTIFICATE_AUTHORITY" description:"Path to a cert file for the certificate authority"`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *CLI) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s", c.KubeConfig)
}
return d, nil
}
// Run performs this command.
func (c *CLI) Run(ctx context.Context) error {
path, err := c.ExpandKubeConfig()
if err != nil {
return err
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Load(path)
if err != nil {
return fmt.Errorf("Could not load kubeconfig: %s", err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
if err != nil {
return fmt.Errorf("Could not find auth-provider: %s", err)
}
tlsConfig, err := c.tlsConfig(authProvider)
if err != nil {
return fmt.Errorf("Could not configure TLS: %s", err)
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
token, err := auth.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
return fmt.Errorf("Authentication error: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
return nil
}
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProviderConfig) (*tls.Config, error) {
p := x509.NewCertPool()
if authProvider.IDPCertificateAuthority() != "" {
b, err := ioutil.ReadFile(authProvider.IDPCertificateAuthority())
if err != nil {
return nil, fmt.Errorf("Could not read idp-certificate-authority: %s", err)
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority: %s", err)
}
log.Printf("Using CA certificate: %s", authProvider.IDPCertificateAuthority())
}
if authProvider.IDPCertificateAuthorityData() != "" {
b, err := base64.StdEncoding.DecodeString(authProvider.IDPCertificateAuthorityData())
if err != nil {
return nil, fmt.Errorf("Could not decode idp-certificate-authority-data: %s", err)
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority-data: %s", err)
}
log.Printf("Using CA certificate of idp-certificate-authority-data")
}
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}
if len(p.Subjects()) > 0 {
cfg.RootCAs = p
}
return cfg, nil
}

57
di/di.go Normal file
View File

@@ -0,0 +1,57 @@
//+build wireinject
// Package di provides dependency injection.
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"github.com/int128/kubelogin/adaptors/env"
"github.com/int128/kubelogin/adaptors/kubeconfig"
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/login"
)
var usecasesSet = wire.NewSet(
login.Login{},
login.Exec{},
wire.Bind((*usecases.Login)(nil), (*login.Login)(nil)),
wire.Bind((*usecases.LoginAndExec)(nil), (*login.Exec)(nil)),
)
var adaptorsSet = wire.NewSet(
cmd.Cmd{},
kubeconfig.Kubeconfig{},
oidc.Factory{},
env.Env{},
wire.Bind((*adaptors.Cmd)(nil), (*cmd.Cmd)(nil)),
wire.Bind((*adaptors.Kubeconfig)(nil), (*kubeconfig.Kubeconfig)(nil)),
wire.Bind((*adaptors.OIDC)(nil), (*oidc.Factory)(nil)),
wire.Bind((*adaptors.Env)(nil), (*env.Env)(nil)),
)
var extraSet = wire.NewSet(
login.ShowLocalServerURL{},
wire.Bind((*usecases.LoginShowLocalServerURL)(nil), (*login.ShowLocalServerURL)(nil)),
logger.New,
)
func NewCmd() adaptors.Cmd {
wire.Build(
usecasesSet,
adaptorsSet,
extraSet,
)
return nil
}
func NewCmdWith(adaptors.Logger, usecases.LoginShowLocalServerURL) adaptors.Cmd {
wire.Build(
usecasesSet,
adaptorsSet,
)
return nil
}

88
di/wire_gen.go Normal file
View File

@@ -0,0 +1,88 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"github.com/int128/kubelogin/adaptors/env"
"github.com/int128/kubelogin/adaptors/kubeconfig"
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/login"
)
// Injectors from di.go:
func NewCmd() adaptors.Cmd {
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
adaptorsLogger := logger.New()
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
showLocalServerURL := &login.ShowLocalServerURL{
Logger: adaptorsLogger,
}
loginLogin := &login.Login{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
exec := &login.Exec{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
LoginAndExec: exec,
Logger: adaptorsLogger,
}
return cmdCmd
}
func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL) adaptors.Cmd {
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
loginLogin := &login.Login{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
exec := &login.Exec{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
LoginAndExec: exec,
Logger: adaptorsLogger,
}
return cmdCmd
}
// di.go:
var usecasesSet = wire.NewSet(login.Login{}, login.Exec{}, wire.Bind((*usecases.Login)(nil), (*login.Login)(nil)), wire.Bind((*usecases.LoginAndExec)(nil), (*login.Exec)(nil)))
var adaptorsSet = wire.NewSet(cmd.Cmd{}, kubeconfig.Kubeconfig{}, oidc.Factory{}, env.Env{}, wire.Bind((*adaptors.Cmd)(nil), (*cmd.Cmd)(nil)), wire.Bind((*adaptors.Kubeconfig)(nil), (*kubeconfig.Kubeconfig)(nil)), wire.Bind((*adaptors.OIDC)(nil), (*oidc.Factory)(nil)), wire.Bind((*adaptors.Env)(nil), (*env.Env)(nil)))
var extraSet = wire.NewSet(login.ShowLocalServerURL{}, wire.Bind((*usecases.LoginShowLocalServerURL)(nil), (*login.ShowLocalServerURL)(nil)), logger.New)

95
docs/google.md Normal file
View File

@@ -0,0 +1,95 @@
# 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
## 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).
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 you.
```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: https://accounts.google.com#1234567890
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials KUBECONTEXT \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
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
```

BIN
docs/keycloak-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

111
docs/keycloak.md Normal file
View File

@@ -0,0 +1,111 @@
# 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"]}`.
## 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).
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 kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials KUBECONTEXT \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://keycloak.example.com/auth/realms/YOUR_REALM
client-id: kubernetes
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
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
```

40
docs/team_ops.md Normal file
View File

@@ -0,0 +1,40 @@
# Team Operation
## kops
Export the kubeconfig.
```sh
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
```
Remove the `admin` access from the kubeconfig.
It should look as like:
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
auth-provider:
name: oidc
config:
client-id: YOUR_CLIEND_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: YOUR_ISSUER
```
You can share the kubeconfig to your team members for easy onboarding.

32
go.mod Normal file
View File

@@ -0,0 +1,32 @@
module github.com/int128/kubelogin
require (
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-test/deep v1.0.1
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.3.1
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/google/wire v0.2.2
github.com/imdario/mergo v0.3.7 // indirect
github.com/int128/oauth2cli v1.4.0
github.com/json-iterator/go v1.1.6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v0.0.4
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190222213804-5cb15d344471 // indirect
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 // indirect
k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.2.0 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
)

130
go.sum Normal file
View File

@@ -0,0 +1,130 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
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-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 v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
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/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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/wire v0.2.2 h1:fSIRzE/K12IaNgV6X0173X/oLrTwHKRiMcFZhiDrN3s=
github.com/google/wire v0.2.2/go.mod h1:7FHVg6mFpFQrjeUZrm+BaD50N5jnDKm50uVPTpyYOmU=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/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.0 h1:Xt4uk2lIb9Mf9Xyd5o43Hf9iV5izb2jYK3zRX/cPgh0=
github.com/int128/oauth2cli v1.4.0/go.mod h1:81pWOyFVt1TRyZ7lZDtZuAGOE/S/jEpb1mpocRopI6U=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE=
github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
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 v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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-20181203042331-505ab145d0a9/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-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 h1:HdqqaWmYAUI7/dmByKKEw+yxDksGSo+9GjkUc9Zp34E=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
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/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.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
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-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v10.0.0+incompatible h1:+xQQxwjrcIPWDMJBAS+1G2FNk1McoPnb53xkvcDiDqE=
k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View File

@@ -1,104 +0,0 @@
package integration
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"html/template"
"log"
"math/big"
"net/http"
"testing"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
// AuthHandler provides the stub handler for OIDC authentication.
type AuthHandler struct {
// Values in templates
Issuer string
AuthCode string
IDToken string
PrivateKey struct{ N, E string }
// Response templates
discoveryJSON *template.Template
tokenJSON *template.Template
jwksJSON *template.Template
}
// NewAuthHandler returns a new AuthHandler.
func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
h := &AuthHandler{
Issuer: issuer,
AuthCode: "0b70006b-f62a-4438-aba5-c0b96775d8e5",
discoveryJSON: template.Must(template.ParseFiles("testdata/oidc-discovery.json")),
tokenJSON: template.Must(template.ParseFiles("testdata/oidc-token.json")),
jwksJSON: template.Must(template.ParseFiles("testdata/oidc-jwks.json")),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
Issuer: h.Issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
})
k, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
t.Fatalf("Could not generate a key pair: %s", err)
}
h.IDToken, err = token.SignedString(k)
if err != nil {
t.Fatalf("Could not generate an ID token: %s", err)
}
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
return h
}
func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
log.Printf("[auth-server] %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := s.discoveryJSON.Execute(w, s); err != nil {
return err
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), s.AuthCode)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
if err := r.ParseForm(); err != nil {
return err
}
if s.AuthCode != r.Form.Get("code") {
return fmt.Errorf("code wants %s but %s", s.AuthCode, r.Form.Get("code"))
}
w.Header().Add("Content-Type", "application/json")
if err := s.tokenJSON.Execute(w, s); err != nil {
return err
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := s.jwksJSON.Execute(w, s); err != nil {
return err
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}
func (s *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := s.serveHTTP(w, r); err != nil {
log.Printf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}

View File

@@ -1,160 +0,0 @@
package integration
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
"github.com/int128/kubelogin/cli"
)
const caCert = "testdata/authserver-ca.crt"
const tlsCert = "testdata/authserver.crt"
const tlsKey = "testdata/authserver.key"
func Test(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "http://localhost:9000"),
}
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "http://localhost:9000",
})
defer os.Remove(kubeconfig)
go listenAndServe(t, authServer)
go authenticate(t, &tls.Config{})
c := cli.CLI{
KubeConfig: kubeconfig,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func TestWithSkipTLSVerify(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
})
defer os.Remove(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{InsecureSkipVerify: true})
c := cli.CLI{
KubeConfig: kubeconfig,
SkipTLSVerify: true,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func TestWithCACert(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: caCert,
})
defer os.Remove(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
c := cli.CLI{
KubeConfig: kubeconfig,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func TestWithCACertData(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
b, err := ioutil.ReadFile(caCert)
if err != nil {
t.Fatal(err)
}
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(b),
})
defer os.Remove(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
c := cli.CLI{
KubeConfig: kubeconfig,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func authenticate(t *testing.T, tlsConfig *tls.Config) {
t.Helper()
time.Sleep(100 * time.Millisecond)
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
res, err := client.Get("http://localhost:8000/")
if err != nil {
t.Error(err)
return
}
if res.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d: res=%+v", res.StatusCode, res)
}
}
func loadCACert(t *testing.T) *x509.CertPool {
p := x509.NewCertPool()
b, err := ioutil.ReadFile(caCert)
if err != nil {
t.Fatal(err)
}
if !p.AppendCertsFromPEM(b) {
t.Fatalf("Could not AppendCertsFromPEM")
}
return p
}
func listenAndServe(t *testing.T, s *http.Server) {
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Fatal(err)
}
}
func listenAndServeTLS(t *testing.T, s *http.Server) {
if err := s.ListenAndServeTLS(tlsCert, tlsKey); err != nil && err != http.ErrServerClosed {
t.Fatal(err)
}
}

View File

@@ -1,44 +0,0 @@
package integration
import (
"html/template"
"io/ioutil"
"strings"
"testing"
)
type kubeconfigValues struct {
Issuer string
IDPCertificateAuthority string
IDPCertificateAuthorityData string
}
func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tpl, err := template.ParseFiles("testdata/kubeconfig.yaml")
if err != nil {
t.Fatal(err)
}
if err := tpl.Execute(f, v); err != nil {
t.Fatal(err)
}
return f.Name()
}
func verifyKubeconfig(t *testing.T, kubeconfig string) {
b, err := ioutil.ReadFile(kubeconfig)
if err != nil {
t.Fatal(err)
}
if strings.Index(string(b), "id-token: ey") == -1 {
t.Errorf("kubeconfig wants id-token but %s", string(b))
}
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
}
}

View File

@@ -1,66 +0,0 @@
package kubeconfig
import (
"fmt"
"k8s.io/client-go/tools/clientcmd/api"
)
// FindCurrentAuthInfo returns the authInfo of current context.
// If the current context does not exist, this returns nil.
func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil
}
return config.AuthInfos[context.AuthInfo]
}
// ToOIDCAuthProviderConfig converts from api.AuthInfo to OIDCAuthProviderConfig.
func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig, error) {
if authInfo.AuthProvider == nil {
return nil, fmt.Errorf("auth-provider is not set, did you setup kubectl as listed here: https://github.com/int128/kubelogin#3-setup-kubectl")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
}
// OIDCAuthProviderConfig represents OIDC configuration in the kubeconfig.
type OIDCAuthProviderConfig api.AuthProviderConfig
// IDPIssuerURL returns the idp-issuer-url.
func (c *OIDCAuthProviderConfig) IDPIssuerURL() string {
return c.Config["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c *OIDCAuthProviderConfig) ClientID() string {
return c.Config["client-id"]
}
// ClientSecret returns the client-secret.
func (c *OIDCAuthProviderConfig) ClientSecret() string {
return c.Config["client-secret"]
}
// IDPCertificateAuthority returns the idp-certificate-authority.
func (c *OIDCAuthProviderConfig) IDPCertificateAuthority() string {
return c.Config["idp-certificate-authority"]
}
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
func (c *OIDCAuthProviderConfig) IDPCertificateAuthorityData() string {
return c.Config["idp-certificate-authority-data"]
}
// SetIDToken replaces the id-token.
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
c.Config["id-token"] = idToken
}
// SetRefreshToken replaces the refresh-token.
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
c.Config["refresh-token"] = refreshToken
}

View File

@@ -1,22 +0,0 @@
package kubeconfig
import (
"fmt"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
// Load loads the file and returns the Config.
func Load(path string) (*api.Config, error) {
config, err := clientcmd.LoadFromFile(path)
if err != nil {
return nil, fmt.Errorf("Could not load kubeconfig from %s: %s", path, err)
}
return config, nil
}
// Write writes the config to the file.
func Write(config *api.Config, path string) error {
return clientcmd.WriteToFile(*config, path)
}

15
kubelogin.rb Normal file
View File

@@ -0,0 +1,15 @@
class Kubelogin < Formula
desc "A kubectl plugin for Kubernetes OpenID Connect authentication"
homepage "https://github.com/int128/kubelogin"
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
version "{{ env "VERSION" }}"
sha256 "{{ sha256 .darwin_amd64_archive }}"
def install
bin.install "kubelogin" => "kubelogin"
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
end
test do
system "#{bin}/kubelogin -h"
system "#{bin}/kubectl-oidc_login -h"
end
end

14
main.go
View File

@@ -2,19 +2,13 @@ package main
import (
"context"
"log"
"os"
"github.com/int128/kubelogin/cli"
"github.com/int128/kubelogin/di"
)
var version = "HEAD"
func main() {
c, err := cli.Parse(os.Args)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := c.Run(ctx); err != nil {
log.Fatal(err)
}
os.Exit(di.NewCmd().Run(context.Background(), os.Args, version))
}

View File

@@ -0,0 +1,27 @@
package kubeconfig
// ContextName represents name of a context.
type ContextName string
// UserName represents name of a user.
type UserName string
// Auth represents the authentication provider,
// i.e. context, user and auth-provider in a kubeconfig.
type Auth struct {
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // Context name (optional)
OIDCConfig OIDCConfig
}
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
}

57
oidc-login.yaml Normal file
View File

@@ -0,0 +1,57 @@
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: oidc-login
spec:
shortDescription: Login for OpenID Connect authentication
description: |
This plugin gets a token from the OIDC provider and writes it to the kubeconfig.
Just run:
% kubectl oidc-login
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
caveats: |
You need to setup the following components:
* OIDC provider
* Kubernetes API server
* Role for your group or user
* kubectl authentication
See https://github.com/int128/kubelogin for more.
homepage: https://github.com/int128/kubelogin
version: {{ env "VERSION" }}
platforms:
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
sha256: "{{ sha256 .linux_amd64_archive }}"
bin: kubelogin
files:
- from: "kubelogin"
to: "."
selector:
matchLabels:
os: linux
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip
sha256: "{{ sha256 .darwin_amd64_archive }}"
bin: kubelogin
files:
- from: "kubelogin"
to: "."
selector:
matchLabels:
os: darwin
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_windows_amd64.zip
sha256: "{{ sha256 .windows_amd64_archive }}"
bin: kubelogin.exe
files:
- from: "kubelogin.exe"
to: "."
selector:
matchLabels:
os: windows
arch: amd64

104
usecases/login/exec.go Normal file
View File

@@ -0,0 +1,104 @@
package login
import (
"context"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
// Exec provide the use case of wrapping the kubectl command.
// If the current auth provider is not oidc, just run kubectl.
// If the kubeconfig has a valid token, just run kubectl.
// Otherwise, update the kubeconfig and run kubectl.
type Exec struct {
Kubeconfig adaptors.Kubeconfig
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Exec) Do(ctx context.Context, in usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
if err := u.doInternal(ctx, in.LoginIn); err != nil {
return nil, errors.WithStack(err)
}
exitCode, err := u.Env.Exec(ctx, in.Executable, in.Args)
if err != nil {
return nil, errors.Wrapf(err, "could not execute kubectl")
}
return &usecases.LoginAndExecOut{ExitCode: exitCode}, nil
}
func (u *Exec) doInternal(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
auth, err := u.Kubeconfig.GetCurrentAuth(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Debugf(1, "The current authentication provider is not oidc: %s", err)
return nil
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
u.Logger.Debugf(1, "A token will be written to %s", auth.LocationOfOrigin)
client, err := u.OIDC.New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not create an OIDC client")
}
if auth.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := client.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig})
if err == nil {
u.Logger.Debugf(1, "You already have a valid token until %s", token.Expiry)
dumpIDToken(u.Logger, token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
var tokenSet *adaptors.OIDCAuthenticateOut
if in.Username != "" {
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return errors.Wrapf(err, "could not read a password")
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: in.Username,
Password: in.Password,
})
if err != nil {
return errors.Wrapf(err, "error while the resource owner password credentials grant flow")
}
tokenSet = out
} else {
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return errors.Wrapf(err, "error while the authorization code grant flow")
}
tokenSet = out
}
u.Logger.Printf("You got a valid token until %s", tokenSet.VerifiedIDToken.Expiry)
dumpIDToken(u.Logger, tokenSet.VerifiedIDToken)
auth.OIDCConfig.IDToken = tokenSet.IDToken
auth.OIDCConfig.RefreshToken = tokenSet.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", auth.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuth(auth); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
}
return nil
}

View File

@@ -0,0 +1,98 @@
package login
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
func TestExec_Do(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: []int{10000},
}), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
u := Exec{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: []int{10000},
},
Executable: "kubectl",
Args: []string{"foo", "bar"},
})
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
if out.ExitCode != 0 {
t.Errorf("ExitCode wants 0 but %d", out.ExitCode)
}
})
t.Run("NoOIDCConfig", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, errors.New("no oidc config"))
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
u := Exec{
Kubeconfig: mockKubeconfig,
OIDC: mock_adaptors.NewMockOIDC(ctrl),
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: []int{10000},
},
Executable: "kubectl",
Args: []string{"foo", "bar"},
})
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
if out.ExitCode != 0 {
t.Errorf("ExitCode wants 0 but %d", out.ExitCode)
}
})
}

122
usecases/login/login.go Normal file
View File

@@ -0,0 +1,122 @@
package login
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
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 passwordPrompt = "Password: "
// Login provides the use case of login to the provider.
// If the current auth provider is not oidc, show the error.
// If the kubeconfig has a valid token, do nothing.
// Otherwise, update the kubeconfig.
type Login struct {
Kubeconfig adaptors.Kubeconfig
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
auth, err := u.Kubeconfig.GetCurrentAuth(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage)
return errors.Wrapf(err, "could not find the current authentication provider")
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
u.Logger.Debugf(1, "A token will be written to %s", auth.LocationOfOrigin)
client, err := u.OIDC.New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not create an OIDC client")
}
if auth.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := client.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig})
if err == nil {
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
dumpIDToken(u.Logger, token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
var tokenSet *adaptors.OIDCAuthenticateOut
if in.Username != "" {
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return errors.Wrapf(err, "could not read a password")
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: in.Username,
Password: in.Password,
})
if err != nil {
return errors.Wrapf(err, "error while the resource owner password credentials grant flow")
}
tokenSet = out
} else {
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return errors.Wrapf(err, "error while the authorization code grant flow")
}
tokenSet = out
}
u.Logger.Printf("You got a valid token until %s", tokenSet.VerifiedIDToken.Expiry)
dumpIDToken(u.Logger, tokenSet.VerifiedIDToken)
auth.OIDCConfig.IDToken = tokenSet.IDToken
auth.OIDCConfig.RefreshToken = tokenSet.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", auth.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuth(auth); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
}
return nil
}
func dumpIDToken(logger adaptors.Logger, token *oidc.IDToken) {
var claims map[string]interface{}
if err := token.Claims(&claims); err != nil {
logger.Debugf(1, "Error while inspection of the ID token: %s", err)
}
for k, v := range claims {
logger.Debugf(1, "The ID token has the claim: %s=%v", k, v)
}
}
// ShowLocalServerURL just shows the URL of local server to console.
type ShowLocalServerURL struct {
Logger adaptors.Logger
}
func (s *ShowLocalServerURL) ShowLocalServerURL(url string) {
s.Logger.Printf("Open %s for authentication", url)
}

View File

@@ -0,0 +1,264 @@
package login
import (
"context"
"testing"
"time"
"github.com/coreos/go-oidc"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
func newMockCodeOIDC(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) *mock_adaptors.MockOIDCClient {
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, in).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDCClient
}
func newMockPasswordOIDC(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) *mock_adaptors.MockOIDCClient {
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, in).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDCClient
}
func newAuth(idToken, refreshToken string) *kubeconfig.Auth {
return &kubeconfig.Auth{
LocationOfOrigin: "theLocation",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: idToken,
RefreshToken: refreshToken,
},
}
}
func TestLogin_Do(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: []int{10000},
}), nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(newMockPasswordOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: "USER",
Password: "PASS",
}), nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockPasswordOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: "USER",
Password: "PASS",
}), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Env: mockEnv,
}
if err := u.Do(ctx, usecases.LoginIn{
Username: "USER",
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mock_adaptors.NewMockOIDCClient(ctrl), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", errors.New("error"))
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Env: mockEnv,
}
if err := u.Do(ctx, usecases.LoginIn{
Username: "USER",
}); err == nil {
t.Errorf("err wants an error but nil")
}
})
t.Run("KubeconfigHasValidToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("VALID_ID_TOKEN", "N/A")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig}).
Return(&oidc.IDToken{Expiry: time.Now()}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mockOIDCClient, nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeconfigHasExpiredToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("EXPIRED_ID_TOKEN", "EXPIRED_REFRESH_TOKEN")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDCClient := newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{Config: auth.OIDCConfig})
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig}).
Return(nil, errors.New("token expired"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mockOIDCClient, nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
}

View File

@@ -0,0 +1,83 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
usecases "github.com/int128/kubelogin/usecases"
reflect "reflect"
)
// MockLogin is a mock of Login interface
type MockLogin struct {
ctrl *gomock.Controller
recorder *MockLoginMockRecorder
}
// MockLoginMockRecorder is the mock recorder for MockLogin
type MockLoginMockRecorder struct {
mock *MockLogin
}
// NewMockLogin creates a new mock instance
func NewMockLogin(ctrl *gomock.Controller) *MockLogin {
mock := &MockLogin{ctrl: ctrl}
mock.recorder = &MockLoginMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLogin) EXPECT() *MockLoginMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockLogin) Do(arg0 context.Context, arg1 usecases.LoginIn) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Do indicates an expected call of Do
func (mr *MockLoginMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLogin)(nil).Do), arg0, arg1)
}
// MockLoginAndExec is a mock of LoginAndExec interface
type MockLoginAndExec struct {
ctrl *gomock.Controller
recorder *MockLoginAndExecMockRecorder
}
// MockLoginAndExecMockRecorder is the mock recorder for MockLoginAndExec
type MockLoginAndExecMockRecorder struct {
mock *MockLoginAndExec
}
// NewMockLoginAndExec creates a new mock instance
func NewMockLoginAndExec(ctrl *gomock.Controller) *MockLoginAndExec {
mock := &MockLoginAndExec{ctrl: ctrl}
mock.recorder = &MockLoginAndExecMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLoginAndExec) EXPECT() *MockLoginAndExecMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockLoginAndExec) Do(arg0 context.Context, arg1 usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*usecases.LoginAndExecOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockLoginAndExecMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLoginAndExec)(nil).Do), arg0, arg1)
}

45
usecases/usecases.go Normal file
View File

@@ -0,0 +1,45 @@
package usecases
import (
"context"
"github.com/int128/kubelogin/models/kubeconfig"
)
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec
type Login interface {
Do(ctx context.Context, in LoginIn) error
}
type LoginIn 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
SkipTLSVerify bool
}
// LoginShowLocalServerURL provides an interface to notify the URL of local server.
// It is needed for the end-to-end tests.
type LoginShowLocalServerURL interface {
ShowLocalServerURL(url string)
}
type LoginAndExec interface {
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
}
type LoginAndExecIn struct {
LoginIn LoginIn
Executable string
Args []string
}
type LoginAndExecOut struct {
ExitCode int
}