mirror of
https://github.com/int128/kubelogin.git
synced 2026-03-02 08:50:19 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42879dc915 | ||
|
|
7ce98c7119 | ||
|
|
8b5e87de75 | ||
|
|
1b545e1c58 | ||
|
|
2fa306c348 | ||
|
|
9018cd65c5 | ||
|
|
9fe6a09943 | ||
|
|
c53d415255 | ||
|
|
aa0718df16 | ||
|
|
40698536b0 | ||
|
|
7ec53a5dd1 | ||
|
|
3a28e44556 | ||
|
|
f8cca818af | ||
|
|
0c6ca03eb9 |
30
.github/workflows/acceptance-test.yaml
vendored
Normal file
30
.github/workflows/acceptance-test.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: acceptance-test
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
name: test
|
||||
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
go-
|
||||
# https://kind.sigs.k8s.io/docs/user/quick-start/
|
||||
- run: |
|
||||
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-linux-amd64"
|
||||
chmod +x ./kind
|
||||
sudo mv ./kind /usr/local/bin/kind
|
||||
kind version
|
||||
# https://packages.ubuntu.com/xenial/libnss3-tools
|
||||
- run: sudo apt install -y libnss3-tools
|
||||
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
|
||||
- run: make -C acceptance_test -j3 setup
|
||||
- run: make -C acceptance_test test
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
/.idea
|
||||
|
||||
/.kubeconfig*
|
||||
/acceptance_test/output/
|
||||
|
||||
/dist/output
|
||||
/coverage.out
|
||||
|
||||
76
README.md
76
README.md
@@ -1,4 +1,4 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin) [](https://goreportcard.com/report/github.com/int128/kubelogin)
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)  [](https://goreportcard.com/report/github.com/int128/kubelogin)
|
||||
|
||||
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
|
||||
|
||||
@@ -28,9 +28,12 @@ brew install int128/kubelogin/kubelogin
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.15.0/kubelogin_linux_amd64.zip
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.16.0/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
|
||||
# Docker
|
||||
docker run --rm quay.io/int128/kubelogin:v1.16.0
|
||||
```
|
||||
|
||||
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
|
||||
@@ -110,27 +113,25 @@ If you are looking for a specific version, see [the release tags](https://github
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubectl oidc-login get-token -h
|
||||
Run as a kubectl credential plugin
|
||||
|
||||
Usage:
|
||||
kubelogin get-token [flags]
|
||||
|
||||
Flags:
|
||||
--oidc-issuer-url string Issuer URL of the provider (mandatory)
|
||||
--oidc-client-id string Client ID of the provider (mandatory)
|
||||
--oidc-client-secret string Client secret of the provider
|
||||
--oidc-extra-scope strings Scopes to request to the provider
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--listen-port ints (Deprecated: use --listen-address)
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
-h, --help help for get-token
|
||||
--oidc-issuer-url string Issuer URL of the provider (mandatory)
|
||||
--oidc-client-id string Client ID of the provider (mandatory)
|
||||
--oidc-client-secret string Client secret of the provider
|
||||
--oidc-extra-scope strings Scopes to request to the provider
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--certificate-authority-data string Base64 encoded data for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--listen-port ints (Deprecated: use --listen-address)
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
-h, --help help for get-token
|
||||
|
||||
Global Flags:
|
||||
--add_dir_header If true, adds the file directory to the header
|
||||
@@ -158,12 +159,13 @@ You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
|
||||
- --oidc-extra-scope=profile
|
||||
```
|
||||
|
||||
### CA Certificates
|
||||
### CA Certificate
|
||||
|
||||
You can use your self-signed certificate for the provider.
|
||||
|
||||
```yaml
|
||||
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
|
||||
- --certificate-authority-data=LS0t...
|
||||
```
|
||||
|
||||
### HTTP Proxy
|
||||
@@ -171,6 +173,38 @@ You can use your self-signed certificate for the provider.
|
||||
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
|
||||
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
|
||||
|
||||
### Docker
|
||||
|
||||
You can run [the Docker image](https://quay.io/repository/int128/kubelogin) instead of the binary.
|
||||
The kubeconfig looks like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: docker
|
||||
args:
|
||||
- run
|
||||
- --rm
|
||||
- -v
|
||||
- /tmp/.token-cache:/.token-cache
|
||||
- -p
|
||||
- 8000:8000
|
||||
- quay.io/int128/kubelogin:v1.16.0
|
||||
- get-token
|
||||
- --token-cache-dir=/.token-cache
|
||||
- --listen-address=0.0.0.0:8000
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Known limitations:
|
||||
|
||||
- It cannot open the browser automatically.
|
||||
- The container port and listen port must be equal for consistency of the redirect URI.
|
||||
|
||||
### Authentication flows
|
||||
|
||||
@@ -274,3 +308,5 @@ make check
|
||||
make
|
||||
./kubelogin
|
||||
```
|
||||
|
||||
See also [the acceptance test](acceptance_test).
|
||||
|
||||
108
acceptance_test/Makefile
Normal file
108
acceptance_test/Makefile
Normal file
@@ -0,0 +1,108 @@
|
||||
CLUSTER_NAME := kubelogin-acceptance-test
|
||||
OUTPUT_DIR := $(CURDIR)/output
|
||||
|
||||
PATH := $(PATH):$(OUTPUT_DIR)/bin
|
||||
export PATH
|
||||
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
|
||||
export KUBECONFIG
|
||||
|
||||
# run the login script instead of opening chrome
|
||||
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
|
||||
export BROWSER
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
# see the setup instruction
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--oidc-extra-scope=email \
|
||||
--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# set up the kubeconfig
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--exec-arg=--oidc-extra-scope=email \
|
||||
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# make sure we can access the cluster
|
||||
kubectl --user=oidc cluster-info
|
||||
# switch the current context
|
||||
kubectl config set-context --current --user=oidc
|
||||
# make sure we can access the cluster
|
||||
kubectl cluster-info
|
||||
|
||||
.PHONY: setup
|
||||
setup: build dex cluster setup-chrome
|
||||
|
||||
.PHONY: setup-chrome
|
||||
setup-chrome: $(OUTPUT_DIR)/ca.crt
|
||||
# add the dex server certificate to the trust store
|
||||
mkdir -p ~/.pki/nssdb
|
||||
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
|
||||
|
||||
# build binaries
|
||||
.PHONY: build
|
||||
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
|
||||
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
|
||||
go build -o $@ ..
|
||||
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
|
||||
go build -o $@ ./chromelogin
|
||||
|
||||
# create a Dex server
|
||||
.PHONY: dex
|
||||
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
|
||||
docker create --name dex-server -p 10443:10443 quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
|
||||
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
|
||||
docker cp $(OUTPUT_DIR)/server.key dex-server:/
|
||||
docker cp dex.yaml dex-server:/
|
||||
docker start dex-server
|
||||
docker logs dex-server
|
||||
|
||||
$(OUTPUT_DIR)/ca.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
|
||||
$(OUTPUT_DIR)/server.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
|
||||
|
||||
# create a Kubernetes cluster
|
||||
.PHONY: cluster
|
||||
cluster: dex create-cluster
|
||||
# add the Dex container IP to /etc/hosts of kube-apiserver
|
||||
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
|
||||
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
|
||||
# wait for kube-apiserver oidc initialization
|
||||
# (oidc authenticator will retry oidc discovery every 10s)
|
||||
sleep 10
|
||||
|
||||
.PHONY: create-cluster
|
||||
create-cluster: $(OUTPUT_DIR)/ca.crt
|
||||
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-acceptance-test-dex-ca.crt
|
||||
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
|
||||
kubectl apply -f role.yaml
|
||||
|
||||
# clean up the resources
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -r $(OUTPUT_DIR)
|
||||
.PHONY: delete-cluster
|
||||
delete-cluster:
|
||||
kind delete cluster --name $(CLUSTER_NAME)
|
||||
.PHONY: delete-dex
|
||||
delete-dex:
|
||||
docker stop dex-server
|
||||
docker rm dex-server
|
||||
91
acceptance_test/README.md
Normal file
91
acceptance_test/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# kubelogin/acceptance_test
|
||||
|
||||
This is an acceptance test to verify behavior of kubelogin using a real Kubernetes cluster and OpenID Connect provider.
|
||||
It runs on [GitHub Actions](https://github.com/int128/kubelogin/actions?query=workflow%3Aacceptance-test).
|
||||
|
||||
Let's take a look at the diagram.
|
||||
|
||||

|
||||
|
||||
It prepares the following resources:
|
||||
|
||||
1. Generate a pair of CA certificate and TLS server certificate for Dex.
|
||||
1. Run Dex on a container.
|
||||
1. Create a Kubernetes cluster using Kind.
|
||||
1. Mutate `/etc/hosts` of the CI machine to access Dex.
|
||||
1. Mutate `/etc/hosts` of the kube-apiserver pod to access Dex.
|
||||
|
||||
It performs the test by the following steps:
|
||||
|
||||
1. Run kubectl.
|
||||
1. kubectl automatically runs kubelogin.
|
||||
1. kubelogin automatically runs [chromelogin](chromelogin).
|
||||
1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
|
||||
1. kubelogin gets an authorization code from the browser.
|
||||
1. kubelogin gets a token.
|
||||
1. kubectl accesses an API with the token.
|
||||
1. kube-apiserver verifies the token by Dex.
|
||||
1. Check if kubectl exited with code 0.
|
||||
|
||||
|
||||
## Technical consideration
|
||||
|
||||
### Network and DNS
|
||||
|
||||
Consider the following issues:
|
||||
|
||||
- kube-apiserver runs on the host network of the kind container.
|
||||
- kube-apiserver cannot resolve a service name by kube-dns.
|
||||
- kube-apiserver cannot access a cluster IP.
|
||||
- kube-apiserver can access another container via the Docker network.
|
||||
- Chrome requires exactly match of domain name between Dex URL and a server certificate.
|
||||
|
||||
Consequently,
|
||||
|
||||
- kube-apiserver accesses Dex by resolving `/etc/hosts` and via the Docker network.
|
||||
- kubelogin and Chrome accesses Dex by resolving `/etc/hosts` and via the Docker network.
|
||||
|
||||
### TLS server certificate
|
||||
|
||||
Consider the following issues:
|
||||
|
||||
- kube-apiserver requires `--oidc-issuer` is HTTPS URL.
|
||||
- kube-apiserver requires a CA certificate at startup, if `--oidc-ca-file` is given.
|
||||
- kube-apiserver mounts `/usr/local/share/ca-certificates` from the kind container.
|
||||
- It is possible to mount a file from the CI machine.
|
||||
- It is not possible to issue a certificate using Let's Encrypt in runtime.
|
||||
- Chrome requires a valid certificate in `~/.pki/nssdb`.
|
||||
|
||||
As a result,
|
||||
|
||||
- kube-apiserver uses the CA certificate of `/usr/local/share/ca-certificates/dex-ca.crt`. See the `extraMounts` section of [`cluster.yaml`](cluster.yaml).
|
||||
- kubelogin uses the CA certificate in `output/ca.crt`.
|
||||
- Chrome uses the CA certificate in `~/.pki/nssdb`.
|
||||
|
||||
### Test environment
|
||||
|
||||
- Set the issuer URL to kubectl. See [`kubeconfig_oidc.yaml`](kubeconfig_oidc.yaml).
|
||||
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
|
||||
- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.
|
||||
|
||||
|
||||
## Run locally
|
||||
|
||||
You need to set up Docker and Kind.
|
||||
|
||||
You need to add the following line to `/etc/hosts`:
|
||||
|
||||
```
|
||||
127.0.0.1 dex-server
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```shell script
|
||||
# run the test
|
||||
make
|
||||
|
||||
# clean up
|
||||
make delete-cluster
|
||||
make delete-dex
|
||||
```
|
||||
93
acceptance_test/chromelogin/main.go
Normal file
93
acceptance_test/chromelogin/main.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s URL", os.Args[0])
|
||||
return
|
||||
}
|
||||
url := os.Args[1]
|
||||
if err := runBrowser(context.Background(), url); err != nil {
|
||||
log.Fatalf("error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runBrowser(ctx context.Context, url string) error {
|
||||
execOpts := chromedp.DefaultExecAllocatorOptions[:]
|
||||
execOpts = append(execOpts, chromedp.NoSandbox)
|
||||
ctx, cancel := chromedp.NewExecAllocator(ctx, execOpts...)
|
||||
defer cancel()
|
||||
ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
|
||||
defer cancel()
|
||||
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if err := logInToDex(ctx, url); err != nil {
|
||||
return fmt.Errorf("could not run the browser: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logInToDex(ctx context.Context, url string) error {
|
||||
for {
|
||||
var location string
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
chromedp.Location(&location),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("location: %s", location)
|
||||
if strings.HasPrefix(location, `http://`) || strings.HasPrefix(location, `https://`) {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
err := chromedp.Run(ctx,
|
||||
// https://dex-server:10443/dex/auth/local
|
||||
chromedp.WaitVisible(`#login`),
|
||||
logPageMetadata(),
|
||||
chromedp.SendKeys(`#login`, `admin@example.com`),
|
||||
chromedp.SendKeys(`#password`, `password`),
|
||||
chromedp.Submit(`#submit-login`),
|
||||
// https://dex-server:10443/dex/approval
|
||||
chromedp.WaitVisible(`.dex-btn.theme-btn--success`),
|
||||
logPageMetadata(),
|
||||
chromedp.Submit(`.dex-btn.theme-btn--success`),
|
||||
// http://localhost:8000
|
||||
chromedp.WaitReady(`body`),
|
||||
logPageMetadata(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logPageMetadata() chromedp.Action {
|
||||
var location string
|
||||
var title string
|
||||
return chromedp.Tasks{
|
||||
chromedp.Location(&location),
|
||||
chromedp.Title(&title),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
log.Printf("location: %s [%s]", location, title)
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
20
acceptance_test/cluster.yaml
Normal file
20
acceptance_test/cluster.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
# https://github.com/dexidp/dex/blob/master/Documentation/kubernetes.md
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
apiVersion: kubeadm.k8s.io/v1beta2
|
||||
kind: ClusterConfiguration
|
||||
metadata:
|
||||
name: config
|
||||
apiServer:
|
||||
extraArgs:
|
||||
oidc-issuer-url: https://dex-server:10443/dex
|
||||
oidc-client-id: YOUR_CLIENT_ID
|
||||
oidc-username-claim: email
|
||||
oidc-ca-file: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
nodes:
|
||||
- role: control-plane
|
||||
extraMounts:
|
||||
- hostPath: /tmp/kubelogin-acceptance-test-dex-ca.crt
|
||||
containerPath: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
23
acceptance_test/dex.yaml
Normal file
23
acceptance_test/dex.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
issuer: https://dex-server:10443/dex
|
||||
web:
|
||||
https: 0.0.0.0:10443
|
||||
tlsCert: /server.crt
|
||||
tlsKey: /server.key
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: /tmp/dex.db
|
||||
staticClients:
|
||||
- id: YOUR_CLIENT_ID
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
name: kubelogin
|
||||
secret: YOUR_CLIENT_SECRET
|
||||
staticPasswords:
|
||||
- email: "admin@example.com"
|
||||
# bcrypt hash of the string "password"
|
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||
username: "admin"
|
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||
# required for staticPasswords
|
||||
enablePasswordDB: true
|
||||
15
acceptance_test/openssl.cnf
Normal file
15
acceptance_test/openssl.cnf
Normal file
@@ -0,0 +1,15 @@
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = @alt_names
|
||||
extendedKeyUsage = serverAuth
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = dex-server
|
||||
DNS.2 = dex-server:30443
|
||||
21
acceptance_test/role.yaml
Normal file
21
acceptance_test/role.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: readonly-all-resources
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: readonly-all-resources
|
||||
subjects:
|
||||
- kind: User
|
||||
name: admin@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: readonly-all-resources
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
4
dist/Dockerfile
vendored
4
dist/Dockerfile
vendored
@@ -5,9 +5,9 @@ ARG KUBELOGIN_SHA256="{{ sha256 .linux_amd64_archive }}"
|
||||
|
||||
# Download the release and test the checksum
|
||||
RUN wget -O /kubelogin.zip "https://github.com/int128/kubelogin/releases/download/$KUBELOGIN_VERSION/kubelogin_linux_amd64.zip" && \
|
||||
echo "$KUBELOGIN_SHA256 /kubelogin.zip" | sha256sum -c - && \
|
||||
unzip /kubelogin.zip && \
|
||||
rm /kubelogin.zip && \
|
||||
echo "$KUBELOGIN_SHA256 /kubelogin" | sha256sum -c -
|
||||
rm /kubelogin.zip
|
||||
|
||||
USER daemon
|
||||
ENTRYPOINT ["/kubelogin"]
|
||||
|
||||
3
docs/acceptance-test-diagram.svg
Normal file
3
docs/acceptance-test-diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
@@ -1,6 +1,6 @@
|
||||
# Kubernetes OpenID Connection authentication
|
||||
|
||||
This document guides how to set up the Kubernetes OpenID Connect (OIDC) authentication.
|
||||
This document guides how to set up Kubernetes OpenID Connect (OIDC) authentication.
|
||||
Let's see the following steps:
|
||||
|
||||
1. Set up the OIDC provider
|
||||
@@ -35,7 +35,7 @@ Variable | Value
|
||||
You can log in with a user of Keycloak.
|
||||
Make sure you have an administrator role of the Keycloak realm.
|
||||
|
||||
Open the Keycloak and create an OIDC client as follows:
|
||||
Open Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Client ID: `YOUR_CLIENT_ID`
|
||||
- Valid Redirect URLs:
|
||||
@@ -52,7 +52,7 @@ You can associate client roles by adding the following mapper:
|
||||
- Token Claim Name: `groups`
|
||||
- Add to ID token: on
|
||||
|
||||
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
For example, if you have `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
@@ -72,7 +72,7 @@ Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an a
|
||||
- Homepage URL: `https://dex.example.com`
|
||||
- Authorization callback URL: `https://dex.example.com/callback`
|
||||
|
||||
Deploy the [dex](https://github.com/dexidp/dex) with the following config:
|
||||
Deploy [Dex](https://github.com/dexidp/dex) with the following config:
|
||||
|
||||
```yaml
|
||||
issuer: https://dex.example.com
|
||||
@@ -138,13 +138,20 @@ kubectl oidc-login setup \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
It will open the browser and you can log in to the provider.
|
||||
Then it will show the instruction.
|
||||
It launches the browser and navigates to `http://localhost:8000`.
|
||||
Please log in to the provider.
|
||||
|
||||
You can set extra options, for example, extra scope or CA certificate.
|
||||
See also the full options.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login setup --help
|
||||
```
|
||||
|
||||
|
||||
## 3. Bind a cluster role
|
||||
|
||||
In this tutorial, bind the `cluster-admin` role to you.
|
||||
Here bind `cluster-admin` role to you.
|
||||
Apply the following manifest:
|
||||
|
||||
```yaml
|
||||
@@ -170,14 +177,14 @@ As well as you can create a custom cluster role and bind it.
|
||||
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following options to the kube-apiserver:
|
||||
Add the following flags to kube-apiserver:
|
||||
|
||||
```
|
||||
--oidc-issuer-url=ISSUER_URL
|
||||
--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
|
||||
See [Kubernetes Authenticating: OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for the all flags.
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
@@ -200,35 +207,32 @@ If you are using [kube-aws](https://github.com/kubernetes-incubator/kube-aws), a
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add the following user to the kubeconfig:
|
||||
Add `oidc` user to the kubeconfig.
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```sh
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=ISSUER_URL \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```sh
|
||||
kubectl --user=oidc cluster-info
|
||||
```
|
||||
% kubectl get nodes
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
|
||||
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
|
||||
|
||||
You can switch the current context to oidc.
|
||||
|
||||
```sh
|
||||
kubectl config set-context --current --user=oidc
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the output.
|
||||
//
|
||||
func TestCredentialPlugin(t *testing.T) {
|
||||
cacheDir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a cache dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(cacheDir); err != nil {
|
||||
t.Errorf("could not clean up the cache dir: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
testCredentialPlugin(t, cacheDir, keys.None, nil)
|
||||
})
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
testCredentialPlugin(t, cacheDir, keys.Server, []string{"--certificate-authority", keys.Server.CACertPath})
|
||||
})
|
||||
}
|
||||
|
||||
func testCredentialPlugin(t *testing.T, cacheDir string, idpTLS keys.Keys, extraArgs []string) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
|
||||
|
||||
args := []string{
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupMockIDPForROPC(service, serverURL, "openid", "USER", "PASS", idToken)
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
|
||||
|
||||
args := []string{
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
setupTokenCache(t, cacheDir,
|
||||
tokencache.Key{
|
||||
IssuerURL: serverURL,
|
||||
ClientID: "kubernetes",
|
||||
CACertFilename: idpTLS.CACertPath,
|
||||
}, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
|
||||
|
||||
args := []string{
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
|
||||
assertTokenCache(t, cacheDir,
|
||||
tokencache.Key{
|
||||
IssuerURL: serverURL,
|
||||
ClientID: "kubernetes",
|
||||
CACertFilename: idpTLS.CACertPath,
|
||||
}, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
|
||||
setupMockIDPForDiscovery(service, serverURL)
|
||||
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(validIDToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
|
||||
setupTokenCache(t, cacheDir,
|
||||
tokencache.Key{
|
||||
IssuerURL: serverURL,
|
||||
ClientID: "kubernetes",
|
||||
CACertFilename: idpTLS.CACertPath,
|
||||
}, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
assertCredentialPluginOutput(t, credentialPluginInteraction, &validIDToken)
|
||||
|
||||
args := []string{
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
|
||||
assertTokenCache(t, cacheDir,
|
||||
tokencache.Key{
|
||||
IssuerURL: serverURL,
|
||||
ClientID: "kubernetes",
|
||||
CACertFilename: idpTLS.CACertPath,
|
||||
}, tokencache.Value{
|
||||
IDToken: validIDToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &validIDToken)
|
||||
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
setupTokenCache(t, cacheDir,
|
||||
tokencache.Key{
|
||||
IssuerURL: serverURL,
|
||||
ClientID: "kubernetes",
|
||||
CACertFilename: idpTLS.CACertPath,
|
||||
}, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
assertCredentialPluginOutput(t, credentialPluginInteraction, &validIDToken)
|
||||
|
||||
args := []string{
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
|
||||
assertTokenCache(t, cacheDir,
|
||||
tokencache.Key{
|
||||
IssuerURL: serverURL,
|
||||
ClientID: "kubernetes",
|
||||
CACertFilename: idpTLS.CACertPath,
|
||||
}, tokencache.Value{
|
||||
IDToken: validIDToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "email profile openid", &idToken)
|
||||
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
|
||||
|
||||
args := []string{
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
|
||||
})
|
||||
}
|
||||
|
||||
func assertCredentialPluginOutput(t *testing.T, credentialPluginInteraction *mock_credentialplugin.MockInterface, idToken *string) {
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(gomock.Any()).
|
||||
Do(func(out credentialplugin.Output) {
|
||||
if out.Token != *idToken {
|
||||
t.Errorf("Token wants %s but %s", *idToken, out.Token)
|
||||
}
|
||||
if out.Expiry != tokenExpiryFuture {
|
||||
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, interaction credentialplugin.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, interaction)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin", "get-token",
|
||||
"--v=1",
|
||||
"--skip-open-browser",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTokenCache(t *testing.T, cacheDir string, k tokencache.Key, v tokencache.Value) {
|
||||
var r tokencache.Repository
|
||||
err := r.Save(cacheDir, k, v)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTokenCache(t *testing.T, cacheDir string, k tokencache.Key, want tokencache.Value) {
|
||||
var r tokencache.Repository
|
||||
got, err := r.FindByKey(cacheDir, k)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(&want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
|
||||
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
|
||||
)
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: expiry.Unix(),
|
||||
}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func setupMockIDPForDiscovery(service *mock_idp.MockService, serverURL string) {
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
}
|
||||
|
||||
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
|
||||
var nonce string
|
||||
setupMockIDPForDiscovery(service, serverURL)
|
||||
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
|
||||
DoAndReturn(func(_, gotNonce string) (string, error) {
|
||||
nonce = gotNonce
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
})
|
||||
service.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
|
||||
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
|
||||
})
|
||||
}
|
||||
|
||||
func setupMockIDPForROPC(service *mock_idp.MockService, serverURL, scope, username, password, idToken string) {
|
||||
setupMockIDPForDiscovery(service, serverURL)
|
||||
service.EXPECT().AuthenticatePassword(username, password, scope).
|
||||
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
|
||||
}
|
||||
|
||||
func openBrowserOnReadyFunc(t *testing.T, ctx context.Context, k keys.Keys) authentication.LocalServerReadyFunc {
|
||||
return func(url string) {
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: k.TLSConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
e2e_test/keys/testdata/.gitignore
vendored
1
e2e_test/keys/testdata/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/CA
|
||||
59
e2e_test/keys/testdata/Makefile
vendored
59
e2e_test/keys/testdata/Makefile
vendored
@@ -1,59 +0,0 @@
|
||||
EXPIRY := 3650
|
||||
|
||||
all: ca.key ca.crt server.key server.crt jws.key
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -v ca.* server.* jws.*
|
||||
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.INTERMEDIATE: ca.csr
|
||||
ca.csr: openssl.cnf ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key ca.key \
|
||||
-subj "/CN=Hello CA" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
ca.crt: ca.csr ca.key
|
||||
openssl x509 \
|
||||
-req \
|
||||
-days $(EXPIRY) \
|
||||
-signkey ca.key \
|
||||
-in ca.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
server.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.INTERMEDIATE: server.csr
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key server.key \
|
||||
-subj "/CN=localhost" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
server.crt: openssl.cnf server.csr ca.key ca.crt
|
||||
rm -fr ./CA
|
||||
mkdir -p ./CA
|
||||
touch CA/index.txt
|
||||
touch CA/index.txt.attr
|
||||
echo 00 > CA/serial
|
||||
openssl ca -config openssl.cnf \
|
||||
-days $(EXPIRY) \
|
||||
-extensions v3_req \
|
||||
-batch \
|
||||
-cert ca.crt \
|
||||
-keyfile ca.key \
|
||||
-in server.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
jws.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
11
e2e_test/keys/testdata/ca.crt
vendored
11
e2e_test/keys/testdata/ca.crt
vendored
@@ -1,11 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnTCCAQYCCQC/aR7GRyndljANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
|
||||
ZWxsbyBDQTAeFw0xOTA5MjUxNDQ0NTFaFw0yOTA5MjIxNDQ0NTFaMBMxETAPBgNV
|
||||
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
|
||||
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
|
||||
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
|
||||
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
|
||||
AE/gsgTC4jzYC3icZdhALJTe3JsZ7geN702dE95zSI5LXAzzHJ/j8wGmorQjrMs2
|
||||
iNPjVOdTU6cVWa1Ba29wWakVyVCUqDmDiWHaVhM/Qyyxo6mVlZGFwSnto3zq/h4y
|
||||
KMFJ8lUtFCYMrzo5wqgj2xOjVrN77F6F4XWZbMufh50G
|
||||
-----END CERTIFICATE-----
|
||||
15
e2e_test/keys/testdata/ca.key
vendored
15
e2e_test/keys/testdata/ca.key
vendored
@@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDnSTDsRx4UJmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ
|
||||
4Pc51itOycWN1nclNVb489i9J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3L
|
||||
u4mysxyOtYZNsdST8GBCP0m42tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQAB
|
||||
AoGAaYmTYm29QvKW4et9oPxDjpYG0bqlz7P0xFRR9kKtKTATAMHjWeu2xFR/JI+b
|
||||
rvJLIdZqHmWe5AmMb3NxZgfLonEB71ohaKQha1L8Vc7aoedRheJvqqaNr+ZxoCMO
|
||||
8xcjsaMYxLEVt0tg6XyKyEhi1/hOufFZ4BSng4oQbrpaNIkCQQD6hPEzzPZtMEEe
|
||||
eRdwTVUIStKFMQbRdwZ5Oc7pyDk2U+SFRJiqkBkqnmFekcf2UgbBQxem+GMhWNgE
|
||||
LItKy/wVAkEA7FiHxbzn2msaE3hZCWudtnXqmJNuPO0zJ5icXe2svwmwPfLA/rm9
|
||||
iazCuyzyK67J8IG9QgIjQFYXtQbMr2chGwJBAMm3dghBx0LQEf8Zfdf9TLSqmqyI
|
||||
d3b+IgZGl+cCQ58NGfp863ibIsiAUuK0+4/JKItBHLBjXF6jjPx/aYFGkqkCQH4w
|
||||
GnXCEYx1qJuCow87jR4xQQsrlC0lfC2E9t/TmWr6UkYRCWg3ZXJPcj0bl0Upcppd
|
||||
ut22ZHniPZAizEBOcMcCQQDnMEOxufxhMsx2NC8yON/noewLINqKcbkMsl6DjvJl
|
||||
+wLbQmzJ0j+uIBgdpsj4rWnEr7GxoL2eWG44QDUBco79
|
||||
-----END RSA PRIVATE KEY-----
|
||||
15
e2e_test/keys/testdata/jws.key
vendored
15
e2e_test/keys/testdata/jws.key
vendored
@@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXwIBAAKBgQCZukkN1GxMlNXkpOZxCnvCF874/rn1sNKzO98fOwmBRPG2+m/c
|
||||
yqBY7t2L2nihqz3+GZTiHmzSrBzMAGPVW1qGmk9KYg3m7akz9SiCxdoUkgM9MCCp
|
||||
X/s8IhgtXkyoKFPcGdwHblDl/3aJG02b6TAQD8vTNQAKoKw7L0FST+pvRwIDAQAB
|
||||
AoGBAJT1fXR5MbfDQL+dSe6fSex5RYTgzzDTdldW3I1Wl487Tz0OzvYTIe0LCIJL
|
||||
4DhHxnpCL5IsCSbav8ytVA+ZxczHpEW6UxbalXt5UfgFu0joTrdoGxDcVWgUCW3J
|
||||
Olbln0lOP55wViKh509gt45Za3VxJrNul3khVfVj7qGG9cKBAkEAxwtT8LxwqTYF
|
||||
nqoeZvPp15JAqlgdk38ttJa4KEqvpBTSxNIXkL9T5gJ+irKZAzxlz/U7bhn5mw6E
|
||||
3xFiljOXpQJBAMW3XRFOjgNBXNjbt81wREF5LdZl9EI8cRMSH6xljt2uwSqw4EG3
|
||||
76gFvccUd+WnfspFQZVypSSD4pWzsAqh13sCQQCA9BLW5Y7r4ab0a2y08JNwaT1h
|
||||
3yKSO5QF6pu25uQyHpeKkj5YNcyKONV40EqXsRqZB10QcN2omlh1GJNRkm1NAkEA
|
||||
qV3lr4mnRUqcinfM/4MINT3k8h/sGUFFa5y+3SMyOtwURMm3kRRLi5c/dmYmPug4
|
||||
SHUDNU48AQeo9awzRShWOQJBAJdw+cfRgi4fo3HY33uZdFa1T9G+qTA2ijhco6O3
|
||||
8tOc0yOFEtPNXM87MsHGIQP3ZCLfIY1gs2O3WCTFbPxR4rc=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
36
e2e_test/keys/testdata/openssl.cnf
vendored
36
e2e_test/keys/testdata/openssl.cnf
vendored
@@ -1,36 +0,0 @@
|
||||
[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = ./CA
|
||||
certs = $dir
|
||||
crl_dir = $dir
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[ req_distinguished_name ]
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = DNS:localhost
|
||||
|
||||
[ v3_ca ]
|
||||
basicConstraints = CA:true
|
||||
52
e2e_test/keys/testdata/server.crt
vendored
52
e2e_test/keys/testdata/server.crt
vendored
@@ -1,52 +0,0 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 0 (0x0)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN=Hello CA
|
||||
Validity
|
||||
Not Before: Aug 18 06:00:06 2019 GMT
|
||||
Not After : Aug 15 06:00:06 2029 GMT
|
||||
Subject: CN=localhost
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:d6:4e:eb:3a:cb:25:f9:7e:92:22:f2:63:99:da:
|
||||
08:05:8b:a3:e7:d3:fd:71:3e:bd:da:c5:d5:63:b7:
|
||||
d3:7b:f8:cd:1a:2e:5c:a2:4f:48:98:c2:b4:da:e8:
|
||||
1e:d3:d7:8f:d8:ee:a9:70:d0:9d:4f:f4:8d:95:e5:
|
||||
8e:9a:71:b6:80:aa:0b:cb:28:1d:f6:0d:7e:aa:78:
|
||||
bf:30:e6:58:d7:6b:92:8f:19:1c:7d:95:f8:d5:2f:
|
||||
8c:58:49:98:88:05:50:88:80:a9:77:c4:16:b4:c1:
|
||||
00:45:1e:d3:d0:ed:98:4d:f7:a3:5d:f1:82:cb:a5:
|
||||
4d:19:64:4d:43:db:13:d4:17
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Non Repudiation, Key Encipherment
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:localhost
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
5a:5c:5e:8b:de:82:86:f4:98:40:0e:cf:c5:51:fe:89:46:49:
|
||||
f0:26:d2:a5:06:e3:91:43:c1:f8:b2:ad:b7:a1:23:13:1a:80:
|
||||
45:00:51:70:b6:06:63:c6:a8:c8:22:5d:1b:00:e0:4a:8c:2e:
|
||||
ce:b4:da:b1:89:8a:d2:d0:e3:eb:0f:16:34:45:a1:bd:64:5c:
|
||||
48:41:8c:0a:bf:66:be:1c:a8:35:47:ce:b0:dc:c8:4f:5e:c1:
|
||||
ec:ef:21:fb:45:55:95:e3:99:40:46:0b:6c:8a:b3:d5:f0:bf:
|
||||
39:a4:ba:c4:d7:58:88:58:08:07:98:59:6e:ca:9c:08:e4:c4:
|
||||
4f:db
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBzTCCATagAwIBAgIBADANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhIZWxs
|
||||
byBDQTAeFw0xOTA4MTgwNjAwMDZaFw0yOTA4MTUwNjAwMDZaMBQxEjAQBgNVBAMM
|
||||
CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1k7rOssl+X6S
|
||||
IvJjmdoIBYuj59P9cT692sXVY7fTe/jNGi5cok9ImMK02uge09eP2O6pcNCdT/SN
|
||||
leWOmnG2gKoLyygd9g1+qni/MOZY12uSjxkcfZX41S+MWEmYiAVQiICpd8QWtMEA
|
||||
RR7T0O2YTfejXfGCy6VNGWRNQ9sT1BcCAwEAAaMwMC4wCQYDVR0TBAIwADALBgNV
|
||||
HQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4GB
|
||||
AFpcXovegob0mEAOz8VR/olGSfAm0qUG45FDwfiyrbehIxMagEUAUXC2BmPGqMgi
|
||||
XRsA4EqMLs602rGJitLQ4+sPFjRFob1kXEhBjAq/Zr4cqDVHzrDcyE9ewezvIftF
|
||||
VZXjmUBGC2yKs9XwvzmkusTXWIhYCAeYWW7KnAjkxE/b
|
||||
-----END CERTIFICATE-----
|
||||
15
e2e_test/keys/testdata/server.key
vendored
15
e2e_test/keys/testdata/server.key
vendored
@@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXgIBAAKBgQDWTus6yyX5fpIi8mOZ2ggFi6Pn0/1xPr3axdVjt9N7+M0aLlyi
|
||||
T0iYwrTa6B7T14/Y7qlw0J1P9I2V5Y6acbaAqgvLKB32DX6qeL8w5ljXa5KPGRx9
|
||||
lfjVL4xYSZiIBVCIgKl3xBa0wQBFHtPQ7ZhN96Nd8YLLpU0ZZE1D2xPUFwIDAQAB
|
||||
AoGBAJhNR7Dl1JwFzndViWE6aP7/6UEFEBWeADDs7aTLbFmrTJ+xmRWkgLRHk14L
|
||||
HnVwuYLywaoyJ8o9wy1nEbxC2e4zWZ94d351MQf3/komCXDBzEsktfAcNsAFnMmS
|
||||
HZuGXfhi0FYWoftpIGxUmEBmQRcq0ctycbLves6TY3y+oajpAkEA8UHmSr/zsM3E
|
||||
XQXPp2BCAvRrTH/njk4R0jwB29Bi89gt/XDD4uvfWbHw7TZxnZuCpWisnxpMPIwa
|
||||
1rjqIQmhEwJBAONncQUOxwYCIuvraIhV0QtkIUa+YpTvAxP8ZNXx+agtHmHG2TTf
|
||||
kGv2YddvjxXZItN/FZOzUGm9OptaeLRTpW0CQHO8CEzNnoqve0agtgf2LlSaiiqt
|
||||
pRhoLTZsYPvhEMcnapCNGvtt6bxul0REfOZ9poPRHhZJGE9naqydEnv80Y8CQQC3
|
||||
pxLfws95SsBpR/VkJepuCK/XMmrrXRxfR7coEgROjiG7VZyV1vgMOS9Ljg1A19wI
|
||||
cto6LtcCjpCGZsqU1/kBAkEAv2tXBts3vuIjguZNMz7KLWmu3zG2SQRaqdEZwL+R
|
||||
DQmD5tbI6gEtd5OmgmSiW8A4mpfgFYvG7Um2fwi7TTXtSA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
3
go.mod
3
go.mod
@@ -3,7 +3,8 @@ module github.com/int128/kubelogin
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible
|
||||
github.com/chromedp/chromedp v0.5.3
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/golang/mock v1.4.0
|
||||
github.com/google/go-cmp v0.4.0
|
||||
|
||||
18
go.sum
18
go.sum
@@ -13,11 +13,17 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac h1:T7V5BXqnYd55Hj/g5uhDYumg9Fp3rMTS6bykYtTIFX4=
|
||||
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
|
||||
github.com/chromedp/chromedp v0.5.3 h1:F9LafxmYpsQhWQBdCs+6Sret1zzeeFyHS5LkRF//Ffg=
|
||||
github.com/chromedp/chromedp v0.5.3/go.mod h1:YLdPtndaHQ4rCpSpBG+IPpy9JvX0VD+7aaLxYgYj28w=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -37,6 +43,12 @@ github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -90,6 +102,8 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -97,6 +111,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -188,6 +204,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
318
integration_test/credetial_plugin_test.go
Normal file
318
integration_test/credetial_plugin_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/integration_test/localserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the output.
|
||||
//
|
||||
func TestCredentialPlugin(t *testing.T) {
|
||||
tokenCacheDir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a cache dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tokenCacheDir); err != nil {
|
||||
t.Errorf("could not clean up the cache dir: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
TokenCacheDir: tokenCacheDir,
|
||||
Keys: keys.None,
|
||||
ExtraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
t.Run("CertFile", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
TokenCacheDir: tokenCacheDir,
|
||||
TokenCacheKey: tokencache.Key{CACertFilename: keys.Server.CACertPath},
|
||||
Keys: keys.Server,
|
||||
ExtraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--certificate-authority", keys.Server.CACertPath,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("CertData", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
TokenCacheDir: tokenCacheDir,
|
||||
TokenCacheKey: tokencache.Key{CACertData: keys.Server.CACertBase64},
|
||||
Keys: keys.Server,
|
||||
ExtraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--certificate-authority-data", keys.Server.CACertBase64,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type credentialPluginTestCase struct {
|
||||
TokenCacheDir string
|
||||
TokenCacheKey tokencache.Key
|
||||
Keys keys.Keys
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
func testCredentialPlugin(t *testing.T, tc credentialPluginTestCase) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(validIDToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
})
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &validIDToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: validIDToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", &validIDToken)
|
||||
provider.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &validIDToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: validIDToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "email profile openid", &idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
}
|
||||
|
||||
func newCredentialPluginWriterMock(t *testing.T, ctrl *gomock.Controller, idToken *string) *mock_credentialpluginwriter.MockInterface {
|
||||
writer := mock_credentialpluginwriter.NewMockInterface(ctrl)
|
||||
writer.EXPECT().
|
||||
Write(gomock.Any()).
|
||||
Do(func(got credentialpluginwriter.Output) {
|
||||
want := credentialpluginwriter.Output{
|
||||
Token: *idToken,
|
||||
Expiry: tokenExpiryFuture,
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
return writer
|
||||
}
|
||||
|
||||
func runGetTokenCmd(t *testing.T, ctx context.Context, b browser.Interface, w credentialpluginwriter.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(mock_logger.New(t), b, w)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin", "get-token",
|
||||
"--v=1",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, v tokencache.Value) {
|
||||
k := tc.TokenCacheKey
|
||||
k.IssuerURL = serverURL
|
||||
k.ClientID = "kubernetes"
|
||||
var r tokencache.Repository
|
||||
err := r.Save(tc.TokenCacheDir, k, v)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, want tokencache.Value) {
|
||||
k := tc.TokenCacheKey
|
||||
k.IssuerURL = serverURL
|
||||
k.ClientID = "kubernetes"
|
||||
var r tokencache.Repository
|
||||
got, err := r.FindByKey(tc.TokenCacheDir, k)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(&want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
93
integration_test/helpers_test.go
Normal file
93
integration_test/helpers_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
|
||||
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
|
||||
)
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: expiry.Unix(),
|
||||
}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func setupAuthCodeFlow(t *testing.T, provider *mock_idp.MockProvider, serverURL, scope string, idToken *string) {
|
||||
var nonce string
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
provider.EXPECT().AuthenticateCode(scope, gomock.Any()).
|
||||
DoAndReturn(func(_, gotNonce string) (string, error) {
|
||||
nonce = gotNonce
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
})
|
||||
provider.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
|
||||
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
|
||||
})
|
||||
}
|
||||
|
||||
func setupROPCFlow(provider *mock_idp.MockProvider, serverURL, scope, username, password, idToken string) {
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
provider.EXPECT().AuthenticatePassword(username, password, scope).
|
||||
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
|
||||
}
|
||||
|
||||
func newBrowserMock(ctx context.Context, t *testing.T, ctrl *gomock.Controller, k keys.Keys) browser.Interface {
|
||||
b := mock_browser.NewMockInterface(ctrl)
|
||||
b.EXPECT().
|
||||
Open(gomock.Any()).
|
||||
Do(func(url string) {
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: k.TLSConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
return b
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package idp provides a test double of the identity provider of OpenID Connect.
|
||||
package idp
|
||||
|
||||
import (
|
||||
@@ -10,16 +9,16 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func NewHandler(t *testing.T, service Service) *Handler {
|
||||
return &Handler{t, service}
|
||||
func NewHandler(t *testing.T, provider Provider) *Handler {
|
||||
return &Handler{t, provider}
|
||||
}
|
||||
|
||||
// Handler provides a HTTP handler for the identity provider of OpenID Connect.
|
||||
// You need to implement the Service interface.
|
||||
// Handler provides a HTTP handler for the OpenID Connect Provider.
|
||||
// You need to implement the Provider interface.
|
||||
// Note that this skips some security checks and is only for testing.
|
||||
type Handler struct {
|
||||
t *testing.T
|
||||
service Service
|
||||
t *testing.T
|
||||
provider Provider
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -58,14 +57,14 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
p := r.URL.Path
|
||||
switch {
|
||||
case m == "GET" && p == "/.well-known/openid-configuration":
|
||||
discoveryResponse := h.service.Discovery()
|
||||
discoveryResponse := h.provider.Discovery()
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w)
|
||||
if err := e.Encode(discoveryResponse); err != nil {
|
||||
return xerrors.Errorf("could not render json: %w", err)
|
||||
}
|
||||
case m == "GET" && p == "/certs":
|
||||
certificatesResponse := h.service.GetCertificates()
|
||||
certificatesResponse := h.provider.GetCertificates()
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w)
|
||||
if err := e.Encode(certificatesResponse); err != nil {
|
||||
@@ -76,7 +75,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
q := r.URL.Query()
|
||||
redirectURI, scope, state, nonce := q.Get("redirect_uri"), q.Get("scope"), q.Get("state"), q.Get("nonce")
|
||||
code, err := h.service.AuthenticateCode(scope, nonce)
|
||||
code, err := h.provider.AuthenticateCode(scope, nonce)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
@@ -92,7 +91,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 3.1.3.1. Token Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
||||
code := r.Form.Get("code")
|
||||
tokenResponse, err := h.service.Exchange(code)
|
||||
tokenResponse, err := h.provider.Exchange(code)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("token request error: %w", err)
|
||||
}
|
||||
@@ -105,7 +104,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 4.3. Resource Owner Password Credentials Grant
|
||||
// https://tools.ietf.org/html/rfc6749#section-4.3
|
||||
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
|
||||
tokenResponse, err := h.service.AuthenticatePassword(username, password, scope)
|
||||
tokenResponse, err := h.provider.AuthenticatePassword(username, password, scope)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
@@ -118,7 +117,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 12.1. Refresh Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
|
||||
refreshToken := r.Form.Get("refresh_token")
|
||||
tokenResponse, err := h.service.Refresh(refreshToken)
|
||||
tokenResponse, err := h.provider.Refresh(refreshToken)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("token refresh error: %w", err)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Package idp provides a test double of an OpenID Connect Provider.
|
||||
package idp
|
||||
|
||||
//go:generate mockgen -destination mock_idp/mock_service.go github.com/int128/kubelogin/e2e_test/idp Service
|
||||
//go:generate mockgen -destination mock_idp/mock_idp.go github.com/int128/kubelogin/e2e_test/idp Provider
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
@@ -9,11 +10,11 @@ import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// Service provides discovery and authentication methods.
|
||||
// Provider provides discovery and authentication methods.
|
||||
// If an implemented method returns an ErrorResponse,
|
||||
// the handler will respond 400 and corresponding json of the ErrorResponse.
|
||||
// Otherwise, the handler will respond 500 and fail the current test.
|
||||
type Service interface {
|
||||
type Provider interface {
|
||||
Discovery() *DiscoveryResponse
|
||||
GetCertificates() *CertificatesResponse
|
||||
AuthenticateCode(scope, nonce string) (code string, err error)
|
||||
@@ -1,40 +1,40 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/e2e_test/idp (interfaces: Service)
|
||||
// Source: github.com/int128/kubelogin/e2e_test/idp (interfaces: Provider)
|
||||
|
||||
// Package mock_idp is a generated GoMock package.
|
||||
package mock_idp
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
idp "github.com/int128/kubelogin/e2e_test/idp"
|
||||
idp "github.com/int128/kubelogin/integration_test/idp"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockService is a mock of Service interface
|
||||
type MockService struct {
|
||||
// MockProvider is a mock of Provider interface
|
||||
type MockProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockServiceMockRecorder
|
||||
recorder *MockProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockServiceMockRecorder is the mock recorder for MockService
|
||||
type MockServiceMockRecorder struct {
|
||||
mock *MockService
|
||||
// MockProviderMockRecorder is the mock recorder for MockProvider
|
||||
type MockProviderMockRecorder struct {
|
||||
mock *MockProvider
|
||||
}
|
||||
|
||||
// NewMockService creates a new mock instance
|
||||
func NewMockService(ctrl *gomock.Controller) *MockService {
|
||||
mock := &MockService{ctrl: ctrl}
|
||||
mock.recorder = &MockServiceMockRecorder{mock}
|
||||
// NewMockProvider creates a new mock instance
|
||||
func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
|
||||
mock := &MockProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockService) EXPECT() *MockServiceMockRecorder {
|
||||
func (m *MockProvider) EXPECT() *MockProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AuthenticateCode mocks base method
|
||||
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
|
||||
func (m *MockProvider) AuthenticateCode(arg0, arg1 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
|
||||
ret0, _ := ret[0].(string)
|
||||
@@ -43,13 +43,13 @@ func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
|
||||
}
|
||||
|
||||
// AuthenticateCode indicates an expected call of AuthenticateCode
|
||||
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockProviderMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockProvider)(nil).AuthenticateCode), arg0, arg1)
|
||||
}
|
||||
|
||||
// AuthenticatePassword mocks base method
|
||||
func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
|
||||
func (m *MockProvider) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
@@ -58,13 +58,13 @@ func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenR
|
||||
}
|
||||
|
||||
// AuthenticatePassword indicates an expected call of AuthenticatePassword
|
||||
func (mr *MockServiceMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
func (mr *MockProviderMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockService)(nil).AuthenticatePassword), arg0, arg1, arg2)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockProvider)(nil).AuthenticatePassword), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Discovery mocks base method
|
||||
func (m *MockService) Discovery() *idp.DiscoveryResponse {
|
||||
func (m *MockProvider) Discovery() *idp.DiscoveryResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Discovery")
|
||||
ret0, _ := ret[0].(*idp.DiscoveryResponse)
|
||||
@@ -72,13 +72,13 @@ func (m *MockService) Discovery() *idp.DiscoveryResponse {
|
||||
}
|
||||
|
||||
// Discovery indicates an expected call of Discovery
|
||||
func (mr *MockServiceMockRecorder) Discovery() *gomock.Call {
|
||||
func (mr *MockProviderMockRecorder) Discovery() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockService)(nil).Discovery))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockProvider)(nil).Discovery))
|
||||
}
|
||||
|
||||
// Exchange mocks base method
|
||||
func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
|
||||
func (m *MockProvider) Exchange(arg0 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Exchange", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
@@ -87,13 +87,13 @@ func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
|
||||
}
|
||||
|
||||
// Exchange indicates an expected call of Exchange
|
||||
func (mr *MockServiceMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockProviderMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockService)(nil).Exchange), arg0)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockProvider)(nil).Exchange), arg0)
|
||||
}
|
||||
|
||||
// GetCertificates mocks base method
|
||||
func (m *MockService) GetCertificates() *idp.CertificatesResponse {
|
||||
func (m *MockProvider) GetCertificates() *idp.CertificatesResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCertificates")
|
||||
ret0, _ := ret[0].(*idp.CertificatesResponse)
|
||||
@@ -101,13 +101,13 @@ func (m *MockService) GetCertificates() *idp.CertificatesResponse {
|
||||
}
|
||||
|
||||
// GetCertificates indicates an expected call of GetCertificates
|
||||
func (mr *MockServiceMockRecorder) GetCertificates() *gomock.Call {
|
||||
func (mr *MockProviderMockRecorder) GetCertificates() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockService)(nil).GetCertificates))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockProvider)(nil).GetCertificates))
|
||||
}
|
||||
|
||||
// Refresh mocks base method
|
||||
func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
|
||||
func (m *MockProvider) Refresh(arg0 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Refresh", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
@@ -116,7 +116,7 @@ func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
|
||||
}
|
||||
|
||||
// Refresh indicates an expected call of Refresh
|
||||
func (mr *MockServiceMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockProviderMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockService)(nil).Refresh), arg0)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockProvider)(nil).Refresh), arg0)
|
||||
}
|
||||
@@ -4,16 +4,21 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Keys represents a pair of certificate and key.
|
||||
type Keys struct {
|
||||
CertPath string
|
||||
KeyPath string
|
||||
CACertPath string
|
||||
TLSConfig *tls.Config
|
||||
CertPath string
|
||||
KeyPath string
|
||||
CACertPath string
|
||||
CACertBase64 string
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// None represents non-TLS.
|
||||
@@ -22,10 +27,11 @@ var None Keys
|
||||
// Server is a Keys for TLS server.
|
||||
// These files should be generated by Makefile before test.
|
||||
var Server = Keys{
|
||||
CertPath: "keys/testdata/server.crt",
|
||||
KeyPath: "keys/testdata/server.key",
|
||||
CACertPath: "keys/testdata/ca.crt",
|
||||
TLSConfig: newTLSConfig("keys/testdata/ca.crt"),
|
||||
CertPath: "keys/testdata/server.crt",
|
||||
KeyPath: "keys/testdata/server.key",
|
||||
CACertPath: "keys/testdata/ca.crt",
|
||||
CACertBase64: readAsBase64("keys/testdata/ca.crt"),
|
||||
TLSConfig: newTLSConfig("keys/testdata/ca.crt"),
|
||||
}
|
||||
|
||||
// JWSKey is path to the key for signing ID tokens.
|
||||
@@ -35,6 +41,23 @@ const JWSKey = "keys/testdata/jws.key"
|
||||
// JWSKeyPair is the key pair loaded from JWSKey.
|
||||
var JWSKeyPair = readPrivateKey(JWSKey)
|
||||
|
||||
func readAsBase64(name string) string {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
var s strings.Builder
|
||||
e := base64.NewEncoder(base64.StdEncoding, &s)
|
||||
if _, err := io.Copy(e, f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := e.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func newTLSConfig(name string) *tls.Config {
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
30
integration_test/keys/testdata/Makefile
vendored
Normal file
30
integration_test/keys/testdata/Makefile
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
EXPIRY := 3650
|
||||
OUTPUT_DIR := testdata
|
||||
TARGETS := ca.key
|
||||
TARGETS += ca.crt
|
||||
TARGETS += server.key
|
||||
TARGETS += server.crt
|
||||
TARGETS += jws.key
|
||||
|
||||
.PHONY: all
|
||||
all: $(TARGETS)
|
||||
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
ca.csr: ca.key
|
||||
openssl req -new -key ca.key -out $@ -subj "/CN=hello-ca" -config openssl.cnf
|
||||
ca.crt: ca.key ca.csr
|
||||
openssl x509 -req -in ca.csr -signkey ca.key -out $@ -days $(EXPIRY)
|
||||
server.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -new -key server.key -out $@ -subj "/CN=localhost" -config openssl.cnf
|
||||
server.crt: openssl.cnf server.csr ca.crt ca.key
|
||||
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out $@ -sha256 -days $(EXPIRY) -extensions v3_req -extfile openssl.cnf
|
||||
|
||||
jws.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -v $(TARGETS)
|
||||
17
integration_test/keys/testdata/ca.crt
vendored
Normal file
17
integration_test/keys/testdata/ca.crt
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICojCCAYoCCQCNsdXicWqF2DANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAho
|
||||
ZWxsby1jYTAeFw0yMDAyMDYwMTMzMzNaFw0zMDAyMDMwMTMzMzNaMBMxETAPBgNV
|
||||
BAMMCGhlbGxvLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxcPN
|
||||
dS88B6ewqVn/m9yO74OLIwNrqMci8l7olP9XlcJhxUZs+3WZQpsSj5nC4yEx8uPQ
|
||||
bTtBKnXXVDe+8k7OsLTruu9+isTaYk4o/TZbuw/N31ZAiT0pJw8hdypTQyMLbeDr
|
||||
Vl4bbrfbYywx30DyrHxUkgzOWs459Uwc1wWu0W7M21GY4KENHFE3OAcD58FMvvrh
|
||||
vgkslATwwW4M2UtXUFJ8XHh26g/J450DU2gwNxpcSdIsvFE6zSyAxU55RElph7mE
|
||||
ru9cNWAYhCRZvlZQ2VlH7C6JQ3SHyA9RZmBbPpXhtl9zavFkGx2MEwDp/3FmUukR
|
||||
yJnS2KnAo0QdBeS1LQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAXGfoDwuKY4TyF
|
||||
fhKg553Y5I6VDCDc97jyW9yyfc0kvPjQc4EGQV+1eQqMSeh2THpgEEKk9hH/g35a
|
||||
grPRcBTsEWpbQd16yWyulQeyOtPeWZB2FvAigMaAdMmeXlTs6++gJ6PjPuACa2Jl
|
||||
nJ/AjCqKFxkn0yEVkPTY0c/I9A12xhCmATqIrQiK1pPowiFxQb4M8Cm0z0AkaJZr
|
||||
iW0NCOJlLzBqRpFquL4umNaIsxTmOshfM70NpQGRjKREBuK6S0qWsRR0wz4b9Rvi
|
||||
62qW4zU94q2EDIoCItjHP4twGENXJDC0vLCsKfA5AvbPzszd5/4ifYe2C00Rn7/O
|
||||
lIxrspMm
|
||||
-----END CERTIFICATE-----
|
||||
17
integration_test/keys/testdata/ca.csr
vendored
Normal file
17
integration_test/keys/testdata/ca.csr
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICrDCCAZQCAQAwEzERMA8GA1UEAwwIaGVsbG8tY2EwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDFw811LzwHp7CpWf+b3I7vg4sjA2uoxyLyXuiU/1eV
|
||||
wmHFRmz7dZlCmxKPmcLjITHy49BtO0EqdddUN77yTs6wtOu6736KxNpiTij9Nlu7
|
||||
D83fVkCJPSknDyF3KlNDIwtt4OtWXhtut9tjLDHfQPKsfFSSDM5azjn1TBzXBa7R
|
||||
bszbUZjgoQ0cUTc4BwPnwUy++uG+CSyUBPDBbgzZS1dQUnxceHbqD8njnQNTaDA3
|
||||
GlxJ0iy8UTrNLIDFTnlESWmHuYSu71w1YBiEJFm+VlDZWUfsLolDdIfID1FmYFs+
|
||||
leG2X3Nq8WQbHYwTAOn/cWZS6RHImdLYqcCjRB0F5LUtAgMBAAGgVDBSBgkqhkiG
|
||||
9w0BCQ4xRTBDMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMBQGA1UdEQQNMAuCCWxv
|
||||
Y2FsaG9zdDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
o/Uthp3NWx0ydTn/GBZI+vA3gI7Qd9UwLvwkeinldRlPM9DOXpG6LR0C40f6u+fj
|
||||
mXvUDerveYJI8rpo+Ds0UVqy63AH/zZLG7M96L5Nv2KnK40bkfVNez858Yqp1u17
|
||||
/ci1ZsQIElU5v2qKozaHdQThDVtD5ZZdZoQwLvBLE/Dwpe/4VZZFh8smPMR+Mhcq
|
||||
+b7gpSy1RiUffk0ZMjuF9Nc9OODdQMTCf+86i0qWXGVzkhfHKAGv+xarHEztcmxF
|
||||
GUgUYW8DMvYBjEzaGRM1n0aIFkQO6y8SUXvGMIGBSMC4jfBH3ghIXg1+nD6Uah4D
|
||||
16r+CLjFsDUdn/DK/fIQVw==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
27
integration_test/keys/testdata/ca.key
vendored
Normal file
27
integration_test/keys/testdata/ca.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAxcPNdS88B6ewqVn/m9yO74OLIwNrqMci8l7olP9XlcJhxUZs
|
||||
+3WZQpsSj5nC4yEx8uPQbTtBKnXXVDe+8k7OsLTruu9+isTaYk4o/TZbuw/N31ZA
|
||||
iT0pJw8hdypTQyMLbeDrVl4bbrfbYywx30DyrHxUkgzOWs459Uwc1wWu0W7M21GY
|
||||
4KENHFE3OAcD58FMvvrhvgkslATwwW4M2UtXUFJ8XHh26g/J450DU2gwNxpcSdIs
|
||||
vFE6zSyAxU55RElph7mEru9cNWAYhCRZvlZQ2VlH7C6JQ3SHyA9RZmBbPpXhtl9z
|
||||
avFkGx2MEwDp/3FmUukRyJnS2KnAo0QdBeS1LQIDAQABAoIBAHSoWtMsaMnPNlu/
|
||||
thM32K0auIGP6/rkdQ3pxGLX+M9jmY7oSzNOHHj4xssklZyroS45CmLU2Ez2tG1+
|
||||
cMm4iR4dqwxbaBbtpjDlEDLF1PiUiwmadHlANb1PpJsJwZHR41UOn2QUITR/ig+H
|
||||
K2gZhM0QjkaU/Uj9a5zyJ/UC6iupmgCtj8ij65B49qKMODxV4gqZstRSZiJ3gb6R
|
||||
TBeR3PUWQS62MZueEQz2eF0eXkXKsFWcbLfHArjfIJ533zW68A0vabgqOhCwJTks
|
||||
+rkyaKUjEwJJQcgpCfUI4t9HAwICqYtw0fQdDDaMak2XTDFnGHn1/VfSi818U5Sa
|
||||
jZ+/uIECgYEA4uCd5ISsLWebSv2r5f97N8+WcW0MSJ0XP5MniiMvlaNOtJmoN3H0
|
||||
PlgR3uwpH9TNWHITZEEb/I93r2E3f24v2018g0uJuwiRDusxHs7yuxw/93Y5rwtG
|
||||
3twL1k0GyeLCxfM2y2QQU/awXISx7nNk2f0umvTWmjrEw632oQSWCSECgYEA3yaF
|
||||
zDk7k1u1GdZAh+pQAyUtjzHSvQjiE2JzLaH8BTorACrczRascX5yyMi0QfLYnoYt
|
||||
UL9dCb4Z8HUclj55kBSx8anvVtf3XAT3hZ3sm8LV3JLGDeURGS8rGdV+tyFk7zw9
|
||||
XgvaLj3xjB5CoJvtOhbXvqF9M9yp4TMBb5El7o0CgYBTx5Bm15thNPZCrgQxbbOJ
|
||||
u42ZmyRDGEeCgYvDVhT3VBP3WxqkRt9juk/3GwxgpcuikpWYmvaDwFL5H5RH6V+g
|
||||
wy9sqJNWzuYKNU2xS8iU0ezJLA5HFon4OBfi7hTIroUwZgzg9LWW2+zqbVHrdQ9T
|
||||
9Eumiy1ITNVmUTJW6YOiIQKBgBE4BsEAdZFkVTAeMTKLqQrlFoPjI1DE27UFNsAB
|
||||
rNG2cFT9+bW1ly7WxAKsQgSIuaBZ2CtP6Nz0l0nPr5oEThsJDcYJB9faqFKoa3Ua
|
||||
/4PxX9E6Xh/6WfxogFno+HMnF4PCUTXtkjNZQkc+moOMJJ0D4DfsfB3BXDZtWiIC
|
||||
wDuNAoGBAN5ZFTXwE1VpWxIWq5dV9+0aVOP/eB+h7Pt+u2xyRip31FGcpGEzGniu
|
||||
VmOzWApW7NmvD0QWtstJWlCpsw+rOptAL0oVunlB70KuYuA+2Q5g/Cw3kUrvXqbe
|
||||
mkCoV21eFAmpU+I6z/n7pUDXPuUsmTkzmwedv0HUuNSQJmHZOMPD
|
||||
-----END RSA PRIVATE KEY-----
|
||||
1
integration_test/keys/testdata/ca.srl
vendored
Normal file
1
integration_test/keys/testdata/ca.srl
vendored
Normal file
@@ -0,0 +1 @@
|
||||
9E12C7A1AF348811
|
||||
27
integration_test/keys/testdata/jws.key
vendored
Normal file
27
integration_test/keys/testdata/jws.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAvKJxjhaEnV/64i+GBDn4+PUno693bQBumE6a/pUf5RC8faZu
|
||||
dpj7vFJsTPHqHNqR5EIKCGZMlYvnxXbOJDElqbDkRtFG4l3k/WXAea5rE4v+AKDm
|
||||
/49dY6SLgH7iP2Z2gmgbqICjDFyn89Mye3H4ZEY0dHiY/7Fbhg637+aaE/CkPNue
|
||||
LFn3UjqmytkxiBhLX8H6zQU6RP6IVFpSjuJ6TfRVvJ4lg1ThxX+rKqUDzIdF2FNe
|
||||
1KBYfEKV1J9ZY671qrV07H8jw42Kw8/hmGECb1hr93JOu0odWM6EhXhig3oL299c
|
||||
2jlQAh4y04uUybS6xYhZxrk6FcpzzaJO/eRPHQIDAQABAoIBABhtlPUIl33l2xCF
|
||||
hP5xH3vmC48X/wg/oRLaQxoq56l7ZF2FOxLitt7pcZr5TQ8VgwUjRDdYQByxtH8O
|
||||
5p0rPCxgev9sxJg1/pyOG8HmQ3mRjIA6Vg/MWhS4T1SBmf0J4Nj8cHB+0B6etSVP
|
||||
OV9hIACkUtCueWnLZwXSTCGmJFfmfeKQSM3yyF3BxQDpyAQFdzCbIwQsTmSnZOZx
|
||||
5wJ8Fv/BMrqKBje158ISePBZWz54eBnFc2VKKDRxj16e5Ni0BhCwpgDI7MRUzKVS
|
||||
qKAkCwsUcmXpRZPUU5mDB6yPJU8DZwTcS5L1PWUIUnY3mlosukVUxTaoJigwulr9
|
||||
RXp9tmUCgYEA6x40cnT2sibSPuytycs4Tpg5UjeCkPMNCscX+x9hdSWjJkC7eolH
|
||||
qTHemjC896ExTMMFzpFKjymJbdeMn6BslOt6RvHnXugJ8IRi10JgZYHFQ/hr6A3+
|
||||
SsPA7cT712Ya63i19WWCySYDccy33qGt2rtdhJlrGKEfnTiNjEMw1hMCgYEAzWNc
|
||||
ae/tWbmYhjEoFt/pJp1Lb/zIDRpjV4zfeyr/wPY9Q0d/llKUISPdi1slQxpNTHqT
|
||||
idiRnc8Qweisqt+84UiTU9JEG7D3T71SA+MO57NIm2wTt/7U7yb3abMZZijIPw5U
|
||||
6td5jh78dGT3WRPgAsGnACXA2WJchD5m0nthrA8CgYEAiCMeJSPab/8Qf8TVP+G+
|
||||
gaucjSF9JWbGJ3ZuSUa7THR1ikGzDFmOt8YbaVZNJGkePZ8yro/sBwb6/zHux8LA
|
||||
/F14mLmayZY7oxtUi+VwIXZJfXjLKjtoAWxlOodzdx40+iET4rpbRxMOrYbm9C7T
|
||||
lrIkjRG0NDefMY68TvncvicCgYBNtkO4PbTj1yqT07OkfBI+rxNlCxMyigJ+lOnW
|
||||
M53TiBgEBeCLozEzHNvtp44AxsnqnxKF/LCUMk3X4M68VK2l3A0KkSt+AsaAoFSQ
|
||||
7e+s0ZQuYoVPgBdXaboBf2ej1Nh3q1eMB/2RPb4t2CoSxUdkI5upnZ9LYUE6NFY5
|
||||
W7/IFwKBgAdQxrwfQmXqtuHwiorfCm3AS/w5xmGjbBTPHnZWme2zrmFTOKSWsxAs
|
||||
XAVU9B+RXJsPehC84SEreHtmZi79RRxHZOZhrl4oxrYth83QuzsR546kJwadBOj0
|
||||
91yAjrekXHI3YHFQJRFUBvlswgZKDiqP74DSBbmXwfO/h2wNUYyZ
|
||||
-----END RSA PRIVATE KEY-----
|
||||
14
integration_test/keys/testdata/openssl.cnf
vendored
Normal file
14
integration_test/keys/testdata/openssl.cnf
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = @alt_names
|
||||
extendedKeyUsage = serverAuth
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = localhost
|
||||
18
integration_test/keys/testdata/server.crt
vendored
Normal file
18
integration_test/keys/testdata/server.crt
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC7zCCAdegAwIBAgIJAJ4Sx6GvNIgRMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
|
||||
BAMMCGhlbGxvLWNhMB4XDTIwMDIwNjAxMzMzM1oXDTMwMDIwMzAxMzMzM1owFDES
|
||||
MBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F7yVyphLtJr+/AxI8k5Er
|
||||
MFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4TKyVaU0XK41ZSbDAchbW
|
||||
349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTkC3gSVF92+TzrrlbkZ5+W
|
||||
YVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDgkGuwwhzXfVUpxJ/DYLXZ
|
||||
yHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV+2ovP0Nggn6XJNh/g1QK
|
||||
o4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABo0UwQzAJBgNVHRMEAjAAMAsGA1UdDwQE
|
||||
AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
|
||||
DQYJKoZIhvcNAQELBQADggEBACqlq8b7trNRtKUm1PbY7dnrAFCOnV4OT2R98s17
|
||||
Q6tmCXM1DvQ101W0ih/lh6iPyU4JM2A0kvO+gizuL/Dmvb6oh+3ox0mMLposptso
|
||||
gCE1K3SXlvlcLdM6hXRJ5+XwlSCHM6o2Y4yABnKjT6Zr+CMh86a5abDx33hkJ4QR
|
||||
6I+/iBHVLiCVv0wUF3jD/T+HxinEQrB4cQsSgKmfPClrc8n7rkWWE+MdwEL4VZCH
|
||||
ufabw178aYibVvJ8k3rushjLlftkDyCNno2rz8YWrnaxabVR0EqSPInyYQenmv2r
|
||||
/CedVKpmdH2RV8ubkwqa0s6cmpRkyu6FS2g3LviJhmm0nwQ=
|
||||
-----END CERTIFICATE-----
|
||||
17
integration_test/keys/testdata/server.csr
vendored
Normal file
17
integration_test/keys/testdata/server.csr
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICrTCCAZUCAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F
|
||||
7yVyphLtJr+/AxI8k5ErMFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4
|
||||
TKyVaU0XK41ZSbDAchbW349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTk
|
||||
C3gSVF92+TzrrlbkZ5+WYVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDg
|
||||
kGuwwhzXfVUpxJ/DYLXZyHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV
|
||||
+2ovP0Nggn6XJNh/g1QKo4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABoFQwUgYJKoZI
|
||||
hvcNAQkOMUUwQzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAUBgNVHREEDTALggls
|
||||
b2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEB
|
||||
ACUQ0j3XuUZY3fso5tEgOGVjVsTU+C4pIsEw3e1KyIB93i56ANKaq1uBLtnlWtYi
|
||||
R4RxZ3Sf08GzoEHAHpWoQ4BQ3WGDQjxSdezbudWMuNnNyvyhkh36tmmp/PLA4iZD
|
||||
Q/d1odzGWg1HMqY0/Q3hfz40MQ9IEBBm+5zKw3tLsKNIKdSdlY7Ul3Z9PUsqsOVW
|
||||
uF0LKsTMEh1CpbYnOBS2EQjComVM5kYfdQwDNh+BMok8rH7mHFYRmrwYUrU9njsM
|
||||
eoKqhLkoSu6hw1Cgd9Yru5lC541KfxsSN4Cj6rkm+Qv/5zjvIPxYZ+akSQQR9hR3
|
||||
2O9THHKqaQS0xmD+y8NjfYk=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
27
integration_test/keys/testdata/server.key
vendored
Normal file
27
integration_test/keys/testdata/server.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F7yVyphLt
|
||||
Jr+/AxI8k5ErMFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4TKyVaU0X
|
||||
K41ZSbDAchbW349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTkC3gSVF92
|
||||
+TzrrlbkZ5+WYVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDgkGuwwhzX
|
||||
fVUpxJ/DYLXZyHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV+2ovP0Ng
|
||||
gn6XJNh/g1QKo4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABAoIBAEYvWFb4C0wurOj2
|
||||
ABrvhP2Ekaesh4kxMp+zgTlqxzsTJnWarS/gjKcPBm9KJon3La3P3tnd7y3pBXcV
|
||||
2F0IBl4DTHRpBTUS6HBVnENc8LnJgK2dHZkOLPyYtRLrA0Et+A73PQ2hNmchC+5V
|
||||
b9K9oQH0Pvim1gCHocnrSIi480hQPazO9/gnHvFtu9Tdsx9kM9jgChhh5VaR+nzM
|
||||
uw6MpUSzCri3/W6K5Hz8F1lLQcZ6o3vyyFtLPjFrWT4J1UiHqAoxuCGeWTvMbGQl
|
||||
9Cg0SMX4FLBpbIoMonhM3hb9Cw3FVRGU/1i4oF+gEIGlX/v9CPT5HAZmD59rawc8
|
||||
11x7yTECgYEA8EYVGKauLHeI1nJdCoVrWI73W1wtwt6prJbzltpoAR4tb3Qu6gBX
|
||||
JQNd+Ifhl28lK6lPnCGk/SsmfiKSp/XE4IyS1GBd2LpxWj5R50GePCg4hAzk9Xyx
|
||||
M9SJAFQw73pODgu4RWFXTCOMo9crahJ6X/O+MckHqbFqqohJ8nChGDkCgYEA6oro
|
||||
Ql/ymO7UdYCay7OlzKeg8ud6XgkZ+wkpSG/5TQ0QZWVN3aSOLztEdVfh3PAqWob0
|
||||
KgVhLmq6CYwq+HSzU92bvFqCgYUMU8tYzeRboGLxyEdAGL+EW0j8wQyZKYR5cNz4
|
||||
yM0hpM6kbJ0zZqkYVM/XfRT2RTGwTMakF/FOHZcCgYEAmf4guTriuIcoAWEstniK
|
||||
Myj16ezrO1Df6Eia+B0kuUqxDhSlmL39HDDLQmU8NYU7in8qEcQSbVwBgKgB3HoM
|
||||
42nVFR5qJ2RfD9qPPar1klKo3iExgRCYtcJKyBYtgt6dNi1WvcjEXX0PP1bBcWtE
|
||||
WUjrphbUvXKDDabp1eNPrCkCgYBOw/F2APTeyS4Oe+8AQ8eFcDIMARLGK7ZO6Oe1
|
||||
TO1jI+UCuD+rFI0vbW7zHV1brkf6+OFcj0vwo6TweeMgZ0il/IFFgvva9UyLg3nC
|
||||
Q1NGDJR4Fv1+kiqn4V4IkuuI1tVVws/F16XZzA/J7g0KB/WE3fvXJMgDuskjL36C
|
||||
D+aU5wKBgQCvEg+mJYny1QiR/mQuowX34xf4CkMl7Xq9YDH7W/3AlwuPrPNHaZjh
|
||||
SvSCz9I8vV0E4ur6atazgCblnvA/G3d4r8YYx+e1l30WJHMgoZRxxHMH74tmUPAj
|
||||
Klic16BJikSQclMeFdlJqf2UHd37eEuYnxorpep8YGP7/eN1ghHWAw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
)
|
||||
|
||||
type Shutdowner interface {
|
||||
@@ -1,4 +1,4 @@
|
||||
package e2e_test
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,14 +7,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/integration_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/integration_test/localserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
)
|
||||
|
||||
// Run the integration tests of the Login use-case.
|
||||
@@ -43,11 +44,12 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
@@ -57,7 +59,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -71,11 +73,12 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupMockIDPForROPC(service, serverURL, "openid", "USER", "PASS", idToken)
|
||||
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
@@ -87,7 +90,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -101,10 +104,11 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
@@ -117,7 +121,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -131,14 +135,15 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
@@ -151,7 +156,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
@@ -165,14 +170,15 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
|
||||
provider.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
@@ -185,7 +191,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -199,11 +205,12 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
@@ -216,7 +223,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -230,11 +237,12 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "profile groups openid", &idToken)
|
||||
setupAuthCodeFlow(t, provider, serverURL, "profile groups openid", &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
@@ -246,7 +254,7 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -254,14 +262,13 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
})
|
||||
}
|
||||
|
||||
func runRootCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, args []string) {
|
||||
func runRootCmd(t *testing.T, ctx context.Context, b browser.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, nil)
|
||||
cmd := di.NewCmdForHeadless(mock_logger.New(t), b, nil)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin",
|
||||
"--v=1",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
"--skip-open-browser",
|
||||
}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
34
pkg/adaptors/browser/browser.go
Normal file
34
pkg/adaptors/browser/browser.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/pkg/browser"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_browser/mock_browser.go github.com/int128/kubelogin/pkg/adaptors/browser Interface
|
||||
|
||||
func init() {
|
||||
// In credential plugin mode, some browser launcher writes a message to stdout
|
||||
// and it may break the credential json for client-go.
|
||||
// This prevents the browser launcher from breaking the credential json.
|
||||
browser.Stdout = os.Stderr
|
||||
}
|
||||
|
||||
// Set provides an implementation and interface for Env.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Browser)),
|
||||
wire.Bind(new(Interface), new(*Browser)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Open(url string) error
|
||||
}
|
||||
|
||||
type Browser struct{}
|
||||
|
||||
// Open opens the default browser.
|
||||
func (*Browser) Open(url string) error {
|
||||
return browser.OpenURL(url)
|
||||
}
|
||||
47
pkg/adaptors/browser/mock_browser/mock_browser.go
Normal file
47
pkg/adaptors/browser/mock_browser/mock_browser.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/browser (interfaces: Interface)
|
||||
|
||||
// Package mock_browser is a generated GoMock package.
|
||||
package mock_browser
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Open mocks base method
|
||||
func (m *MockInterface) Open(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Open", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Open indicates an expected call of Open
|
||||
func (mr *MockInterfaceMockRecorder) Open(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockInterface)(nil).Open), arg0)
|
||||
}
|
||||
@@ -205,6 +205,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--certificate-authority-data", "BASE64ENCODED",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--grant-type", "authcode",
|
||||
@@ -221,6 +222,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
CACertFilename: "/path/to/cacert",
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
|
||||
@@ -16,7 +16,8 @@ type getTokenOptions struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CertificateAuthority string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
TokenCacheDir string
|
||||
authenticationOptions authenticationOptions
|
||||
@@ -28,7 +29,8 @@ func (o *getTokenOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded data for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
|
||||
o.authenticationOptions.register(f)
|
||||
@@ -66,7 +68,8 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
CACertFilename: o.CACertFilename,
|
||||
CACertData: o.CACertData,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
TokenCacheDir: o.TokenCacheDir,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
|
||||
@@ -15,7 +15,8 @@ type setupOptions struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CertificateAuthority string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
@@ -26,7 +27,8 @@ func (o *setupOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded data for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
o.authenticationOptions.register(f)
|
||||
}
|
||||
@@ -51,7 +53,8 @@ func (cmd *Setup) New(ctx context.Context) *cobra.Command {
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
CACertFilename: o.CACertFilename,
|
||||
CACertData: o.CACertData,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package credentialplugin provides interaction with kubectl for a credential plugin.
|
||||
package credentialplugin
|
||||
// Package credentialpluginwriter provides a writer for a credential plugin.
|
||||
package credentialpluginwriter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_credentialplugin/mock_credentialplugin.go github.com/int128/kubelogin/pkg/adaptors/credentialplugin Interface
|
||||
//go:generate mockgen -destination mock_credentialpluginwriter/mock_credentialpluginwriter.go github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter Interface
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Interaction), "*"),
|
||||
wire.Bind(new(Interface), new(*Interaction)),
|
||||
wire.Struct(new(Writer), "*"),
|
||||
wire.Bind(new(Interface), new(*Writer)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
@@ -29,10 +29,10 @@ type Output struct {
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type Interaction struct{}
|
||||
type Writer struct{}
|
||||
|
||||
// Write writes the ExecCredential to standard output for kubectl.
|
||||
func (*Interaction) Write(out Output) error {
|
||||
func (*Writer) Write(out Output) error {
|
||||
ec := &v1beta1.ExecCredential{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
@@ -1,12 +1,12 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialplugin (interfaces: Interface)
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter (interfaces: Interface)
|
||||
|
||||
// Package mock_credentialplugin is a generated GoMock package.
|
||||
package mock_credentialplugin
|
||||
// Package mock_credentialpluginwriter is a generated GoMock package.
|
||||
package mock_credentialpluginwriter
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
credentialplugin "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
credentialpluginwriter "github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
|
||||
func (m *MockInterface) Write(arg0 credentialpluginwriter.Output) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Write", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
17
pkg/adaptors/env/env.go
vendored
17
pkg/adaptors/env/env.go
vendored
@@ -10,20 +10,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/pkg/browser"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_env/mock_env.go github.com/int128/kubelogin/pkg/adaptors/env Interface
|
||||
|
||||
func init() {
|
||||
// In credential plugin mode, some browser launcher writes a message to stdout
|
||||
// and it may break the credential json for client-go.
|
||||
// This prevents the browser launcher from breaking the credential json.
|
||||
browser.Stdout = os.Stderr
|
||||
}
|
||||
|
||||
// Set provides an implementation and interface for Env.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Env), "*"),
|
||||
@@ -33,7 +25,6 @@ var Set = wire.NewSet(
|
||||
type Interface interface {
|
||||
ReadString(prompt string) (string, error)
|
||||
ReadPassword(prompt string) (string, error)
|
||||
OpenBrowser(url string) error
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
@@ -69,14 +60,6 @@ func (*Env) ReadPassword(prompt string) (string, error) {
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// OpenBrowser opens the default browser.
|
||||
func (env *Env) OpenBrowser(url string) error {
|
||||
if err := browser.OpenURL(url); err != nil {
|
||||
return xerrors.Errorf("could not open the browser: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now returns the current time.
|
||||
func (*Env) Now() time.Time {
|
||||
return time.Now()
|
||||
|
||||
14
pkg/adaptors/env/mock_env/mock_env.go
vendored
14
pkg/adaptors/env/mock_env/mock_env.go
vendored
@@ -47,20 +47,6 @@ func (mr *MockInterfaceMockRecorder) Now() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockInterface)(nil).Now))
|
||||
}
|
||||
|
||||
// OpenBrowser mocks base method
|
||||
func (m *MockInterface) OpenBrowser(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OpenBrowser indicates an expected call of OpenBrowser
|
||||
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
|
||||
}
|
||||
|
||||
// ReadPassword mocks base method
|
||||
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -32,6 +32,7 @@ type Key struct {
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
|
||||
13
pkg/di/di.go
13
pkg/di/di.go
@@ -5,9 +5,10 @@ package di
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/cmd"
|
||||
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
)
|
||||
@@ -27,19 +28,19 @@ func NewCmd() cmd.Interface {
|
||||
|
||||
// dependencies for production
|
||||
logger.Set,
|
||||
wire.Value(authentication.DefaultLocalServerReadyFunc),
|
||||
credentialPluginAdaptor.Set,
|
||||
browser.Set,
|
||||
credentialpluginwriter.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
|
||||
func NewCmdForHeadless(logger.Interface, authentication.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
|
||||
func NewCmdForHeadless(logger.Interface, browser.Interface, credentialpluginwriter.Interface) cmd.Interface {
|
||||
wire.Build(
|
||||
// use-cases
|
||||
authentication.Set,
|
||||
standalone.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
credentialplugin.Set,
|
||||
setup.Set,
|
||||
|
||||
// adaptors
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
)
|
||||
@@ -25,24 +26,20 @@ import (
|
||||
|
||||
func NewCmd() cmd.Interface {
|
||||
loggerInterface := logger.New()
|
||||
localServerReadyFunc := _wireLocalServerReadyFuncValue
|
||||
interaction := &credentialplugin.Interaction{}
|
||||
cmdInterface := NewCmdForHeadless(loggerInterface, localServerReadyFunc, interaction)
|
||||
browserBrowser := &browser.Browser{}
|
||||
writer := &credentialpluginwriter.Writer{}
|
||||
cmdInterface := NewCmdForHeadless(loggerInterface, browserBrowser, writer)
|
||||
return cmdInterface
|
||||
}
|
||||
|
||||
var (
|
||||
_wireLocalServerReadyFuncValue = authentication.DefaultLocalServerReadyFunc
|
||||
)
|
||||
|
||||
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc authentication.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
|
||||
func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browser.Interface, credentialpluginwriterInterface credentialpluginwriter.Interface) cmd.Interface {
|
||||
newFunc := _wireNewFuncValue
|
||||
decoder := &jwtdecoder.Decoder{}
|
||||
envEnv := &env.Env{}
|
||||
authCode := &authentication.AuthCode{
|
||||
Env: envEnv,
|
||||
Logger: loggerInterface,
|
||||
LocalServerReadyFunc: localServerReadyFunc,
|
||||
Env: envEnv,
|
||||
Browser: browserInterface,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
authCodeKeyboard := &authentication.AuthCodeKeyboard{
|
||||
Env: envEnv,
|
||||
@@ -76,11 +73,11 @@ func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc au
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
getToken := &credentialplugin.GetToken{
|
||||
Authentication: authenticationAuthentication,
|
||||
TokenCacheRepository: repository,
|
||||
NewCertPool: certpoolNewFunc,
|
||||
Interaction: credentialpluginInterface,
|
||||
Writer: credentialpluginwriterInterface,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdGetToken := &cmd.GetToken{
|
||||
|
||||
@@ -3,6 +3,7 @@ package authentication
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
@@ -13,9 +14,9 @@ import (
|
||||
|
||||
// AuthCode provides the authentication code flow.
|
||||
type AuthCode struct {
|
||||
Env env.Interface
|
||||
Logger logger.Interface
|
||||
LocalServerReadyFunc LocalServerReadyFunc // only for e2e tests
|
||||
Env env.Interface
|
||||
Browser browser.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.Interface) (*Output, error) {
|
||||
@@ -45,15 +46,15 @@ func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
u.Logger.Printf("Open %s for authentication", url)
|
||||
if u.LocalServerReadyFunc != nil {
|
||||
u.LocalServerReadyFunc(url)
|
||||
}
|
||||
if o.SkipOpenBrowser {
|
||||
u.Logger.Printf("Please visit the following URL in your browser: %s", url)
|
||||
return nil
|
||||
}
|
||||
if err := u.Env.OpenBrowser(url); err != nil {
|
||||
u.Logger.V(1).Infof("could not open the browser: %s", err)
|
||||
if err := u.Browser.Open(url); err != nil {
|
||||
u.Logger.Printf(`error: could not open the browser: %s
|
||||
|
||||
Please visit the following URL in your browser manually: %s`, err, url)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
|
||||
@@ -78,12 +78,12 @@ func TestAuthCode_Do(t *testing.T) {
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockEnv := mock_env.NewMockInterface(ctrl)
|
||||
mockEnv.EXPECT().
|
||||
OpenBrowser("LOCAL_SERVER_URL")
|
||||
mockBrowser := mock_browser.NewMockInterface(ctrl)
|
||||
mockBrowser.EXPECT().
|
||||
Open("LOCAL_SERVER_URL")
|
||||
u := AuthCode{
|
||||
Logger: mock_logger.New(t),
|
||||
Env: mockEnv,
|
||||
Logger: mock_logger.New(t),
|
||||
Browser: mockBrowser,
|
||||
}
|
||||
got, err := u.Do(ctx, o, mockOIDCClient)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,12 +24,6 @@ var Set = wire.NewSet(
|
||||
wire.Struct(new(ROPC), "*"),
|
||||
)
|
||||
|
||||
// LocalServerReadyFunc provides an extension point for e2e tests.
|
||||
type LocalServerReadyFunc func(url string)
|
||||
|
||||
// DefaultLocalServerReadyFunc is the default noop function.
|
||||
var DefaultLocalServerReadyFunc = LocalServerReadyFunc(nil)
|
||||
|
||||
type Interface interface {
|
||||
Do(ctx context.Context, in Input) (*Output, error)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
@@ -32,7 +32,8 @@ type Input struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
CACertFilename string // If set, use the CA cert
|
||||
CACertFilename string // optional
|
||||
CACertData string // optional
|
||||
SkipTLSVerify bool
|
||||
TokenCacheDir string
|
||||
GrantOptionSet authentication.GrantOptionSet
|
||||
@@ -42,7 +43,7 @@ type GetToken struct {
|
||||
Authentication authentication.Interface
|
||||
TokenCacheRepository tokencache.Interface
|
||||
NewCertPool certpool.NewFunc
|
||||
Interaction credentialplugin.Interface
|
||||
Writer credentialpluginwriter.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
return xerrors.Errorf("could not get a token from the cache or provider: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("writing the token to client-go")
|
||||
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenClaims.Expiry}); err != nil {
|
||||
if err := u.Writer.Write(credentialpluginwriter.Output{Token: out.IDToken, Expiry: out.IDTokenClaims.Expiry}); err != nil {
|
||||
return xerrors.Errorf("could not write the token to client-go: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -67,6 +68,7 @@ func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
CACertFilename: in.CACertFilename,
|
||||
CACertData: in.CACertData,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
}
|
||||
tokenCacheValue, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, tokenCacheKey)
|
||||
@@ -77,7 +79,12 @@ func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*
|
||||
certPool := u.NewCertPool()
|
||||
if in.CACertFilename != "" {
|
||||
if err := certPool.AddFile(in.CACertFilename); err != nil {
|
||||
return nil, xerrors.Errorf("could not load the certificate: %w", err)
|
||||
return nil, xerrors.Errorf("could not load the certificate file: %w", err)
|
||||
}
|
||||
}
|
||||
if in.CACertData != "" {
|
||||
if err := certPool.AddBase64Encoded(in.CACertData); err != nil {
|
||||
return nil, xerrors.Errorf("could not load the certificate data: %w", err)
|
||||
}
|
||||
}
|
||||
out, err := u.Authentication.Do(ctx, authentication.Input{
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache/mock_tokencache"
|
||||
@@ -37,12 +37,15 @@ func TestGetToken_Do(t *testing.T) {
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
CACertFilename: "/path/to/cert",
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
mockCertPool := mock_certpool.NewMockInterface(ctrl)
|
||||
mockCertPool.EXPECT().
|
||||
AddFile("/path/to/cert")
|
||||
mockCertPool.EXPECT().
|
||||
AddBase64Encoded("BASE64ENCODED")
|
||||
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, authentication.Input{
|
||||
@@ -66,6 +69,7 @@ func TestGetToken_Do(t *testing.T) {
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
CACertFilename: "/path/to/cert",
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
}).
|
||||
Return(nil, xerrors.New("file not found"))
|
||||
@@ -76,15 +80,16 @@ func TestGetToken_Do(t *testing.T) {
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
CACertFilename: "/path/to/cert",
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
},
|
||||
tokencache.Value{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(credentialplugin.Output{
|
||||
credentialPluginWriter := mock_credentialpluginwriter.NewMockInterface(ctrl)
|
||||
credentialPluginWriter.EXPECT().
|
||||
Write(credentialpluginwriter.Output{
|
||||
Token: "YOUR_ID_TOKEN",
|
||||
Expiry: dummyTokenClaims.Expiry,
|
||||
})
|
||||
@@ -92,7 +97,7 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Authentication: mockAuthentication,
|
||||
TokenCacheRepository: tokenCacheRepository,
|
||||
NewCertPool: func() certpool.Interface { return mockCertPool },
|
||||
Interaction: credentialPluginInteraction,
|
||||
Writer: credentialPluginWriter,
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
if err := u.Do(ctx, in); err != nil {
|
||||
@@ -135,9 +140,9 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Return(&tokencache.Value{
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
}, nil)
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(credentialplugin.Output{
|
||||
credentialPluginWriter := mock_credentialpluginwriter.NewMockInterface(ctrl)
|
||||
credentialPluginWriter.EXPECT().
|
||||
Write(credentialpluginwriter.Output{
|
||||
Token: "VALID_ID_TOKEN",
|
||||
Expiry: dummyTokenClaims.Expiry,
|
||||
})
|
||||
@@ -145,7 +150,7 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Authentication: mockAuthentication,
|
||||
TokenCacheRepository: tokenCacheRepository,
|
||||
NewCertPool: func() certpool.Interface { return mockCertPool },
|
||||
Interaction: credentialPluginInteraction,
|
||||
Writer: credentialPluginWriter,
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
if err := u.Do(ctx, in); err != nil {
|
||||
@@ -185,7 +190,7 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Authentication: mockAuthentication,
|
||||
TokenCacheRepository: tokenCacheRepository,
|
||||
NewCertPool: func() certpool.Interface { return mockCertPool },
|
||||
Interaction: mock_credentialplugin.NewMockInterface(ctrl),
|
||||
Writer: mock_credentialpluginwriter.NewMockInterface(ctrl),
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
if err := u.Do(ctx, in); err == nil {
|
||||
|
||||
@@ -10,11 +10,18 @@ import (
|
||||
)
|
||||
|
||||
var stage2Tpl = template.Must(template.New("").Parse(`
|
||||
## 3. Bind a role
|
||||
## 2. Verify authentication
|
||||
|
||||
Run the following command:
|
||||
You got a token with the following claims:
|
||||
|
||||
{{- range $k, $v := .Claims }}
|
||||
{{ $k }}={{ $v }}
|
||||
{{- end }}
|
||||
|
||||
## 3. Bind a cluster role
|
||||
|
||||
Apply the following manifest:
|
||||
|
||||
kubectl apply -f - <<-EOF
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
@@ -26,7 +33,6 @@ Run the following command:
|
||||
subjects:
|
||||
- kind: User
|
||||
name: {{ .IssuerURL }}#{{ .Subject }}
|
||||
EOF
|
||||
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
@@ -37,25 +43,32 @@ Add the following options to the kube-apiserver:
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add the following user to the kubeconfig:
|
||||
Run the following command:
|
||||
|
||||
users:
|
||||
- name: google
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
{{- range .Args }}
|
||||
- {{ . }}
|
||||
--exec-arg={{ . }}
|
||||
{{- end }}
|
||||
|
||||
Run kubectl and verify cluster access.
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
kubectl --user=oidc get nodes
|
||||
|
||||
You can switch the default context to oidc.
|
||||
|
||||
kubectl config set-context --current --user=oidc
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
`))
|
||||
|
||||
type stage2Vars struct {
|
||||
Claims map[string]string
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
Args []string
|
||||
@@ -68,18 +81,24 @@ type Stage2Input struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
CACertFilename string // If set, use the CA cert
|
||||
CACertFilename string // optional
|
||||
CACertData string // optional
|
||||
SkipTLSVerify bool
|
||||
ListenAddressArgs []string // non-nil if set by the command arg
|
||||
GrantOptionSet authentication.GrantOptionSet
|
||||
}
|
||||
|
||||
func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
|
||||
u.Logger.Printf(`## 2. Verify authentication`)
|
||||
u.Logger.Printf("authentication in progress...")
|
||||
certPool := u.NewCertPool()
|
||||
if in.CACertFilename != "" {
|
||||
if err := certPool.AddFile(in.CACertFilename); err != nil {
|
||||
return xerrors.Errorf("could not load the certificate: %w", err)
|
||||
return xerrors.Errorf("could not load the certificate file: %w", err)
|
||||
}
|
||||
}
|
||||
if in.CACertData != "" {
|
||||
if err := certPool.AddBase64Encoded(in.CACertData); err != nil {
|
||||
return xerrors.Errorf("could not load the certificate data: %w", err)
|
||||
}
|
||||
}
|
||||
out, err := u.Authentication.Do(ctx, authentication.Input{
|
||||
@@ -94,12 +113,9 @@ func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
u.Logger.Printf("You got the following claims in the token:")
|
||||
for k, v := range out.IDTokenClaims.Pretty {
|
||||
u.Logger.Printf("\t%s=%s", k, v)
|
||||
}
|
||||
|
||||
v := stage2Vars{
|
||||
Claims: out.IDTokenClaims.Pretty,
|
||||
IssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
Args: makeCredentialPluginArgs(in),
|
||||
@@ -126,6 +142,9 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
|
||||
if in.CACertFilename != "" {
|
||||
args = append(args, "--certificate-authority="+in.CACertFilename)
|
||||
}
|
||||
if in.CACertData != "" {
|
||||
args = append(args, "--certificate-authority-data="+in.CACertData)
|
||||
}
|
||||
if in.SkipTLSVerify {
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
}
|
||||
|
||||
@@ -118,29 +118,24 @@ var deprecationTpl = template.Must(template.New("").Parse(
|
||||
The credential plugin mode is available since v1.14.0.
|
||||
Kubectl will automatically run kubelogin and you do not need to run kubelogin explicitly.
|
||||
|
||||
You can switch to the credential plugin mode by setting the following user to
|
||||
{{ .Kubeconfig }}.
|
||||
---
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
You can switch to the credential plugin mode by the following command:
|
||||
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
{{- range .Args }}
|
||||
- {{ . }}
|
||||
--exec-arg={{ . }}
|
||||
{{- end }}
|
||||
---
|
||||
kubectl config set-context --current --user=oidc
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
`))
|
||||
|
||||
type deprecationVars struct {
|
||||
Kubeconfig string
|
||||
Args []string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error {
|
||||
@@ -156,6 +151,9 @@ func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error
|
||||
if p.IDPCertificateAuthority != "" {
|
||||
args = append(args, "--certificate-authority="+p.IDPCertificateAuthority)
|
||||
}
|
||||
if p.IDPCertificateAuthorityData != "" {
|
||||
args = append(args, "--certificate-authority-data="+p.IDPCertificateAuthorityData)
|
||||
}
|
||||
if in.CACertFilename != "" {
|
||||
args = append(args, "--certificate-authority="+in.CACertFilename)
|
||||
}
|
||||
@@ -166,8 +164,7 @@ func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error
|
||||
}
|
||||
|
||||
v := deprecationVars{
|
||||
Kubeconfig: p.LocationOfOrigin,
|
||||
Args: args,
|
||||
Args: args,
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := deprecationTpl.Execute(&b, &v); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user