Compare commits

...

96 Commits
1.1 ... v1.10.0

Author SHA1 Message Date
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
Hidetake Iwata
b7bbcd44e1 Merge pull request #9 from int128/oidc-ca
Add support of OIDC CA certificate
2018-08-24 14:05:21 +09:00
Hidetake Iwata
7726ac6c51 Add support of OIDC CA certificate 2018-08-24 14:02:42 +09:00
Hidetake Iwata
adaeba4c24 Refactor 2018-08-22 13:37:12 +09:00
Hidetake Iwata
e8acaa28b3 Refactor 2018-08-22 13:28:51 +09:00
Hidetake Iwata
031f9fb81a Merge pull request #6 from int128/integration-test
Add integration test
2018-08-22 12:52:09 +09:00
Hidetake Iwata
8a7da83338 Add integration test 2018-08-22 12:48:48 +09:00
Hidetake Iwata
b776bac764 Add insecure-skip-tls-verify option 2018-08-16 09:34:12 +09:00
Hidetake Iwata
4bf77886a8 Introduce flags 2018-08-15 19:12:59 +09:00
Hidetake Iwata
ea711f91b4 Update README.md 2018-08-14 16:33:00 +09:00
Hidetake Iwata
cfc6376f69 Refactor README.md 2018-08-14 10:56:30 +09:00
51 changed files with 3213 additions and 460 deletions

View File

@@ -2,35 +2,36 @@ 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:
- 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: |
go get -v \
golang.org/x/lint/golint \
github.com/int128/goxzst \
github.com/tcnksm/ghr \
github.com/int128/ghcp
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -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: 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
# workaround for https://github.com/golang/go/issues/27925
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
- run: make check
- 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,11 +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

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
/.idea
/dist
/kubelogin
/kubectl-oidc_login
/.kubeconfig

36
Makefile Normal file
View File

@@ -0,0 +1,36 @@
TARGET := kubelogin
TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
.PHONY: check run release clean
all: $(TARGET)
check:
golint
go vet
$(MAKE) -C adaptors_test/keys/testdata
go test -v -race ./...
$(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/

207
README.md
View File

@@ -1,153 +1,140 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/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).
It gets a token from the OIDC provider and writes it to the kubeconfig.
## Getting Started
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it as `/usr/local/bin/kubelogin`.
You need to setup the following components:
You have to configure `kubectl` to authenticate with OIDC.
See the later section for details.
- OIDC provider
- Kubernetes API server
- Role for your group or user
- kubectl authentication
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://keycloak.example.com/auth/realms/hello \
--auth-provider-arg client-id=kubernetes \
--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.9.1/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
Run `kubelogin`.
After initial setup or when the token has been expired, just run:
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: devops.hidetake.org
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
Using current-context: hello.k8s.local
Open http://localhost:8000 for authorization
Got a token for subject 0123456789 (valid until 2019-04-12 11:00:49 +0900 JST)
Updated ~/.kube/config
```
Now your `~/.kube/config` looks like:
```yaml
# ~/.kube/config (snip)
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://keycloak.example.com/auth/realms/hello
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:
or run as a kubectl plugin:
```
% kubectl version
Client Version: version.Info{...}
Server Version: version.Info{...}
% 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.
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)
## Configuration
You can set the following environment variable:
This supports the following options.
- `KUBECONFIG` - Path to the config. Defaults to `~/.kube/config`.
```
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
--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]
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
-v, --v= If set to 1 or greater, show debug log (default: 0)
## Prerequisite
You have to setup your OIDC identity provider and Kubernetes cluster.
### 1. Setup OIDC Identity Provider
This tutorial assumes you have created an OIDC client with the following:
- Issuer URL: `https://keycloak.example.com/auth/realms/hello`
- Client ID: `kubernetes`
- Client Secret: `YOUR_CLIENT_SECRET`
- Allowed redirect URLs: `http://localhost:8000/`
- Groups claim: `groups` (optional for group based access controll)
### 2. Setup Kubernetes API Server
Configure the Kubernetes API server allows your identity provider.
If you are using [kops](https://github.com/kubernetes/kops), `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcClientID: kubernetes
oidcGroupsClaim: groups
oidcIssuerURL: https://keycloak.example.com/auth/realms/hello
Help Options:
-h, --help Show this help message
```
### 3. Setup kubectl
This also supports the following keys of `auth-provider` in kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
Run the following command to configure `kubectl` to authenticate by your identity provider.
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 path
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
export KUBECONFIG="$PWD/.kubeconfig"
```
In actual team operation, you can share the following config to your team members for easy setup.
```yaml
#!/bin/sh
CLUSTER_NAME="hello.k8s.local"
### Extra scopes
# Set the certificate
mkdir -p "$HOME/.kube"
cat > "$HOME/.kube/$CLUSTER_NAME.crt" <<EOF
-----BEGIN CERTIFICATE-----
MII...
-----END CERTIFICATE-----
EOF
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
# Set the cluster
kubectl config set-cluster "$CLUSTER_NAME" \
--server https://api-xxx.xxx.elb.amazonaws.com \
--certificate-authority "$HOME/.kube/$CLUSTER_NAME.crt"
# Set the credentials
kubectl config set-credentials "$CLUSTER_NAME" \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_SECRET
# Set the context
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
# Set the current context
kubectl config use-context "$CLUSTER_NAME"
```sh
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 `idp-certificate-authority` and `idp-certificate-authority-data` in the kubeconfig.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```
If kubelogin could not parse the certificate, it shows a warning and skips it.
### 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.

75
adaptors/cmd.go Normal file
View File

@@ -0,0 +1,75 @@
package adaptors
import (
"context"
"fmt"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"go.uber.org/dig"
)
func NewCmd(i Cmd) adaptors.Cmd {
return &i
}
type Cmd struct {
dig.In
Login usecases.Login
Logger adaptors.Logger
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var o cmdOptions
parser := flags.NewParser(&o, flags.HelpFlag)
parser.LongDescription = fmt.Sprintf(`Version %s
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
version)
args, err := parser.ParseArgs(args[1:])
if err != nil {
cmd.Logger.Printf("Error: %s", err)
return 1
}
if len(args) > 0 {
cmd.Logger.Printf("Error: too many arguments")
return 1
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
kubeConfig, err := o.ExpandKubeConfig()
if err != nil {
cmd.Logger.Printf("Error: invalid option: %s", err)
return 1
}
in := usecases.LoginIn{
KubeConfig: kubeConfig,
ListenPort: o.ListenPort,
SkipTLSVerify: o.SkipTLSVerify,
SkipOpenBrowser: o.SkipOpenBrowser,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("Error: %s", err)
return 1
}
return 0
}
type cmdOptions struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
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"`
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
Verbose int `long:"v" short:"v" default:"0" description:"If set to 1 or greater, show debug log"`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *cmdOptions) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig)
}
return d, nil
}

98
adaptors/cmd_test.go Normal file
View File

@@ -0,0 +1,98 @@
package adaptors
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/mitchellh/go-homedir"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("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{
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 8000,
})
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("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{
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 10080,
SkipTLSVerify: true,
SkipOpenBrowser: true,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"--listen-port", "10080",
"--insecure-skip-tls-verify",
"--skip-open-browser",
"-v1",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
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)
}
})
}
func expand(t *testing.T, path string) string {
d, err := homedir.Expand(path)
if err != nil {
t.Fatalf("could not expand: %s", err)
}
return d
}

82
adaptors/http.go Normal file
View File

@@ -0,0 +1,82 @@
package adaptors
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/infrastructure"
"github.com/pkg/errors"
"go.uber.org/dig"
)
func NewHTTP(i HTTP) adaptors.HTTP {
return &i
}
type HTTP struct {
dig.In
Logger adaptors.Logger
}
func (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
return &httpClientConfig{
certPool: x509.NewCertPool(),
}
}
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
return &http.Client{
Transport: &infrastructure.LoggingTransport{
Base: &http.Transport{
TLSClientConfig: config.TLSConfig(),
Proxy: http.ProxyFromEnvironment,
},
Logger: h.Logger,
},
}, nil
}
type httpClientConfig struct {
certPool *x509.CertPool
skipTLSVerify bool
}
func (c *httpClientConfig) AddCertificateFromFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "could not read %s", filename)
}
if c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func (c *httpClientConfig) AddEncodedCertificate(base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return errors.Wrapf(err, "could not decode base64")
}
if c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate")
}
return nil
}
func (c *httpClientConfig) TLSConfig() *tls.Config {
tlsConfig := &tls.Config{
InsecureSkipVerify: c.skipTLSVerify,
}
if len(c.certPool.Subjects()) > 0 {
tlsConfig.RootCAs = c.certPool
}
return tlsConfig
}
func (c *httpClientConfig) SetSkipTLSVerify(b bool) {
c.skipTLSVerify = b
}

View File

@@ -0,0 +1,82 @@
package adaptors
import (
"context"
"crypto/tls"
"net/http"
"github.com/coreos/go-oidc"
"k8s.io/client-go/tools/clientcmd/api"
)
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type KubeConfig interface {
LoadFromFile(filename string) (*api.Config, error)
WriteToFile(config *api.Config, filename string) error
}
type HTTP interface {
NewClientConfig() HTTPClientConfig
NewClient(config HTTPClientConfig) (*http.Client, error)
}
type HTTPClientConfig interface {
AddCertificateFromFile(filename string) error
AddEncodedCertificate(base64String string) error
SetSkipTLSVerify(b bool)
TLSConfig() *tls.Config
}
type OIDC interface {
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
VerifyIDToken(ctx context.Context, in OIDCVerifyTokenIn) (*oidc.IDToken, error)
}
type OIDCAuthenticateIn struct {
Issuer string
ClientID string
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
type OIDCAuthenticateCallback struct {
ShowLocalServerURL func(url string)
}
type OIDCAuthenticateOut struct {
VerifiedIDToken *oidc.IDToken
IDToken string
RefreshToken string
}
type OIDCVerifyTokenIn struct {
IDToken string
Issuer string
ClientID string
Client *http.Client
}
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

30
adaptors/kubeconfig.go Normal file
View File

@@ -0,0 +1,30 @@
package adaptors
import (
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func NewKubeConfig() adaptors.KubeConfig {
return &KubeConfig{}
}
type KubeConfig struct{}
func (*KubeConfig) LoadFromFile(filename string) (*api.Config, error) {
config, err := clientcmd.LoadFromFile(filename)
if err != nil {
return nil, errors.Wrapf(err, "could not read the kubeconfig from %s", filename)
}
return config, err
}
func (*KubeConfig) WriteToFile(config *api.Config, filename string) error {
err := clientcmd.WriteToFile(*config, filename)
if err != nil {
return errors.Wrapf(err, "could not write the kubeconfig to %s", filename)
}
return err
}

49
adaptors/logger.go Normal file
View File

@@ -0,0 +1,49 @@
package adaptors
import (
"log"
"os"
"github.com/int128/kubelogin/adaptors/interfaces"
)
// NewLogger returns a Logger with the standard log.Logger for messages and debug.
func NewLogger() adaptors.Logger {
return &Logger{
stdLogger: log.New(os.Stderr, "", 0),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
}
}
// NewLoggerWith returns a Logger with the given standard log.Logger.
func NewLoggerWith(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
}

53
adaptors/logger_test.go Normal file
View File

@@ -0,0 +1,53 @@
package adaptors
import (
"fmt"
"testing"
"github.com/int128/kubelogin/adaptors/interfaces"
)
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/interfaces"
)
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,305 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
import (
context "context"
tls "crypto/tls"
go_oidc "github.com/coreos/go-oidc"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
api "k8s.io/client-go/tools/clientcmd/api"
http "net/http"
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
}
// LoadFromFile mocks base method
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*api.Config, error) {
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
ret0, _ := ret[0].(*api.Config)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadFromFile indicates an expected call of LoadFromFile
func (mr *MockKubeConfigMockRecorder) LoadFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadFromFile", reflect.TypeOf((*MockKubeConfig)(nil).LoadFromFile), arg0)
}
// WriteToFile mocks base method
func (m *MockKubeConfig) WriteToFile(arg0 *api.Config, arg1 string) error {
ret := m.ctrl.Call(m, "WriteToFile", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// WriteToFile indicates an expected call of WriteToFile
func (mr *MockKubeConfigMockRecorder) WriteToFile(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteToFile", reflect.TypeOf((*MockKubeConfig)(nil).WriteToFile), arg0, arg1)
}
// MockHTTP is a mock of HTTP interface
type MockHTTP struct {
ctrl *gomock.Controller
recorder *MockHTTPMockRecorder
}
// MockHTTPMockRecorder is the mock recorder for MockHTTP
type MockHTTPMockRecorder struct {
mock *MockHTTP
}
// NewMockHTTP creates a new mock instance
func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP {
mock := &MockHTTP{ctrl: ctrl}
mock.recorder = &MockHTTPMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder {
return m.recorder
}
// NewClient mocks base method
func (m *MockHTTP) NewClient(arg0 interfaces.HTTPClientConfig) (*http.Client, error) {
ret := m.ctrl.Call(m, "NewClient", arg0)
ret0, _ := ret[0].(*http.Client)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewClient indicates an expected call of NewClient
func (mr *MockHTTPMockRecorder) NewClient(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockHTTP)(nil).NewClient), arg0)
}
// NewClientConfig mocks base method
func (m *MockHTTP) NewClientConfig() interfaces.HTTPClientConfig {
ret := m.ctrl.Call(m, "NewClientConfig")
ret0, _ := ret[0].(interfaces.HTTPClientConfig)
return ret0
}
// NewClientConfig indicates an expected call of NewClientConfig
func (mr *MockHTTPMockRecorder) NewClientConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientConfig", reflect.TypeOf((*MockHTTP)(nil).NewClientConfig))
}
// MockHTTPClientConfig is a mock of HTTPClientConfig interface
type MockHTTPClientConfig struct {
ctrl *gomock.Controller
recorder *MockHTTPClientConfigMockRecorder
}
// MockHTTPClientConfigMockRecorder is the mock recorder for MockHTTPClientConfig
type MockHTTPClientConfigMockRecorder struct {
mock *MockHTTPClientConfig
}
// NewMockHTTPClientConfig creates a new mock instance
func NewMockHTTPClientConfig(ctrl *gomock.Controller) *MockHTTPClientConfig {
mock := &MockHTTPClientConfig{ctrl: ctrl}
mock.recorder = &MockHTTPClientConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTPClientConfig) EXPECT() *MockHTTPClientConfigMockRecorder {
return m.recorder
}
// AddCertificateFromFile mocks base method
func (m *MockHTTPClientConfig) AddCertificateFromFile(arg0 string) error {
ret := m.ctrl.Call(m, "AddCertificateFromFile", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddCertificateFromFile indicates an expected call of AddCertificateFromFile
func (mr *MockHTTPClientConfigMockRecorder) AddCertificateFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertificateFromFile", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddCertificateFromFile), arg0)
}
// AddEncodedCertificate mocks base method
func (m *MockHTTPClientConfig) AddEncodedCertificate(arg0 string) error {
ret := m.ctrl.Call(m, "AddEncodedCertificate", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddEncodedCertificate indicates an expected call of AddEncodedCertificate
func (mr *MockHTTPClientConfigMockRecorder) AddEncodedCertificate(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEncodedCertificate", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddEncodedCertificate), arg0)
}
// SetSkipTLSVerify mocks base method
func (m *MockHTTPClientConfig) SetSkipTLSVerify(arg0 bool) {
m.ctrl.Call(m, "SetSkipTLSVerify", arg0)
}
// SetSkipTLSVerify indicates an expected call of SetSkipTLSVerify
func (mr *MockHTTPClientConfigMockRecorder) SetSkipTLSVerify(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSkipTLSVerify", reflect.TypeOf((*MockHTTPClientConfig)(nil).SetSkipTLSVerify), arg0)
}
// TLSConfig mocks base method
func (m *MockHTTPClientConfig) TLSConfig() *tls.Config {
ret := m.ctrl.Call(m, "TLSConfig")
ret0, _ := ret[0].(*tls.Config)
return ret0
}
// TLSConfig indicates an expected call of TLSConfig
func (mr *MockHTTPClientConfigMockRecorder) TLSConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TLSConfig", reflect.TypeOf((*MockHTTPClientConfig)(nil).TLSConfig))
}
// 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
}
// Authenticate mocks base method
func (m *MockOIDC) Authenticate(arg0 context.Context, arg1 interfaces.OIDCAuthenticateIn, arg2 interfaces.OIDCAuthenticateCallback) (*interfaces.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1, arg2)
ret0, _ := ret[0].(*interfaces.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authenticate indicates an expected call of Authenticate
func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1, arg2)
}
// VerifyIDToken mocks base method
func (m *MockOIDC) VerifyIDToken(arg0 context.Context, arg1 interfaces.OIDCVerifyTokenIn) (*go_oidc.IDToken, error) {
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
ret0, _ := ret[0].(*go_oidc.IDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyIDToken indicates an expected call of VerifyIDToken
func (mr *MockOIDCMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockOIDC)(nil).VerifyIDToken), arg0, arg1)
}
// 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 interfaces.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 interfaces.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 interfaces.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)
}

73
adaptors/oidc.go Normal file
View File

@@ -0,0 +1,73 @@
package adaptors
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
func NewOIDC() adaptors.OIDC {
return &OIDC{}
}
type OIDC struct{}
func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) (*adaptors.OIDCAuthenticateOut, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
Scopes: append(in.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
ShowLocalServerURL: cb.ShowLocalServerURL,
}
token, err := flow.GetToken(ctx)
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.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 (*OIDC) VerifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) (*oidc.IDToken, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return verifiedIDToken, nil
}

View File

@@ -0,0 +1,38 @@
package authserver
import (
"crypto/rsa"
"net/http"
"testing"
)
// Config represents server configuration.
type Config struct {
Issuer string
Scope string
TLSServerCert string
TLSServerKey string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
}
// Start starts a HTTP server.
func Start(t *testing.T, c Config) *http.Server {
s := &http.Server{
Addr: "localhost:9000",
Handler: newHandler(t, c),
}
go func() {
var err error
if c.TLSServerCert != "" && c.TLSServerKey != "" {
err = s.ListenAndServeTLS(c.TLSServerCert, c.TLSServerKey)
} else {
err = s.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return s
}

View File

@@ -0,0 +1,108 @@
package authserver
import (
"encoding/base64"
"fmt"
"math/big"
"net/http"
"testing"
"text/template"
"github.com/pkg/errors"
)
type handler struct {
t *testing.T
discovery *template.Template
token *template.Template
jwks *template.Template
authCode string
// Template values
Issuer string
Scope string // Default to openid
IDToken string
RefreshToken string
PrivateKey struct{ N, E string }
}
func newHandler(t *testing.T, c Config) *handler {
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)
}
h := handler{
t: t,
discovery: tpl.Lookup("oidc-discovery.json"),
token: tpl.Lookup("oidc-token.json"),
jwks: tpl.Lookup("oidc-jwks.json"),
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
Issuer: c.Issuer,
Scope: c.Scope,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
}
if h.Scope == "" {
h.Scope = "openid"
}
if c.IDTokenKeyPair != nil {
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("[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 := h.discovery.Execute(w, h); 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.Scope != q.Get("scope") {
return errors.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.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 errors.Wrapf(err, "could not parse the form")
}
if h.authCode != r.Form.Get("code") {
return errors.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
}
w.Header().Add("Content-Type", "application/json")
if err := h.token.Execute(w, h); 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.jwks.Execute(w, h); 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,85 @@
{
"issuer": "{{ .Issuer }}",
"authorization_endpoint": "{{ .Issuer }}/protocol/openid-connect/auth",
"token_endpoint": "{{ .Issuer }}/protocol/openid-connect/token",
"token_introspection_endpoint": "{{ .Issuer }}/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "{{ .Issuer }}/protocol/openid-connect/userinfo",
"end_session_endpoint": "{{ .Issuer }}/protocol/openid-connect/logout",
"jwks_uri": "{{ .Issuer }}/protocol/openid-connect/certs",
"check_session_iframe": "{{ .Issuer }}/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials"
],
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"subject_types_supported": [
"public",
"pairwise"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"userinfo_signing_alg_values_supported": [
"RS256"
],
"request_object_signing_alg_values_supported": [
"none",
"RS256"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"registration_endpoint": "{{ .Issuer }}/clients-registrations/openid-connect",
"token_endpoint_auth_methods_supported": [
"private_key_jwt",
"client_secret_basic",
"client_secret_post",
"client_secret_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256"
],
"claims_supported": [
"sub",
"iss",
"auth_time",
"name",
"given_name",
"family_name",
"preferred_username",
"email"
],
"claim_types_supported": [
"normal"
],
"claims_parameter_supported": false,
"scopes_supported": [
"openid",
"offline_access",
"phone",
"address",
"email",
"profile"
],
"request_parameter_supported": true,
"request_uri_parameter_supported": true,
"code_challenge_methods_supported": [
"plain",
"S256"
],
"tls_client_certificate_bound_access_tokens": true
}

View File

@@ -0,0 +1,12 @@
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "xxx",
"n": "{{ .PrivateKey.N }}",
"e": "{{ .PrivateKey.E }}"
}
]
}

View File

@@ -0,0 +1,7 @@
{
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
"token_type": "Bearer",
"refresh_token": "{{ .RefreshToken }}",
"expires_in": 3600,
"id_token": "{{ .IDToken }}"
}

222
adaptors_test/cmd_test.go Normal file
View File

@@ -0,0 +1,222 @@
package adaptors_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/adaptors/interfaces"
"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 at port 9000.
// 2. Run the Cmd.
// 3. Open a request for port 8000.
// 4. Wait for the Cmd.
// 5. Shutdown the auth server.
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("NoTLS", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9000")
serverConfig := authserver.Config{
Issuer: "http://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "http://localhost:9000",
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9000")
serverConfig := authserver.Config{
Issuer: "http://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "http://localhost:9000",
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACert", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9000")
serverConfig := authserver.Config{
Issuer: "https://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
TLSServerCert: keys.TLSServerCert,
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACertData", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9000")
serverConfig := authserver.Config{
Issuer: "https://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
TLSServerCert: keys.TLSServerCert,
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
serverConfig := authserver.Config{
Issuer: "http://localhost:9000",
IDTokenKeyPair: keys.JWSKeyPair,
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
idToken := newIDToken(t, "http://localhost:9000")
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "http://localhost:9000",
IDToken: idToken,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
})
})
}
func newIDToken(t *testing.T, issuer string) string {
t.Helper()
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
})
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, args ...string) {
t.Helper()
newLogger := func() adaptors.Logger {
return logger.New(t)
}
if err := di.InvokeWithExtra(func(cmd adaptors.Cmd) {
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}, newLogger); err != nil {
t.Errorf("Invoke returned error: %+v", err)
}
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) {
t.Helper()
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", "http://localhost:8000/", nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
go func() {
time.Sleep(50 * time.Millisecond)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}()
}

View File

@@ -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

@@ -0,0 +1,4 @@
/CA
*.key
*.csr
*.crt

53
adaptors_test/keys/testdata/Makefile vendored Normal file
View File

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

37
adaptors_test/keys/testdata/openssl.cnf vendored Normal file
View File

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

View File

@@ -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,34 @@
apiVersion: v1
kind: Config
clusters:
- cluster:
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:
config:
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"
)
func New(t testingLogger) *adaptors.Logger {
return adaptors.NewLoggerWith(&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,61 +0,0 @@
package authn
import (
"context"
"fmt"
oidc "github.com/coreos/go-oidc"
"github.com/int128/kubelogin/authz"
"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 := authz.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,12 +0,0 @@
package authz
import (
"context"
"golang.org/x/oauth2"
)
// Flow represents an authorization method.
type Flow interface {
GetToken(context.Context) (*oauth2.Token, error)
}

View File

@@ -1,97 +0,0 @@
package authz
import (
"context"
"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 := generateOAuthState()
if err != nil {
return nil, 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 (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,35 +0,0 @@
package authz
import (
"context"
"fmt"
"log"
"golang.org/x/oauth2"
)
// CLIAuthCodeFlow is a flow to get a token by keyboard interaction.
type CLIAuthCodeFlow struct {
oauth2.Config
}
// GetToken returns a token by browser interaction.
func (f *CLIAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
f.Config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
state, err := generateOAuthState()
if err != nil {
return nil, err
}
authCodeURL := f.Config.AuthCodeURL(state)
log.Printf("Open %s for authorization", authCodeURL)
fmt.Print("Enter code: ")
var code string
if _, err := fmt.Scanln(&code); 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
}

View File

@@ -1,15 +0,0 @@
package authz
import (
"crypto/rand"
"encoding/binary"
"fmt"
)
func generateOAuthState() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", err
}
return fmt.Sprintf("%x", n), nil
}

42
di/di.go Normal file
View File

@@ -0,0 +1,42 @@
// Package di provides dependency injection.
package di
import (
"github.com/int128/kubelogin/adaptors"
adaptorsInterfaces "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"go.uber.org/dig"
)
var constructors = []interface{}{
usecases.NewLogin,
adaptors.NewCmd,
adaptors.NewKubeConfig,
adaptors.NewOIDC,
adaptors.NewHTTP,
}
var extraConstructors = []interface{}{
adaptors.NewLogger,
}
// Invoke runs the function with the default constructors.
func Invoke(f func(cmd adaptorsInterfaces.Cmd)) error {
return InvokeWithExtra(f, extraConstructors...)
}
// InvokeWithExtra runs the function with the given constructors.
func InvokeWithExtra(f func(cmd adaptorsInterfaces.Cmd), extra ...interface{}) error {
c := dig.New()
for _, constructor := range append(constructors, extra...) {
if err := c.Provide(constructor); err != nil {
return errors.Wrapf(err, "could not provide the constructor")
}
}
if err := c.Invoke(f); err != nil {
return errors.Wrapf(err, "could not invoke")
}
return nil
}

18
di/di_test.go Normal file
View File

@@ -0,0 +1,18 @@
package di_test
import (
"testing"
adaptors "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
func TestInvoke(t *testing.T) {
if err := di.Invoke(func(cmd adaptors.Cmd) {
if cmd == nil {
t.Errorf("cmd wants non-nil but nil")
}
}); err != nil {
t.Fatalf("Invoke returned error: %+v", err)
}
}

100
docs/google.md Normal file
View File

@@ -0,0 +1,100 @@
# 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).
### kops
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 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
```
## 5. Run kubelogin
Run `kubelogin`.
```
% 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
```
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
```

107
docs/keycloak.md Normal file
View File

@@ -0,0 +1,107 @@
# Getting Started with Keycloak
## Prerequisite
- You have administrator access to the Keycloak.
- 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 Keycloak
Open the Keycloak and 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`
Create a group `kubernetes:admin` and join to it.
This is used for group based access control.
## 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).
### kops
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
```
## 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 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
```
## 5. Run kubelogin
Run `kubelogin`.
```
% 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
```
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.

35
go.mod Normal file
View File

@@ -0,0 +1,35 @@
module github.com/int128/kubelogin
require (
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.2.0
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/int128/oauth2cli v1.2.1
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.6 // indirect
github.com/mitchellh/go-homedir v1.1.0
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/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.3.0 // indirect
go.uber.org/dig v1.7.0
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
golang.org/x/net v0.0.0-20190328230028-74de082e2cca // indirect
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
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
)

88
go.sum Normal file
View File

@@ -0,0 +1,88 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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/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/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.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
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/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/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/int128/oauth2cli v1.1.0 h1:qAT6C8GyaLaSf0aseQUTcJvZ+j2MueETzGkpoFow0kc=
github.com/int128/oauth2cli v1.1.0/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
github.com/int128/oauth2cli v1.2.1 h1:rhYQ++Kijz/sleAfzy2u2qEsQJCQSHVYjANgOM/LfLA=
github.com/int128/oauth2cli v1.2.1/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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/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/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
go.uber.org/dig v1.7.0/go.mod h1:z+dSd2TP9Usi48jL8M3v63iSBVkiwtVyMKxMZYYauPg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/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-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/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=
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=

50
infrastructure/http.go Normal file
View File

@@ -0,0 +1,50 @@
package infrastructure
import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/adaptors/interfaces"
)
const (
logLevelDumpHeaders = 2
logLevelDumpBody = 3
)
type LoggingTransport struct {
Base http.RoundTripper
Logger adaptors.Logger
}
func (t *LoggingTransport) 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 *LoggingTransport) IsDumpEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpHeaders)
}
func (t *LoggingTransport) IsDumpBodyEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpBody)
}

View File

@@ -0,0 +1,90 @@
package infrastructure
import (
"bufio"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"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 := &LoggingTransport{
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 := &LoggingTransport{
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 := &LoggingTransport{
Logger: logger,
}
if transport.IsDumpBodyEnabled() != true {
t.Errorf("IsDumpBodyEnabled wants true")
}
}

View File

@@ -1,52 +1,80 @@
package kubeconfig
import (
"fmt"
"strings"
"github.com/pkg/errors"
"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 {
// FindOIDCAuthProvider returns the current OIDC authProvider.
// If the context, auth-info or auth-provider does not exist, this returns an error.
// If auth-provider is not "oidc", this returns an error.
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil
return nil, errors.Errorf("context %s does not exist", config.CurrentContext)
}
authInfo := config.AuthInfos[context.AuthInfo]
if authInfo == nil {
return nil, errors.Errorf("auth-info %s does not exist", context.AuthInfo)
}
return config.AuthInfos[context.AuthInfo]
}
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")
return nil, errors.Errorf("auth-provider is not set")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
return nil, errors.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
}
type OIDCAuthProviderConfig api.AuthProviderConfig
// OIDCAuthProvider represents OIDC configuration in the kubeconfig.
type OIDCAuthProvider api.AuthProviderConfig
// IDPIssuerURL returns the idp-issuer-url.
func (c *OIDCAuthProviderConfig) IDPIssuerURL() string {
func (c *OIDCAuthProvider) IDPIssuerURL() string {
return c.Config["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c *OIDCAuthProviderConfig) ClientID() string {
func (c *OIDCAuthProvider) ClientID() string {
return c.Config["client-id"]
}
// ClientSecret returns the client-secret.
func (c *OIDCAuthProviderConfig) ClientSecret() string {
func (c *OIDCAuthProvider) ClientSecret() string {
return c.Config["client-secret"]
}
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
// IDPCertificateAuthority returns the idp-certificate-authority.
func (c *OIDCAuthProvider) IDPCertificateAuthority() string {
return c.Config["idp-certificate-authority"]
}
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
return c.Config["idp-certificate-authority-data"]
}
// ExtraScopes returns the extra-scopes.
func (c *OIDCAuthProvider) ExtraScopes() []string {
if c.Config["extra-scopes"] == "" {
return []string{}
}
return strings.Split(c.Config["extra-scopes"], ",")
}
// IDToken returns the id-token.
func (c *OIDCAuthProvider) IDToken() string {
return c.Config["id-token"]
}
// SetIDToken replaces the id-token.
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
c.Config["id-token"] = idToken
}
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
// SetRefreshToken replaces the refresh-token.
func (c *OIDCAuthProvider) SetRefreshToken(refreshToken string) {
c.Config["refresh-token"] = refreshToken
}

View File

@@ -1,48 +0,0 @@
package kubeconfig
import (
"fmt"
"os"
homedir "github.com/mitchellh/go-homedir"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
const userKubeConfig = "~/.kube/config"
// Find returns path to the kubeconfig file,
// that is given by env:KUBECONFIG or ~/.kube/config.
// This returns an error if it is not found or I/O error occurred.
func Find() (string, error) {
path := os.Getenv("KUBECONFIG")
if path == "" {
var err error
path, err = homedir.Expand(userKubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s: %s", userKubeConfig, err)
}
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("Could not stat %s: %s", userKubeConfig, err)
}
if info.IsDir() {
return "", fmt.Errorf("%s should be a file", userKubeConfig)
}
return path, nil
}
// 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 "{{ .darwin_amd64_zip_sha256 }}"
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

39
main.go
View File

@@ -3,39 +3,18 @@ package main
import (
"context"
"log"
"os"
"github.com/int128/kubelogin/authn"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
var version = "HEAD"
func main() {
path, err := kubeconfig.Find()
if err != nil {
log.Fatalf("Could not find kubeconfig: %s", err)
if err := di.Invoke(func(cmd adaptors.Cmd) {
os.Exit(cmd.Run(context.Background(), os.Args, version))
}); err != nil {
log.Fatalf("Error: %s", err)
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Load(path)
if err != nil {
log.Fatalf("Could not load kubeconfig: %s", err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
log.Fatalf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
if err != nil {
log.Fatalf("Could not find auth-provider: %s", err)
}
ctx := context.Background()
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
log.Fatalf("Authentication error: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
}

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: "{{ .linux_amd64_zip_sha256 }}"
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: "{{ .darwin_amd64_zip_sha256 }}"
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: "{{ .windows_amd64_zip_sha256 }}"
bin: kubelogin.exe
files:
- from: "kubelogin.exe"
to: "."
selector:
matchLabels:
os: windows
arch: amd64

View File

@@ -0,0 +1,16 @@
package usecases
import "context"
//go:generate mockgen -package mock_usecases -destination ../mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases/interfaces Login
type Login interface {
Do(ctx context.Context, in LoginIn) error
}
type LoginIn struct {
KubeConfig string
SkipTLSVerify bool
SkipOpenBrowser bool
ListenPort int
}

123
usecases/login.go Normal file
View File

@@ -0,0 +1,123 @@
package usecases
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/pkg/errors"
"go.uber.org/dig"
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials %[1]s \
--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`
func NewLogin(i Login) usecases.Login {
return &i
}
type Login struct {
dig.In
KubeConfig adaptors.KubeConfig
HTTP adaptors.HTTP
OIDC adaptors.OIDC
Logger adaptors.Logger
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: Log may contain your secrets, e.g. token or password")
u.Logger.Debugf(1, "Loading %s", in.KubeConfig)
cfg, err := u.KubeConfig.LoadFromFile(in.KubeConfig)
if err != nil {
return errors.Wrapf(err, "could not read the kubeconfig")
}
u.Logger.Printf("Using current-context: %s", cfg.CurrentContext)
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage, cfg.CurrentContext)
return errors.Wrapf(err, "could not find an oidc auth-provider in the kubeconfig")
}
clientConfig := u.HTTP.NewClientConfig()
clientConfig.SetSkipTLSVerify(in.SkipTLSVerify)
if authProvider.IDPCertificateAuthority() != "" {
filename := authProvider.IDPCertificateAuthority()
u.Logger.Printf("Using the certificate %s", filename)
if err := clientConfig.AddCertificateFromFile(filename); err != nil {
u.Logger.Printf("Skip the certificate %s: %s", filename, err)
}
}
if authProvider.IDPCertificateAuthorityData() != "" {
encoded := authProvider.IDPCertificateAuthorityData()
u.Logger.Printf("Using the certificate of idp-certificate-authority-data")
if err := clientConfig.AddEncodedCertificate(encoded); err != nil {
u.Logger.Printf("Skip the certificate of idp-certificate-authority-data: %s", err)
}
}
hc, err := u.HTTP.NewClient(clientConfig)
if err != nil {
return errors.Wrapf(err, "could not create a HTTP client")
}
if token := u.verifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
IDToken: authProvider.IDToken(),
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
Client: hc,
}); token != nil {
u.Logger.Printf("You already have a valid token (until %s)", token.Expiry)
return nil
}
out, err := u.OIDC.Authenticate(ctx,
adaptors.OIDCAuthenticateIn{
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: hc,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
},
adaptors.OIDCAuthenticateCallback{
ShowLocalServerURL: func(url string) {
u.Logger.Printf("Open %s for authentication", url)
},
})
if err != nil {
return errors.Wrapf(err, "could not get token from OIDC provider")
}
u.Logger.Printf("Got a token for subject %s (valid until %s)", out.VerifiedIDToken.Subject, out.VerifiedIDToken.Expiry)
u.Logger.Debugf(1, "Got an ID token %+v", out.VerifiedIDToken)
authProvider.SetIDToken(out.IDToken)
authProvider.SetRefreshToken(out.RefreshToken)
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", in.KubeConfig)
if err := u.KubeConfig.WriteToFile(cfg, in.KubeConfig); err != nil {
return errors.Wrapf(err, "could not update the kubeconfig")
}
u.Logger.Printf("Updated %s", in.KubeConfig)
return nil
}
func (u *Login) verifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) *oidc.IDToken {
if in.IDToken == "" {
return nil
}
token, err := u.OIDC.VerifyIDToken(ctx, in)
if err != nil {
u.Logger.Debugf(1, "Could not verify the ID token in the kubeconfig: %s", err)
return nil
}
u.Logger.Debugf(1, "Verified token %+v", token)
return token
}

521
usecases/login_test.go Normal file
View File

@@ -0,0 +1,521 @@
package usecases
import (
"context"
"net/http"
"testing"
"github.com/coreos/go-oidc"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
func TestLogin_Do(t *testing.T) {
httpClient := &http.Client{}
newMockKubeConfig := func(ctrl *gomock.Controller, in *api.Config, out *api.Config) adaptors.KubeConfig {
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
kubeConfig.EXPECT().
LoadFromFile("/path/to/kubeconfig").
Return(in, nil)
kubeConfig.EXPECT().
WriteToFile(out, "/path/to/kubeconfig")
return kubeConfig
}
newMockHTTP := func(ctrl *gomock.Controller, config adaptors.HTTPClientConfig) adaptors.HTTP {
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClientConfig().
Return(config)
mockHTTP.EXPECT().
NewClient(config).
Return(httpClient, nil)
return mockHTTP
}
newInConfig := func() *api.Config {
return &api.Config{
APIVersion: "v1",
CurrentContext: "default",
Contexts: map[string]*api.Context{
"default": {
AuthInfo: "google",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"client-id": "YOUR_CLIENT_ID",
"client-secret": "YOUR_CLIENT_SECRET",
"idp-issuer-url": "https://accounts.google.com",
},
},
},
},
}
}
newOutConfig := func(in *api.Config) *api.Config {
config := in.DeepCopy()
config.AuthInfos["google"].AuthProvider.Config["id-token"] = "YOUR_ID_TOKEN"
config.AuthInfos["google"].AuthProvider.Config["refresh-token"] = "YOUR_REFRESH_TOKEN"
return config
}
t.Run("Defaults", func(t *testing.T) {
inConfig := newInConfig()
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Do(func(_ context.Context, _ adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) {
cb.ShowLocalServerURL("http://localhost:10000")
}).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
inConfig := newInConfig()
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(true)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
SkipTLSVerify: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("SkipOpenBrowser", func(t *testing.T) {
inConfig := newInConfig()
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
SkipOpenBrowser: true,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
SkipOpenBrowser: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/ValidToken", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "VALID"
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
kubeConfig.EXPECT().
LoadFromFile("/path/to/kubeconfig").
Return(inConfig, nil)
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
IDToken: "VALID",
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
Client: httpClient,
}).
Return(&oidc.IDToken{}, nil)
u := Login{
KubeConfig: kubeConfig,
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/InvalidToken", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "EXPIRED"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
IDToken: "EXPIRED",
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
Client: httpClient,
}).
Return(nil, errors.New("token is expired"))
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/extra-scopes", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["extra-scopes"] = "email,profile"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority"] = "/path/to/cert"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddCertificateFromFile("/path/to/cert")
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority/error", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority"] = "/path/to/cert"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddCertificateFromFile("/path/to/cert").
Return(errors.New("not found"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority-data", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority-data"] = "base64encoded"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddEncodedCertificate("base64encoded")
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority-data/error", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority-data"] = "base64encoded"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddEncodedCertificate("base64encoded").
Return(errors.New("invalid"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
}

View File

@@ -0,0 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases/interfaces (interfaces: Login)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/usecases/interfaces"
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 interfaces.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)
}