mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-27 23:43:49 +00:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf73e9a495 | ||
|
|
488e8c62ec | ||
|
|
58d170fa65 | ||
|
|
c488888834 | ||
|
|
2cd741735e | ||
|
|
dbb684f10e | ||
|
|
a0e81e762c | ||
|
|
c4ce1629e2 | ||
|
|
e965114ecb | ||
|
|
804a245fde | ||
|
|
ffaa26cba8 | ||
|
|
923a4251f1 | ||
|
|
98b84d87e0 | ||
|
|
1ae2008e28 | ||
|
|
8197b5b35a | ||
|
|
7196c64bec | ||
|
|
d8419f0dd3 | ||
|
|
5f61d298f5 | ||
|
|
7310b3c271 | ||
|
|
ba4d010f86 | ||
|
|
41d8e74ba9 | ||
|
|
23a1efb4f1 | ||
|
|
3cfeacc861 | ||
|
|
509f29637b | ||
|
|
4f96435e97 | ||
|
|
22005fb715 | ||
|
|
8af36b13e4 | ||
|
|
f0c399b8fc | ||
|
|
17499aac24 | ||
|
|
d81457995d | ||
|
|
2e7b93a31e | ||
|
|
9aeffbc71e | ||
|
|
c2b0c101af | ||
|
|
e8161d5a47 | ||
|
|
a3946c7f5f | ||
|
|
6b880febdb | ||
|
|
a51c15aec2 | ||
|
|
77a6b91be8 | ||
|
|
9e27385c0b | ||
|
|
3c50431a09 | ||
|
|
e41fdf3dcd | ||
|
|
dd93a6537d | ||
|
|
822f6c86de | ||
|
|
dd22ccb9c3 | ||
|
|
f6c4a1257d | ||
|
|
a0c62a9ff1 | ||
|
|
0aa3e43e62 | ||
|
|
9028199abb | ||
|
|
c308ccb511 | ||
|
|
ff1aa97d87 | ||
|
|
ca21c6568b | ||
|
|
117a8d35d4 | ||
|
|
5557290105 | ||
|
|
fe85419312 | ||
|
|
d8d810bc0d | ||
|
|
170eeb4ed5 | ||
|
|
01637fbe12 | ||
|
|
e152e95a9f | ||
|
|
bf8eefd045 | ||
|
|
e88138c640 | ||
|
|
9ad520ba22 | ||
|
|
35e8ecab8d | ||
|
|
c5621239e8 | ||
|
|
582ca48092 | ||
|
|
da32d2184d | ||
|
|
3a9768d6de | ||
|
|
16d6fa2fbb | ||
|
|
175275bf3d | ||
|
|
4ffc914d0e | ||
|
|
fa008bef55 | ||
|
|
50047417ab | ||
|
|
4c0ebb0284 | ||
|
|
c1173accd3 | ||
|
|
d4addcfc6e | ||
|
|
695347e419 | ||
|
|
d5c738697f | ||
|
|
777a60c96c | ||
|
|
6f6e0723f1 | ||
|
|
3fb074a4a8 | ||
|
|
59b5f1bd89 | ||
|
|
70819843f0 | ||
|
|
87c46a24ae | ||
|
|
ac504150b2 | ||
|
|
caeb55f21d | ||
|
|
2ccda6099f | ||
|
|
7f4f72c9e0 |
17
.circleci/Makefile
Normal file
17
.circleci/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
.PHONY: all
|
||||
all:
|
||||
|
||||
.PHONY: install-test-deps
|
||||
install-test-deps:
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(HOME)/go/bin v1.24.0
|
||||
go get -v github.com/int128/goxzst
|
||||
|
||||
.PHONY: install-release-deps
|
||||
install-release-deps: go
|
||||
go get -v github.com/int128/goxzst github.com/int128/ghcp
|
||||
|
||||
go:
|
||||
curl -sSfL -o go.tgz "https://golang.org/dl/go`ruby go_version_from_config.rb < config.yml`.darwin-amd64.tar.gz"
|
||||
tar -xf go.tgz
|
||||
rm go.tgz
|
||||
./go/bin/go version
|
||||
@@ -1,27 +1,60 @@
|
||||
version: 2
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/golang:1.14.1
|
||||
- image: cimg/go:1.14.6
|
||||
steps:
|
||||
- run: mkdir -p ~/bin
|
||||
- run: echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
|
||||
- checkout
|
||||
- run: make ci-setup-linux-amd64
|
||||
- run: make VERSION=$CIRCLE_TAG ci
|
||||
- run: |
|
||||
if [ "$CIRCLE_TAG" ]; then
|
||||
make VERSION=$CIRCLE_TAG GITHUB_USERNAME=$CIRCLE_PROJECT_USERNAME GITHUB_REPONAME=$CIRCLE_PROJECT_REPONAME release
|
||||
fi
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-sum-{{ checksum "go.sum" }}
|
||||
- run: make -C .circleci install-test-deps
|
||||
- run: make check
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: make dist
|
||||
- save_cache:
|
||||
key: go-sum-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- ~/go/pkg
|
||||
- store_artifacts:
|
||||
path: gotest.log
|
||||
|
||||
release:
|
||||
macos:
|
||||
# https://circleci.com/docs/2.0/testing-ios/
|
||||
xcode: 11.5.0
|
||||
steps:
|
||||
- run: echo 'export PATH="$HOME/go/bin:$PWD/.circleci/go/bin:$PATH"' >> $BASH_ENV
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-macos-{{ checksum "go.sum" }}
|
||||
- run: make -C .circleci install-release-deps
|
||||
- run: make dist
|
||||
- run: |
|
||||
if [ "$CIRCLE_TAG" ]; then
|
||||
make release
|
||||
fi
|
||||
- save_cache:
|
||||
key: go-macos-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- ~/go/pkg
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
all:
|
||||
build:
|
||||
jobs:
|
||||
- build:
|
||||
context: open-source
|
||||
- test:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
only: /^v.*/
|
||||
- release:
|
||||
context: open-source
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^release-feature.*/
|
||||
tags:
|
||||
only: /^v.*/
|
||||
|
||||
3
.circleci/go.mod
Normal file
3
.circleci/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/int128/kubelogin/.circleci
|
||||
|
||||
go 1.13
|
||||
11
.circleci/go_version_from_config.rb
Normal file
11
.circleci/go_version_from_config.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
require 'yaml'
|
||||
|
||||
config = YAML.load(STDIN)
|
||||
|
||||
image = config["jobs"]["test"]["docker"][0]["image"]
|
||||
if !image.start_with?("cimg/go:")
|
||||
raise "unknown image #{image} in #{configPath}"
|
||||
end
|
||||
|
||||
goVersion = image.delete_prefix("cimg/go:")
|
||||
print(goVersion)
|
||||
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment**
|
||||
- OS: [e.g. macOS, Linux]
|
||||
- kubelogin version: [e.g. 1.19.3]
|
||||
- kubectl version: [e.g. 1.19]
|
||||
- OpenID Connect provider: [e.g. Google, Okta]
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
8
.github/renovate.json
vendored
Normal file
8
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"postUpdateOptions": [
|
||||
"gomodTidy"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
name: acceptance-test
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
name: test
|
||||
system-test:
|
||||
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
@@ -19,12 +17,13 @@ jobs:
|
||||
go-
|
||||
# https://kind.sigs.k8s.io/docs/user/quick-start/
|
||||
- run: |
|
||||
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-linux-amd64"
|
||||
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.8.1/kind-linux-amd64"
|
||||
chmod +x ./kind
|
||||
sudo mv ./kind /usr/local/bin/kind
|
||||
kind version
|
||||
# https://packages.ubuntu.com/xenial/libnss3-tools
|
||||
- run: sudo apt update
|
||||
- run: sudo apt install -y libnss3-tools
|
||||
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
|
||||
- run: make -C acceptance_test -j3 setup
|
||||
- run: make -C acceptance_test test
|
||||
- run: make -C system_test -j3 setup
|
||||
- run: make -C system_test test
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
/.idea
|
||||
|
||||
/system_test/output/
|
||||
/acceptance_test/output/
|
||||
|
||||
/dist/output
|
||||
@@ -8,3 +9,5 @@
|
||||
|
||||
/kubelogin
|
||||
/kubectl-oidc_login
|
||||
|
||||
/.circleci/go/
|
||||
|
||||
31
Makefile
31
Makefile
@@ -1,10 +1,11 @@
|
||||
# CI must provide the following variables (on tag push)
|
||||
# VERSION
|
||||
# GITHUB_USERNAME
|
||||
# GITHUB_REPONAME
|
||||
# CircleCI specific variables
|
||||
CIRCLE_TAG ?= latest
|
||||
GITHUB_USERNAME := $(CIRCLE_PROJECT_USERNAME)
|
||||
GITHUB_REPONAME := $(CIRCLE_PROJECT_REPONAME)
|
||||
|
||||
TARGET := kubelogin
|
||||
VERSION ?= latest
|
||||
TARGET_OSARCH := linux_amd64 darwin_amd64 windows_amd64 linux_arm linux_arm64
|
||||
VERSION ?= $(CIRCLE_TAG)
|
||||
LDFLAGS := -X main.version=$(VERSION)
|
||||
|
||||
all: $(TARGET)
|
||||
@@ -12,12 +13,6 @@ all: $(TARGET)
|
||||
$(TARGET): $(wildcard **/*.go)
|
||||
go build -o $@ -ldflags "$(LDFLAGS)"
|
||||
|
||||
.PHONY: ci
|
||||
ci:
|
||||
$(MAKE) check
|
||||
bash -c "bash <(curl -s https://codecov.io/bash)"
|
||||
$(MAKE) dist
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
golangci-lint run
|
||||
@@ -27,7 +22,7 @@ check:
|
||||
dist: dist/output
|
||||
dist/output:
|
||||
# make the zip files for GitHub Releases
|
||||
VERSION=$(VERSION) CGO_ENABLED=0 goxzst -d dist/output -i "LICENSE" -o "$(TARGET)" -t "dist/kubelogin.rb dist/oidc-login.yaml dist/Dockerfile" -- -ldflags "$(LDFLAGS)"
|
||||
VERSION=$(VERSION) goxzst -d dist/output -i "LICENSE" -o "$(TARGET)" -osarch "$(TARGET_OSARCH)" -t "dist/kubelogin.rb dist/oidc-login.yaml dist/Dockerfile" -- -ldflags "$(LDFLAGS)"
|
||||
# test the zip file
|
||||
zipinfo dist/output/kubelogin_linux_amd64.zip
|
||||
# make the krew yaml structure
|
||||
@@ -52,15 +47,3 @@ clean:
|
||||
-rm $(TARGET)
|
||||
-rm -r dist/output/
|
||||
-rm coverage.out gotest.log
|
||||
|
||||
.PHONY: ci-setup-linux-amd64
|
||||
ci-setup-linux-amd64:
|
||||
mkdir -p ~/bin
|
||||
# https://github.com/golangci/golangci-lint
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.24.0
|
||||
# https://github.com/int128/goxzst
|
||||
curl -sfL -o /tmp/goxzst.zip https://github.com/int128/goxzst/releases/download/v0.3.0/goxzst_linux_amd64.zip
|
||||
unzip /tmp/goxzst.zip -d ~/bin
|
||||
# https://github.com/int128/ghcp
|
||||
curl -sfL -o /tmp/ghcp.zip https://github.com/int128/ghcp/releases/download/v1.8.0/ghcp_linux_amd64.zip
|
||||
unzip /tmp/ghcp.zip -d ~/bin
|
||||
|
||||
76
README.md
76
README.md
@@ -1,10 +1,10 @@
|
||||
# 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`.
|
||||
|
||||
Here is an example of Kubernetes authentication with the Google Identity Platform:
|
||||
|
||||
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/70971501-7bcebc80-20e4-11ea-8afc-539dcaea0aa8.gif" width="652" height="455">
|
||||
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/85427290-86e43700-b5b6-11ea-9e97-ffefd736c9b7.gif" width="572" height="391">
|
||||
|
||||
Kubelogin is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
When you run kubectl, kubelogin opens the browser and you can log in to the provider.
|
||||
@@ -18,19 +18,14 @@ Take a look at the diagram:
|
||||
|
||||
### Setup
|
||||
|
||||
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
|
||||
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases).
|
||||
|
||||
```sh
|
||||
# Homebrew
|
||||
# Homebrew (macOS and Linux)
|
||||
brew install int128/kubelogin/kubelogin
|
||||
|
||||
# Krew
|
||||
# Krew (macOS, Linux, Windows and ARM)
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.18.0/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
|
||||
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
|
||||
@@ -51,7 +46,7 @@ users:
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
See [the setup guide](docs/setup.md) for more.
|
||||
See [setup guide](docs/setup.md) for more.
|
||||
|
||||
|
||||
### Run
|
||||
@@ -88,21 +83,26 @@ If the refresh token has expired, kubelogin will perform reauthentication.
|
||||
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
|
||||
Kubelogin will perform authentication if the token cache file does not exist.
|
||||
|
||||
You can dump the claims of token by passing `-v1` option.
|
||||
You can dump claims of an ID token by `setup` command.
|
||||
|
||||
```
|
||||
I0221 21:54:08.151850 28231 get_token.go:104] you got a token: {
|
||||
```console
|
||||
% kubectl oidc-login setup --oidc-issuer-url https://accounts.google.com --oidc-client-id REDACTED --oidc-client-secret REDACTED
|
||||
authentication in progress...
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
You got a token with the following claims:
|
||||
|
||||
{
|
||||
"sub": "********",
|
||||
"iss": "https://accounts.google.com",
|
||||
"aud": "********",
|
||||
"iat": 1582289639,
|
||||
"exp": 1582293239,
|
||||
"jti": "********",
|
||||
"nonce": "********",
|
||||
"at_hash": "********"
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -120,17 +120,18 @@ Flags:
|
||||
--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
|
||||
--token-cache-dir string Path to a directory for token cache (default "~/.kube/cache/oidc-login")
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--certificate-authority-data string Base64 encoded data for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--listen-port ints (Deprecated: use --listen-address)
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
--certificate-authority-data string Base64 encoded cert for the certificate authority
|
||||
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--grant-type string Authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings [authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--skip-open-browser [authcode] Do not open the browser automatically
|
||||
--open-url-after-authentication string [authcode] If set, open the URL in the browser after authentication
|
||||
--oidc-redirect-url-hostname string [authcode] Hostname of the redirect URL (default "localhost")
|
||||
--oidc-auth-request-extra-params stringToString [authcode, authcode-keyboard] Extra query parameters to send with an authentication request (default [])
|
||||
--username string [password] Username for resource owner password credentials grant
|
||||
--password string [password] Password for resource owner password credentials grant
|
||||
-h, --help help for get-token
|
||||
|
||||
Global Flags:
|
||||
@@ -192,12 +193,25 @@ You can change the listening address.
|
||||
- --listen-address=127.0.0.1:23456
|
||||
```
|
||||
|
||||
You can change the hostname of redirect URI from the default value `localhost`.
|
||||
|
||||
```yaml
|
||||
- --oidc-redirect-url-hostname=127.0.0.1
|
||||
```
|
||||
|
||||
You can add extra parameters to the authentication request.
|
||||
|
||||
```yaml
|
||||
- --oidc-auth-request-extra-params=ttl=86400
|
||||
```
|
||||
|
||||
When authentication completed, kubelogin shows a message to close the browser.
|
||||
You can change the URL to show after authentication.
|
||||
|
||||
```yaml
|
||||
- --open-url-after-authentication=https://example.com/success.html
|
||||
```
|
||||
|
||||
#### Authorization code flow with keyboard interactive
|
||||
|
||||
If you cannot access the browser, instead use the authorization code flow with keyboard interactive.
|
||||
@@ -281,7 +295,7 @@ users:
|
||||
- /tmp/.token-cache:/.token-cache
|
||||
- -p
|
||||
- 8000:8000
|
||||
- quay.io/int128/kubelogin:v1.18.0
|
||||
- quay.io/int128/kubelogin
|
||||
- get-token
|
||||
- --token-cache-dir=/.token-cache
|
||||
- --listen-address=0.0.0.0:8000
|
||||
@@ -321,4 +335,4 @@ make
|
||||
./kubelogin
|
||||
```
|
||||
|
||||
See also [the acceptance test](acceptance_test).
|
||||
See also [the system test](system_test).
|
||||
|
||||
@@ -1,99 +1,31 @@
|
||||
CLUSTER_NAME := kubelogin-acceptance-test
|
||||
OUTPUT_DIR := $(CURDIR)/output
|
||||
|
||||
PATH := $(PATH):$(OUTPUT_DIR)/bin
|
||||
export PATH
|
||||
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
|
||||
export KUBECONFIG
|
||||
|
||||
# run the login script instead of opening chrome
|
||||
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
|
||||
export BROWSER
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
# see the setup instruction
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--oidc-extra-scope=email \
|
||||
--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# set up the kubeconfig
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--exec-arg=--oidc-extra-scope=email \
|
||||
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# make sure we can access the cluster
|
||||
kubectl --user=oidc cluster-info
|
||||
# switch the current context
|
||||
kubectl config set-context --current --user=oidc
|
||||
# make sure we can access the cluster
|
||||
kubectl cluster-info
|
||||
|
||||
.PHONY: setup
|
||||
setup: build dex cluster setup-chrome
|
||||
|
||||
.PHONY: setup-chrome
|
||||
setup-chrome: $(OUTPUT_DIR)/ca.crt
|
||||
# add the dex server certificate to the trust store
|
||||
mkdir -p ~/.pki/nssdb
|
||||
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
|
||||
|
||||
# build binaries
|
||||
.PHONY: build
|
||||
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
|
||||
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
|
||||
go build -o $@ ..
|
||||
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
|
||||
go build -o $@ ./chromelogin
|
||||
|
||||
# create a Dex server
|
||||
.PHONY: dex
|
||||
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
|
||||
docker create --name dex-server -p 10443:10443 quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
|
||||
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
|
||||
docker cp $(OUTPUT_DIR)/server.key dex-server:/
|
||||
docker cp dex.yaml dex-server:/
|
||||
docker start dex-server
|
||||
docker logs dex-server
|
||||
|
||||
$(OUTPUT_DIR)/ca.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
|
||||
$(OUTPUT_DIR)/server.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
|
||||
|
||||
# create a Kubernetes cluster
|
||||
.PHONY: cluster
|
||||
cluster: dex create-cluster
|
||||
# add the Dex container IP to /etc/hosts of kube-apiserver
|
||||
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
|
||||
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
|
||||
# wait for kube-apiserver oidc initialization
|
||||
# (oidc authenticator will retry oidc discovery every 10s)
|
||||
sleep 10
|
||||
|
||||
.PHONY: create-cluster
|
||||
create-cluster: $(OUTPUT_DIR)/ca.crt
|
||||
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-acceptance-test-dex-ca.crt
|
||||
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
|
||||
kubectl apply -f role.yaml
|
||||
cluster:
|
||||
# create a cluster
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
sed -e "s|OIDC_ISSUER_URL|$(OIDC_ISSUER_URL)|" -e "s|OIDC_CLIENT_ID|$(OIDC_CLIENT_ID)|" cluster.yaml > $(OUTPUT_DIR)/cluster.yaml
|
||||
kind create cluster --name $(CLUSTER_NAME) --config $(OUTPUT_DIR)/cluster.yaml
|
||||
# set up access control
|
||||
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
|
||||
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=$(YOUR_EMAIL)
|
||||
# set up kubectl
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=$(CURDIR)/../kubelogin \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--token-cache-dir=$(OUTPUT_DIR)/token-cache \
|
||||
--exec-arg=--oidc-issuer-url=$(OIDC_ISSUER_URL) \
|
||||
--exec-arg=--oidc-client-id=$(OIDC_CLIENT_ID) \
|
||||
--exec-arg=--oidc-client-secret=$(OIDC_CLIENT_SECRET) \
|
||||
--exec-arg=--oidc-extra-scope=email
|
||||
# switch the default user
|
||||
kubectl config set-context --current --user=oidc
|
||||
|
||||
# clean up the resources
|
||||
.PHONY: clean
|
||||
@@ -102,7 +34,9 @@ clean:
|
||||
.PHONY: delete-cluster
|
||||
delete-cluster:
|
||||
kind delete cluster --name $(CLUSTER_NAME)
|
||||
.PHONY: delete-dex
|
||||
delete-dex:
|
||||
docker stop dex-server
|
||||
docker rm dex-server
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
docker version
|
||||
kind version
|
||||
kubectl version --client
|
||||
|
||||
@@ -1,109 +1,75 @@
|
||||
# kubelogin/acceptance_test
|
||||
|
||||
This is an acceptance test for walkthrough of the OIDC initial setup and plugin behavior using a real Kubernetes cluster and OpenID Connect provider, running on [GitHub Actions](https://github.com/int128/kubelogin/actions?query=workflow%3Aacceptance-test).
|
||||
|
||||
It is intended to verify the following points:
|
||||
|
||||
- User can set up Kubernetes OIDC authentication and this plugin.
|
||||
- User can access a cluster after login.
|
||||
|
||||
It performs the test using the following components:
|
||||
|
||||
- Kubernetes cluster (Kind)
|
||||
- OIDC provider (Dex)
|
||||
- Browser (Chrome)
|
||||
- kubectl command
|
||||
This is a manual test for verifying Kubernetes OIDC authentication with your OIDC provider.
|
||||
|
||||
|
||||
## How it works
|
||||
## Purpose
|
||||
|
||||
Let's take a look at the diagram.
|
||||
This test checks the following points:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
1. You can set up your OIDC provider using [setup guide](../docs/setup.md).
|
||||
1. The plugin works with your OIDC provider.
|
||||
|
||||
|
||||
## Run locally
|
||||
## Getting Started
|
||||
|
||||
You need to set up the following components:
|
||||
### Prerequisite
|
||||
|
||||
You need to build the plugin into the parent directory.
|
||||
|
||||
```sh
|
||||
make -C ..
|
||||
```
|
||||
|
||||
You need to set up your provider.
|
||||
See [setup guide](../docs/setup.md) for more.
|
||||
|
||||
You need to install the following tools:
|
||||
|
||||
- Docker
|
||||
- Kind
|
||||
- Chrome or Chromium
|
||||
- kubectl
|
||||
|
||||
You need to add the following line to `/etc/hosts` so that the browser can access the Dex.
|
||||
You can check if the tools are available.
|
||||
|
||||
```
|
||||
127.0.0.1 dex-server
|
||||
```sh
|
||||
make check
|
||||
```
|
||||
|
||||
Run the test.
|
||||
### 1. Create a cluster
|
||||
|
||||
```shell script
|
||||
# run the test
|
||||
make
|
||||
Create a cluster.
|
||||
For example, you can create a cluster with Google account authentication.
|
||||
|
||||
# clean up
|
||||
```sh
|
||||
make OIDC_ISSUER_URL=https://accounts.google.com \
|
||||
OIDC_CLIENT_ID=REDACTED.apps.googleusercontent.com \
|
||||
OIDC_CLIENT_SECRET=REDACTED \
|
||||
YOUR_EMAIL=REDACTED@gmail.com
|
||||
```
|
||||
|
||||
It will do the following steps:
|
||||
|
||||
1. Create a cluster.
|
||||
1. Set up access control. It allows read-only access from your email address.
|
||||
1. Set up kubectl to enable the plugin.
|
||||
|
||||
You can change kubectl configuration in generated `output/kubeconfig.yaml`.
|
||||
|
||||
### 2. Run kubectl
|
||||
|
||||
Make sure you can log in to the provider and access the cluster.
|
||||
|
||||
```console
|
||||
% export KUBECONFIG=$PWD/output/kubeconfig.yaml
|
||||
% kubectl get pods -A
|
||||
```
|
||||
|
||||
### Clean up
|
||||
|
||||
To delete the cluster and generated files:
|
||||
|
||||
```sh
|
||||
make delete-cluster
|
||||
make delete-dex
|
||||
make clean
|
||||
```
|
||||
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
# https://github.com/dexidp/dex/blob/master/Documentation/kubernetes.md
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
apiVersion: kubeadm.k8s.io/v1beta2
|
||||
@@ -9,12 +8,6 @@ kubeadmConfigPatches:
|
||||
name: config
|
||||
apiServer:
|
||||
extraArgs:
|
||||
oidc-issuer-url: https://dex-server:10443/dex
|
||||
oidc-client-id: YOUR_CLIENT_ID
|
||||
oidc-issuer-url: OIDC_ISSUER_URL
|
||||
oidc-client-id: OIDC_CLIENT_ID
|
||||
oidc-username-claim: email
|
||||
oidc-ca-file: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
nodes:
|
||||
- role: control-plane
|
||||
extraMounts:
|
||||
- hostPath: /tmp/kubelogin-acceptance-test-dex-ca.crt
|
||||
containerPath: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: readonly-all-resources
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: readonly-all-resources
|
||||
subjects:
|
||||
- kind: User
|
||||
name: admin@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: readonly-all-resources
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
2
dist/Dockerfile
vendored
2
dist/Dockerfile
vendored
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.12
|
||||
|
||||
ARG KUBELOGIN_VERSION="{{ env "VERSION" }}"
|
||||
ARG KUBELOGIN_SHA256="{{ sha256 .linux_amd64_archive }}"
|
||||
|
||||
16
dist/kubelogin.rb
vendored
16
dist/kubelogin.rb
vendored
@@ -1,15 +1,27 @@
|
||||
class Kubelogin < Formula
|
||||
desc "A kubectl plugin for Kubernetes OpenID Connect authentication"
|
||||
homepage "https://github.com/int128/kubelogin"
|
||||
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
|
||||
baseurl = "https://github.com/int128/kubelogin/releases/download"
|
||||
version "{{ env "VERSION" }}"
|
||||
sha256 "{{ sha256 .darwin_amd64_archive }}"
|
||||
|
||||
if OS.mac?
|
||||
kernel = "darwin"
|
||||
sha256 "{{ sha256 .darwin_amd64_archive }}"
|
||||
elsif OS.linux?
|
||||
kernel = "linux"
|
||||
sha256 "{{ sha256 .linux_amd64_archive }}"
|
||||
end
|
||||
|
||||
url baseurl + "/#{version}/kubelogin_#{kernel}_amd64.zip"
|
||||
|
||||
def install
|
||||
bin.install "kubelogin" => "kubelogin"
|
||||
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/kubelogin -h"
|
||||
system "#{bin}/kubectl-oidc_login -h"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
26
dist/oidc-login.yaml
vendored
26
dist/oidc-login.yaml
vendored
@@ -22,8 +22,6 @@ spec:
|
||||
|
||||
caveats: |
|
||||
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
version: {{ env "VERSION" }}
|
||||
platforms:
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
|
||||
@@ -62,3 +60,27 @@ spec:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm.zip
|
||||
sha256: "{{ sha256 .linux_arm_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: kubelogin
|
||||
to: .
|
||||
- from: LICENSE
|
||||
to: .
|
||||
selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm64.zip
|
||||
sha256: "{{ sha256 .linux_arm64_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: kubelogin
|
||||
to: .
|
||||
- from: LICENSE
|
||||
to: .
|
||||
selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
@@ -126,6 +126,9 @@ Variable | Value
|
||||
|
||||
You do not need to set `YOUR_CLIENT_SECRET`.
|
||||
|
||||
If you need `groups` claim for access control,
|
||||
see [jetstack/okta-kubectl-auth](https://github.com/jetstack/okta-kubectl-auth/blob/master/docs/okta-setup.md) and [#250](https://github.com/int128/kubelogin/issues/250).
|
||||
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
@@ -152,24 +155,9 @@ kubectl oidc-login setup --help
|
||||
## 3. Bind a cluster role
|
||||
|
||||
Here bind `cluster-admin` role to you.
|
||||
Apply the following manifest:
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: oidc-cluster-admin
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: ISSUER_URL#YOUR_SUBJECT
|
||||
```
|
||||
|
||||
```sh
|
||||
kubectl apply -f oidc-cluster-admin.yaml
|
||||
kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='ISSUER_URL#YOUR_SUBJECT'
|
||||
```
|
||||
|
||||
As well as you can create a custom cluster role and bind it.
|
||||
|
||||
@@ -75,57 +75,6 @@ If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
## Usage
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubectl oidc-login -h
|
||||
Login to the OpenID Connect provider.
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
Run the following command to show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
Usage:
|
||||
main [flags]
|
||||
main [command]
|
||||
|
||||
Available Commands:
|
||||
get-token Run as a kubectl credential plugin
|
||||
help Help about any command
|
||||
setup Show the setup instruction
|
||||
version Print the version information
|
||||
|
||||
Flags:
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--listen-port ints (Deprecated: use --listen-address)
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
--add_dir_header If true, adds the file directory to the header
|
||||
--alsologtostderr log to standard error as well as files
|
||||
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
|
||||
--log_dir string If non-empty, write log files in this directory
|
||||
--log_file string If non-empty, use this log file
|
||||
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
|
||||
--logtostderr log to standard error instead of files (default true)
|
||||
--skip_headers If true, avoid header prefixes in the log messages
|
||||
--skip_log_headers If true, avoid headers when opening log files
|
||||
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
|
||||
-v, --v Level number for the log level verbosity
|
||||
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
|
||||
-h, --help help for main
|
||||
--version version for main
|
||||
```
|
||||
|
||||
### Kubeconfig
|
||||
|
||||
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
16
go.mod
16
go.mod
@@ -6,21 +6,21 @@ require (
|
||||
github.com/chromedp/chromedp v0.5.3
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/go-cmp v0.5.1
|
||||
github.com/google/wire v0.4.0
|
||||
github.com/int128/oauth2cli v1.10.0
|
||||
github.com/int128/oauth2cli v1.12.1
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/spf13/cobra v0.0.6
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
k8s.io/apimachinery v0.17.4
|
||||
k8s.io/client-go v0.17.4
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
k8s.io/apimachinery v0.18.6
|
||||
k8s.io/client-go v0.18.6
|
||||
k8s.io/klog v1.0.0
|
||||
)
|
||||
|
||||
82
go.sum
82
go.sum
@@ -32,7 +32,6 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -40,7 +39,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -63,15 +62,15 @@ 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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -83,11 +82,14 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
@@ -96,6 +98,7 @@ github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
|
||||
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
@@ -110,12 +113,12 @@ github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/int128/listener v1.0.0 h1:a9H3m4jbXgXpxJUK3fxWrh37Iic/UU/kYOGE0WtjbbI=
|
||||
github.com/int128/listener v1.0.0/go.mod h1:sho0rrH7mNRRZH4hYOYx+xwRDGmtRndaUiu2z9iumes=
|
||||
github.com/int128/oauth2cli v1.10.0 h1:ypYxwjuBblyTRTdZTFQLgA08gYhVwsdlBEvuoNs6Xsw=
|
||||
github.com/int128/oauth2cli v1.10.0/go.mod h1:m5tJro14TyPDlIg+RIlGVnavkm1kTooROlcFlnhteVo=
|
||||
github.com/int128/listener v1.1.0 h1:2Jb41DWLpkQ3I9bIdBzO8H/tNwMvyl/OBZWtCV5Pjuw=
|
||||
github.com/int128/listener v1.1.0/go.mod h1:68WkmTN8PQtLzc9DucIaagAKeGVyMnyyKIkW4Xn47UA=
|
||||
github.com/int128/oauth2cli v1.12.1 h1:F+6sykVdM+0rede+jAJ2RICP3GAsLLGvPjSFLlI0U9Q=
|
||||
github.com/int128/oauth2cli v1.12.1/go.mod h1:0Wf2wAxKJNzbkPkUIYNhTjeLn/pqIBDOBAGfwrxGYQw=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
@@ -142,7 +145,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
@@ -152,7 +154,7 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
@@ -160,7 +162,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
@@ -183,8 +184,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
|
||||
github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
@@ -194,7 +195,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -247,8 +247,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -258,12 +260,10 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
@@ -281,8 +281,6 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -314,26 +312,28 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.17.4 h1:HbwOhDapkguO8lTAE8OX3hdF2qp8GtpC9CW/MQATXXo=
|
||||
k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA=
|
||||
k8s.io/apimachinery v0.17.4 h1:UzM+38cPUJnzqSQ+E1PY4YxMHIzQyCg29LOoGfo79Zw=
|
||||
k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
|
||||
k8s.io/client-go v0.17.4 h1:VVdVbpTY70jiNHS1eiFkUt7ZIJX3txd29nDxxXH4en8=
|
||||
k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc=
|
||||
k8s.io/api v0.18.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE=
|
||||
k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI=
|
||||
k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag=
|
||||
k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
|
||||
k8s.io/client-go v0.18.6 h1:I+oWqJbibLSGsZj8Xs8F0aWVXJVIoUHWaaJV3kUN/Zw=
|
||||
k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
|
||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
|
||||
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU=
|
||||
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/integration_test/localserver"
|
||||
"github.com/int128/kubelogin/integration_test/httpdriver"
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
"github.com/int128/kubelogin/integration_test/oidcserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
"github.com/int128/kubelogin/pkg/testing/clock"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
@@ -31,6 +30,8 @@ import (
|
||||
// 4. Verify the output.
|
||||
//
|
||||
func TestCredentialPlugin(t *testing.T) {
|
||||
timeout := 3 * time.Second
|
||||
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
tokenCacheDir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a cache dir: %s", err)
|
||||
@@ -41,307 +42,379 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
TokenCacheDir: tokenCacheDir,
|
||||
Keys: keys.None,
|
||||
ExtraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
for name, tc := range map[string]struct {
|
||||
keyPair keypair.KeyPair
|
||||
args []string
|
||||
}{
|
||||
"NoTLS": {},
|
||||
"TLS": {
|
||||
keyPair: keypair.Server,
|
||||
args: []string{"--certificate-authority", keypair.Server.CACertPath},
|
||||
},
|
||||
} {
|
||||
httpDriverOption := httpdriver.Option{
|
||||
TLSConfig: tc.keyPair.TLSConfig,
|
||||
BodyContains: "Authenticated",
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("AuthCode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("ROPC", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Username: "USER1",
|
||||
Password: "PASS1",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.Zero(t),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: append([]string{
|
||||
"--username", "USER1",
|
||||
"--password", "PASS1",
|
||||
}, tc.args...),
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("TokenCacheLifecycle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
|
||||
t.Run("NoCache", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.Zero(t),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
t.Run("Refresh", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(3 * time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now.Add(2 * time.Hour),
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(3*time.Hour))
|
||||
})
|
||||
t.Run("RefreshAgain", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(5 * time.Hour),
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now.Add(4 * time.Hour),
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(5*time.Hour))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("PKCE", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
t.Run("CertFile", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
TokenCacheDir: tokenCacheDir,
|
||||
TokenCacheKey: tokencache.Key{CACertFilename: keys.Server.CACertPath},
|
||||
Keys: keys.Server,
|
||||
ExtraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--certificate-authority", keys.Server.CACertPath,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("CertData", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
TokenCacheDir: tokenCacheDir,
|
||||
TokenCacheKey: tokencache.Key{CACertData: keys.Server.CACertBase64},
|
||||
Keys: keys.Server,
|
||||
ExtraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--certificate-authority-data", keys.Server.CACertBase64,
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
}
|
||||
|
||||
type credentialPluginTestCase struct {
|
||||
TokenCacheDir string
|
||||
TokenCacheKey tokencache.Key
|
||||
Keys keys.Keys
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
func testCredentialPlugin(t *testing.T, tc credentialPluginTestCase) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Run("TLSData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", nil, &idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
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", nil, &validIDToken)
|
||||
provider.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &validIDToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: validIDToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig, BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{"--certificate-authority-data", keypair.Server.CACertBase64},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "email profile openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.Keys)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "email profile openid", nil, &idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
t.Run("OpenURLAfterAuthentication", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "URL=https://example.com/success"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{"--open-url-after-authentication", "https://example.com/success"},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
t.Run("RedirectURLHostname", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://127.0.0.1:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{"--oidc-redirect-url-hostname", "127.0.0.1"},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("ExtraParams", 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", map[string]string{
|
||||
"ttl": "86400",
|
||||
"reauth": "false",
|
||||
}, &idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.Keys)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-auth-request-extra-params", "ttl=86400",
|
||||
"--oidc-auth-request-extra-params", "reauth=false",
|
||||
}
|
||||
args = append(args, tc.ExtraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
ExtraParams: map[string]string{
|
||||
"ttl": "86400",
|
||||
"reauth": "false",
|
||||
},
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{
|
||||
"--oidc-auth-request-extra-params", "ttl=86400",
|
||||
"--oidc-auth-request-extra-params", "reauth=false",
|
||||
},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
}
|
||||
|
||||
func newCredentialPluginWriterMock(t *testing.T, ctrl *gomock.Controller, idToken *string) *mock_credentialpluginwriter.MockInterface {
|
||||
writer := mock_credentialpluginwriter.NewMockInterface(ctrl)
|
||||
writer.EXPECT().
|
||||
Write(gomock.Any()).
|
||||
Do(func(got credentialpluginwriter.Output) {
|
||||
want := credentialpluginwriter.Output{
|
||||
Token: *idToken,
|
||||
Expiry: tokenExpiryFuture,
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
return writer
|
||||
type getTokenConfig struct {
|
||||
tokenCacheDir string
|
||||
issuerURL string
|
||||
httpDriver browser.Interface
|
||||
stdout io.Writer
|
||||
now time.Time
|
||||
args []string
|
||||
}
|
||||
|
||||
func runGetTokenCmd(t *testing.T, ctx context.Context, b browser.Interface, w credentialpluginwriter.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(logger.New(t), b, w)
|
||||
func runGetToken(t *testing.T, ctx context.Context, cfg getTokenConfig) {
|
||||
cmd := di.NewCmdForHeadless(clock.Fake(cfg.now), os.Stdin, cfg.stdout, logger.New(t), cfg.httpDriver)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin", "get-token",
|
||||
"--v=1",
|
||||
"kubelogin",
|
||||
"get-token",
|
||||
"--token-cache-dir", cfg.tokenCacheDir,
|
||||
"--oidc-issuer-url", cfg.issuerURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
}, args...), "HEAD")
|
||||
}, cfg.args...), "latest")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, v tokencache.Value) {
|
||||
k := tc.TokenCacheKey
|
||||
k.IssuerURL = serverURL
|
||||
k.ClientID = "kubernetes"
|
||||
var r tokencache.Repository
|
||||
err := r.Save(tc.TokenCacheDir, k, v)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, want tokencache.Value) {
|
||||
k := tc.TokenCacheKey
|
||||
k.IssuerURL = serverURL
|
||||
k.ClientID = "kubernetes"
|
||||
var r tokencache.Repository
|
||||
got, err := r.FindByKey(tc.TokenCacheDir, k)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(&want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
func assertCredentialPluginStdout(t *testing.T, stdout io.Reader, token string, expiry time.Time) {
|
||||
var got clientauthenticationv1beta1.ExecCredential
|
||||
if err := json.NewDecoder(stdout).Decode(&got); err != nil {
|
||||
t.Errorf("could not decode json of the credential plugin: %s", err)
|
||||
return
|
||||
}
|
||||
want := clientauthenticationv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Kind: "ExecCredential",
|
||||
},
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
Token: token,
|
||||
ExpirationTimestamp: &metav1.Time{Time: expiry},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
)
|
||||
|
||||
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()
|
||||
return jwt.EncodeF(t, func(claims *jwt.Claims) {
|
||||
claims.Issuer = issuer
|
||||
claims.Subject = "SUBJECT"
|
||||
claims.IssuedAt = time.Now().Unix()
|
||||
claims.ExpiresAt = expiry.Unix()
|
||||
claims.Audience = []string{"kubernetes", "system"}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
})
|
||||
}
|
||||
|
||||
func setupAuthCodeFlow(t *testing.T, provider *mock_idp.MockProvider, serverURL, scope string, extraParams map[string]string, idToken *string) {
|
||||
var nonce string
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
provider.EXPECT().AuthenticateCode(gomock.Any()).
|
||||
DoAndReturn(func(req idp.AuthenticationRequest) (string, error) {
|
||||
if req.Scope != scope {
|
||||
t.Errorf("scope wants `%s` but was `%s`", scope, req.Scope)
|
||||
}
|
||||
for k, v := range extraParams {
|
||||
got := req.RawQuery.Get(k)
|
||||
if got != v {
|
||||
t.Errorf("parameter %s wants `%s` but was `%s`", k, v, got)
|
||||
}
|
||||
}
|
||||
nonce = req.Nonce
|
||||
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(jwt.PrivateKey))
|
||||
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
|
||||
}
|
||||
70
integration_test/httpdriver/http_driver.go
Normal file
70
integration_test/httpdriver/http_driver.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Package httpdriver provides a test double of the browser.
|
||||
package httpdriver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
TLSConfig *tls.Config
|
||||
BodyContains string
|
||||
}
|
||||
|
||||
// New returns a client to simulate browser access.
|
||||
func New(ctx context.Context, t *testing.T, o Option) *client {
|
||||
return &client{ctx, t, o}
|
||||
}
|
||||
|
||||
// Zero returns a client which call is not expected.
|
||||
func Zero(t *testing.T) *zeroClient {
|
||||
return &zeroClient{t}
|
||||
}
|
||||
|
||||
type client struct {
|
||||
ctx context.Context
|
||||
t *testing.T
|
||||
o Option
|
||||
}
|
||||
|
||||
func (c *client) Open(url string) error {
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.o.TLSConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
c.t.Errorf("could not create a request: %s", err)
|
||||
return nil
|
||||
}
|
||||
req = req.WithContext(c.ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.t.Errorf("could not send a request: %s", err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
c.t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
c.t.Errorf("could not read body: %s", err)
|
||||
return nil
|
||||
}
|
||||
body := string(b)
|
||||
if !strings.Contains(body, c.o.BodyContains) {
|
||||
c.t.Errorf("body should contain %s but was %s", c.o.BodyContains, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type zeroClient struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (c *zeroClient) Open(url string) error {
|
||||
c.t.Errorf("unexpected function call Open(%s)", url)
|
||||
return nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/integration_test/idp (interfaces: Provider)
|
||||
|
||||
// Package mock_idp is a generated GoMock package.
|
||||
package mock_idp
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
idp "github.com/int128/kubelogin/integration_test/idp"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockProvider is a mock of Provider interface
|
||||
type MockProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockProviderMockRecorder is the mock recorder for MockProvider
|
||||
type MockProviderMockRecorder struct {
|
||||
mock *MockProvider
|
||||
}
|
||||
|
||||
// NewMockProvider creates a new mock instance
|
||||
func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
|
||||
mock := &MockProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockProvider) EXPECT() *MockProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AuthenticateCode mocks base method
|
||||
func (m *MockProvider) AuthenticateCode(arg0 idp.AuthenticationRequest) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateCode", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticateCode indicates an expected call of AuthenticateCode
|
||||
func (mr *MockProviderMockRecorder) AuthenticateCode(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockProvider)(nil).AuthenticateCode), arg0)
|
||||
}
|
||||
|
||||
// AuthenticatePassword mocks base method
|
||||
func (m *MockProvider) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticatePassword indicates an expected call of AuthenticatePassword
|
||||
func (mr *MockProviderMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockProvider)(nil).AuthenticatePassword), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Discovery mocks base method
|
||||
func (m *MockProvider) Discovery() *idp.DiscoveryResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Discovery")
|
||||
ret0, _ := ret[0].(*idp.DiscoveryResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Discovery indicates an expected call of Discovery
|
||||
func (mr *MockProviderMockRecorder) Discovery() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockProvider)(nil).Discovery))
|
||||
}
|
||||
|
||||
// Exchange mocks base method
|
||||
func (m *MockProvider) Exchange(arg0 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Exchange", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Exchange indicates an expected call of Exchange
|
||||
func (mr *MockProviderMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockProvider)(nil).Exchange), arg0)
|
||||
}
|
||||
|
||||
// GetCertificates mocks base method
|
||||
func (m *MockProvider) GetCertificates() *idp.CertificatesResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCertificates")
|
||||
ret0, _ := ret[0].(*idp.CertificatesResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCertificates indicates an expected call of GetCertificates
|
||||
func (mr *MockProviderMockRecorder) GetCertificates() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockProvider)(nil).GetCertificates))
|
||||
}
|
||||
|
||||
// Refresh mocks base method
|
||||
func (m *MockProvider) Refresh(arg0 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Refresh", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Refresh indicates an expected call of Refresh
|
||||
func (mr *MockProviderMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockProvider)(nil).Refresh), arg0)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package keys
|
||||
package keypair
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Keys represents a pair of certificate and key.
|
||||
type Keys struct {
|
||||
// KeyPair represents a pair of certificate and key.
|
||||
type KeyPair struct {
|
||||
CertPath string
|
||||
KeyPath string
|
||||
CACertPath string
|
||||
@@ -20,16 +20,16 @@ type Keys struct {
|
||||
}
|
||||
|
||||
// None represents non-TLS.
|
||||
var None Keys
|
||||
var None KeyPair
|
||||
|
||||
// Server is a Keys for TLS server.
|
||||
// Server is a KeyPair for TLS server.
|
||||
// These files should be generated by Makefile before test.
|
||||
var Server = Keys{
|
||||
CertPath: "keys/testdata/server.crt",
|
||||
KeyPath: "keys/testdata/server.key",
|
||||
CACertPath: "keys/testdata/ca.crt",
|
||||
CACertBase64: readAsBase64("keys/testdata/ca.crt"),
|
||||
TLSConfig: newTLSConfig("keys/testdata/ca.crt"),
|
||||
var Server = KeyPair{
|
||||
CertPath: "keypair/testdata/server.crt",
|
||||
KeyPath: "keypair/testdata/server.key",
|
||||
CACertPath: "keypair/testdata/ca.crt",
|
||||
CACertBase64: readAsBase64("keypair/testdata/ca.crt"),
|
||||
TLSConfig: newTLSConfig("keypair/testdata/ca.crt"),
|
||||
}
|
||||
|
||||
func readAsBase64(name string) string {
|
||||
@@ -1,4 +1,5 @@
|
||||
package idp
|
||||
// Package handler provides a HTTP handler for the OpenID Connect Provider.
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func NewHandler(t *testing.T, provider Provider) *Handler {
|
||||
func New(t *testing.T, provider Provider) *Handler {
|
||||
return &Handler{t, provider}
|
||||
}
|
||||
|
||||
@@ -71,16 +72,16 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
return xerrors.Errorf("could not render json: %w", err)
|
||||
}
|
||||
case m == "GET" && p == "/auth":
|
||||
// 3.1.2.1. Authentication Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
q := r.URL.Query()
|
||||
redirectURI, state := q.Get("redirect_uri"), q.Get("state")
|
||||
code, err := h.provider.AuthenticateCode(AuthenticationRequest{
|
||||
RedirectURI: redirectURI,
|
||||
State: state,
|
||||
Scope: q.Get("scope"),
|
||||
Nonce: q.Get("nonce"),
|
||||
RawQuery: q,
|
||||
RedirectURI: redirectURI,
|
||||
State: state,
|
||||
Scope: q.Get("scope"),
|
||||
Nonce: q.Get("nonce"),
|
||||
CodeChallenge: q.Get("code_challenge"),
|
||||
CodeChallengeMethod: q.Get("code_challenge_method"),
|
||||
RawQuery: q,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authentication error: %w", err)
|
||||
@@ -94,10 +95,10 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
grantType := r.Form.Get("grant_type")
|
||||
switch grantType {
|
||||
case "authorization_code":
|
||||
// 3.1.3.1. Token Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
||||
code := r.Form.Get("code")
|
||||
tokenResponse, err := h.provider.Exchange(code)
|
||||
tokenResponse, err := h.provider.Exchange(TokenRequest{
|
||||
Code: r.Form.Get("code"),
|
||||
CodeVerifier: r.Form.Get("code_verifier"),
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("token request error: %w", err)
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
// Package idp provides a test double of an OpenID Connect Provider.
|
||||
package idp
|
||||
|
||||
//go:generate mockgen -destination mock_idp/mock_idp.go github.com/int128/kubelogin/integration_test/idp Provider
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
@@ -19,7 +13,7 @@ type Provider interface {
|
||||
Discovery() *DiscoveryResponse
|
||||
GetCertificates() *CertificatesResponse
|
||||
AuthenticateCode(req AuthenticationRequest) (code string, err error)
|
||||
Exchange(code string) (*TokenResponse, error)
|
||||
Exchange(req TokenRequest) (*TokenResponse, error)
|
||||
AuthenticatePassword(username, password, scope string) (*TokenResponse, error)
|
||||
Refresh(refreshToken string) (*TokenResponse, error)
|
||||
}
|
||||
@@ -40,26 +34,6 @@ type DiscoveryResponse struct {
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||
}
|
||||
|
||||
// NewDiscoveryResponse returns a DiscoveryResponse for the local server.
|
||||
// This is based on https://accounts.google.com/.well-known/openid-configuration.
|
||||
func NewDiscoveryResponse(issuer string) *DiscoveryResponse {
|
||||
return &DiscoveryResponse{
|
||||
Issuer: issuer,
|
||||
AuthorizationEndpoint: issuer + "/auth",
|
||||
TokenEndpoint: issuer + "/token",
|
||||
JwksURI: issuer + "/certs",
|
||||
UserinfoEndpoint: issuer + "/userinfo",
|
||||
RevocationEndpoint: issuer + "/revoke",
|
||||
ResponseTypesSupported: []string{"code id_token"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||
ScopesSupported: []string{"openid", "email", "profile"},
|
||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
ClaimsSupported: []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
|
||||
}
|
||||
}
|
||||
|
||||
type CertificatesResponse struct {
|
||||
Keys []*CertificatesResponseKey `json:"keys"`
|
||||
}
|
||||
@@ -73,29 +47,23 @@ type CertificatesResponseKey struct {
|
||||
E string `json:"e"`
|
||||
}
|
||||
|
||||
// NewCertificatesResponse returns a CertificatesResponse using the key pair.
|
||||
// This is used for verifying a signature of ID token.
|
||||
func NewCertificatesResponse(idTokenKeyPair *rsa.PrivateKey) *CertificatesResponse {
|
||||
return &CertificatesResponse{
|
||||
Keys: []*CertificatesResponseKey{
|
||||
{
|
||||
Kty: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Kid: "dummy",
|
||||
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
|
||||
N: base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
|
||||
},
|
||||
},
|
||||
}
|
||||
// AuthenticationRequest represents a type of:
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
type AuthenticationRequest struct {
|
||||
RedirectURI string
|
||||
State string
|
||||
Scope string // space separated string
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
RawQuery url.Values
|
||||
}
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
RedirectURI string
|
||||
State string
|
||||
Scope string // space separated string
|
||||
Nonce string
|
||||
RawQuery url.Values
|
||||
// TokenRequest represents a type of:
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
||||
type TokenRequest struct {
|
||||
Code string
|
||||
CodeVerifier string
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
@@ -106,17 +74,6 @@ type TokenResponse struct {
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
// NewTokenResponse returns a TokenResponse.
|
||||
func NewTokenResponse(idToken, refreshToken string) *TokenResponse {
|
||||
return &TokenResponse{
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
AccessToken: "YOUR_ACCESS_TOKEN",
|
||||
IDToken: idToken,
|
||||
RefreshToken: refreshToken,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response described in the following section:
|
||||
// 5.2 Error Response
|
||||
// https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
@@ -1,7 +1,5 @@
|
||||
// Package localserver provides a http server running on localhost.
|
||||
// This is only for testing.
|
||||
//
|
||||
package localserver
|
||||
// Package http provides a http server running on localhost for testing.
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
)
|
||||
|
||||
type Shutdowner interface {
|
||||
@@ -17,28 +15,23 @@ type Shutdowner interface {
|
||||
}
|
||||
|
||||
type shutdowner struct {
|
||||
l net.Listener
|
||||
s *http.Server
|
||||
}
|
||||
|
||||
func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
|
||||
// s.Shutdown() closes the lister as well,
|
||||
// so we do not need to call l.Close() explicitly
|
||||
if err := s.s.Shutdown(ctx); err != nil {
|
||||
t.Errorf("Could not shutdown the server: %s", err)
|
||||
t.Errorf("could not shutdown the server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts an authentication server.
|
||||
// If k is non-nil, it starts a TLS server.
|
||||
func Start(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
|
||||
if k == keys.None {
|
||||
func Start(t *testing.T, h http.Handler, k keypair.KeyPair) (string, Shutdowner) {
|
||||
if k == keypair.None {
|
||||
return startNoTLS(t, h)
|
||||
}
|
||||
return startTLS(t, h, k)
|
||||
}
|
||||
|
||||
func startNoTLS(t *testing.T, h http.Handler) (string, Shutdowner) {
|
||||
func startNoTLS(t *testing.T, h http.Handler) (string, *shutdowner) {
|
||||
t.Helper()
|
||||
l, port := newLocalhostListener(t)
|
||||
url := "http://localhost:" + port
|
||||
@@ -51,10 +44,10 @@ func startNoTLS(t *testing.T, h http.Handler) (string, Shutdowner) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
return url, &shutdowner{l, s}
|
||||
return url, &shutdowner{s}
|
||||
}
|
||||
|
||||
func startTLS(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
|
||||
func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) (string, *shutdowner) {
|
||||
t.Helper()
|
||||
l, port := newLocalhostListener(t)
|
||||
url := "https://localhost:" + port
|
||||
@@ -67,7 +60,7 @@ func startTLS(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
return url, &shutdowner{l, s}
|
||||
return url, &shutdowner{s}
|
||||
}
|
||||
|
||||
func newLocalhostListener(t *testing.T) (net.Listener, string) {
|
||||
217
integration_test/oidcserver/server.go
Normal file
217
integration_test/oidcserver/server.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// Package oidcserver provides a stub of OpenID Connect provider.
|
||||
package oidcserver
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
"github.com/int128/kubelogin/integration_test/oidcserver/handler"
|
||||
"github.com/int128/kubelogin/integration_test/oidcserver/http"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
http.Shutdowner
|
||||
IssuerURL() string
|
||||
SetConfig(Config)
|
||||
LastTokenResponse() *handler.TokenResponse
|
||||
}
|
||||
|
||||
// Want represents a set of expected values.
|
||||
type Want struct {
|
||||
Scope string
|
||||
RedirectURIPrefix string
|
||||
CodeChallengeMethod string // optional
|
||||
ExtraParams map[string]string // optional
|
||||
Username string // optional
|
||||
Password string // optional
|
||||
RefreshToken string // optional
|
||||
}
|
||||
|
||||
// Response represents a set of response values.
|
||||
type Response struct {
|
||||
IDTokenExpiry time.Time
|
||||
RefreshToken string
|
||||
RefreshError string // if set, Refresh() will return the error
|
||||
CodeChallengeMethodsSupported []string // optional
|
||||
}
|
||||
|
||||
// Config represents a configuration of the OpenID Connect provider.
|
||||
type Config struct {
|
||||
Want Want
|
||||
Response Response
|
||||
}
|
||||
|
||||
// New starts a HTTP server for the OpenID Connect provider.
|
||||
func New(t *testing.T, k keypair.KeyPair, c Config) Server {
|
||||
sv := server{Config: c, t: t}
|
||||
sv.issuerURL, sv.Shutdowner = http.Start(t, handler.New(t, &sv), k)
|
||||
return &sv
|
||||
}
|
||||
|
||||
type server struct {
|
||||
Config
|
||||
http.Shutdowner
|
||||
t *testing.T
|
||||
issuerURL string
|
||||
lastAuthenticationRequest *handler.AuthenticationRequest
|
||||
lastTokenResponse *handler.TokenResponse
|
||||
}
|
||||
|
||||
func (sv *server) IssuerURL() string {
|
||||
return sv.issuerURL
|
||||
}
|
||||
|
||||
func (sv *server) SetConfig(cfg Config) {
|
||||
sv.Config = cfg
|
||||
}
|
||||
|
||||
func (sv *server) LastTokenResponse() *handler.TokenResponse {
|
||||
return sv.lastTokenResponse
|
||||
}
|
||||
|
||||
func (sv *server) Discovery() *handler.DiscoveryResponse {
|
||||
// based on https://accounts.google.com/.well-known/openid-configuration
|
||||
return &handler.DiscoveryResponse{
|
||||
Issuer: sv.issuerURL,
|
||||
AuthorizationEndpoint: sv.issuerURL + "/auth",
|
||||
TokenEndpoint: sv.issuerURL + "/token",
|
||||
JwksURI: sv.issuerURL + "/certs",
|
||||
UserinfoEndpoint: sv.issuerURL + "/userinfo",
|
||||
RevocationEndpoint: sv.issuerURL + "/revoke",
|
||||
ResponseTypesSupported: []string{"code id_token"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||
ScopesSupported: []string{"openid", "email", "profile"},
|
||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
|
||||
CodeChallengeMethodsSupported: sv.Config.Response.CodeChallengeMethodsSupported,
|
||||
ClaimsSupported: []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *server) GetCertificates() *handler.CertificatesResponse {
|
||||
idTokenKeyPair := jwt.PrivateKey
|
||||
return &handler.CertificatesResponse{
|
||||
Keys: []*handler.CertificatesResponseKey{
|
||||
{
|
||||
Kty: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Kid: "dummy",
|
||||
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
|
||||
N: base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *server) AuthenticateCode(req handler.AuthenticationRequest) (code string, err error) {
|
||||
if req.Scope != sv.Want.Scope {
|
||||
sv.t.Errorf("scope wants `%s` but was `%s`", sv.Want.Scope, req.Scope)
|
||||
}
|
||||
if !strings.HasPrefix(req.RedirectURI, sv.Want.RedirectURIPrefix) {
|
||||
sv.t.Errorf("redirectURI wants prefix `%s` but was `%s`", sv.Want.RedirectURIPrefix, req.RedirectURI)
|
||||
}
|
||||
if req.CodeChallengeMethod != sv.Want.CodeChallengeMethod {
|
||||
sv.t.Errorf("code_challenge_method wants `%s` but was `%s`", sv.Want.CodeChallengeMethod, req.CodeChallengeMethod)
|
||||
}
|
||||
for k, v := range sv.Want.ExtraParams {
|
||||
got := req.RawQuery.Get(k)
|
||||
if got != v {
|
||||
sv.t.Errorf("parameter %s wants `%s` but was `%s`", k, v, got)
|
||||
}
|
||||
}
|
||||
sv.lastAuthenticationRequest = &req
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
}
|
||||
|
||||
func (sv *server) Exchange(req handler.TokenRequest) (*handler.TokenResponse, error) {
|
||||
if req.Code != "YOUR_AUTH_CODE" {
|
||||
return nil, xerrors.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.Code)
|
||||
}
|
||||
if sv.lastAuthenticationRequest.CodeChallengeMethod == "S256" {
|
||||
// https://tools.ietf.org/html/rfc7636#section-4.6
|
||||
challenge := computeS256Challenge(req.CodeVerifier)
|
||||
if challenge != sv.lastAuthenticationRequest.CodeChallenge {
|
||||
sv.t.Errorf("pkce S256 challenge did not match (want %s but was %s)", sv.lastAuthenticationRequest.CodeChallenge, challenge)
|
||||
}
|
||||
}
|
||||
resp := &handler.TokenResponse{
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
AccessToken: "YOUR_ACCESS_TOKEN",
|
||||
RefreshToken: sv.Response.RefreshToken,
|
||||
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
|
||||
claims.Issuer = sv.issuerURL
|
||||
claims.Subject = "SUBJECT"
|
||||
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
|
||||
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
|
||||
claims.Audience = []string{"kubernetes"}
|
||||
claims.Nonce = sv.lastAuthenticationRequest.Nonce
|
||||
}),
|
||||
}
|
||||
sv.lastTokenResponse = resp
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func computeS256Challenge(verifier string) string {
|
||||
c := sha256.Sum256([]byte(verifier))
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(c[:])
|
||||
}
|
||||
|
||||
func (sv *server) AuthenticatePassword(username, password, scope string) (*handler.TokenResponse, error) {
|
||||
if scope != sv.Want.Scope {
|
||||
sv.t.Errorf("scope wants `%s` but was `%s`", sv.Want.Scope, scope)
|
||||
}
|
||||
if username != sv.Want.Username {
|
||||
sv.t.Errorf("username wants `%s` but was `%s`", sv.Want.Username, username)
|
||||
}
|
||||
if password != sv.Want.Password {
|
||||
sv.t.Errorf("password wants `%s` but was `%s`", sv.Want.Password, password)
|
||||
}
|
||||
resp := &handler.TokenResponse{
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
AccessToken: "YOUR_ACCESS_TOKEN",
|
||||
RefreshToken: sv.Response.RefreshToken,
|
||||
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
|
||||
claims.Issuer = sv.issuerURL
|
||||
claims.Subject = "SUBJECT"
|
||||
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
|
||||
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
|
||||
claims.Audience = []string{"kubernetes"}
|
||||
}),
|
||||
}
|
||||
sv.lastTokenResponse = resp
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (sv *server) Refresh(refreshToken string) (*handler.TokenResponse, error) {
|
||||
if refreshToken != sv.Want.RefreshToken {
|
||||
sv.t.Errorf("refreshToken wants %s but was %s", sv.Want.RefreshToken, refreshToken)
|
||||
}
|
||||
if sv.Response.RefreshError != "" {
|
||||
return nil, &handler.ErrorResponse{Code: "invalid_request", Description: sv.Response.RefreshError}
|
||||
}
|
||||
resp := &handler.TokenResponse{
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
AccessToken: "YOUR_ACCESS_TOKEN",
|
||||
RefreshToken: sv.Response.RefreshToken,
|
||||
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
|
||||
claims.Issuer = sv.issuerURL
|
||||
claims.Subject = "SUBJECT"
|
||||
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
|
||||
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
|
||||
claims.Audience = []string{"kubernetes"}
|
||||
}),
|
||||
}
|
||||
sv.lastTokenResponse = resp
|
||||
return resp, nil
|
||||
}
|
||||
@@ -6,16 +6,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keys"
|
||||
"github.com/int128/kubelogin/integration_test/httpdriver"
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
"github.com/int128/kubelogin/integration_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/integration_test/localserver"
|
||||
"github.com/int128/kubelogin/integration_test/oidcserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
"github.com/int128/kubelogin/pkg/testing/clock"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
)
|
||||
|
||||
@@ -27,207 +24,246 @@ import (
|
||||
// 4. Verify the kubeconfig.
|
||||
//
|
||||
func TestStandalone(t *testing.T) {
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
testStandalone(t, keys.None)
|
||||
})
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
testStandalone(t, keys.Server)
|
||||
})
|
||||
}
|
||||
timeout := 3 * time.Second
|
||||
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
timeout := 5 * time.Second
|
||||
for name, tc := range map[string]struct {
|
||||
keyPair keypair.KeyPair
|
||||
args []string
|
||||
}{
|
||||
"NoTLS": {},
|
||||
"TLS": {
|
||||
keyPair: keypair.Server,
|
||||
},
|
||||
} {
|
||||
httpDriverOption := httpdriver.Option{
|
||||
TLSConfig: tc.keyPair.TLSConfig,
|
||||
BodyContains: "Authenticated",
|
||||
}
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("AuthCode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: sv.IssuerURL(),
|
||||
IDPCertificateAuthority: tc.keyPair.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: sv.LastTokenResponse().RefreshToken,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ROPC", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Username: "USER1",
|
||||
Password: "PASS1",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: sv.IssuerURL(),
|
||||
IDPCertificateAuthority: tc.keyPair.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.Zero(t),
|
||||
now: now,
|
||||
args: []string{
|
||||
"--username", "USER1",
|
||||
"--password", "PASS1",
|
||||
},
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: sv.LastTokenResponse().RefreshToken,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("TokenLifecycle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: sv.IssuerURL(),
|
||||
IDPCertificateAuthority: tc.keyPair.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
t.Run("NoToken", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
},
|
||||
})
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
})
|
||||
})
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{})
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.Zero(t),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
})
|
||||
})
|
||||
t.Run("Refresh", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(3 * time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
},
|
||||
})
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now.Add(2 * time.Hour),
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
})
|
||||
})
|
||||
t.Run("RefreshAgain", func(t *testing.T) {
|
||||
sv.SetConfig(oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(5 * time.Hour),
|
||||
},
|
||||
})
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
now: now.Add(4 * time.Hour),
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("TLSData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", nil, &idToken)
|
||||
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
Issuer: sv.IssuerURL(),
|
||||
IDPCertificateAuthorityData: keypair.Server.CACertBase64,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig}),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", nil, &idToken)
|
||||
provider.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: sv.LastTokenResponse().RefreshToken,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("env_KUBECONFIG", func(t *testing.T) {
|
||||
// do not run this in parallel due to change of the env var
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "openid", nil, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
Issuer: sv.IssuerURL(),
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: sv.LastTokenResponse().RefreshToken,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -235,42 +271,49 @@ func testStandalone(t *testing.T, idpTLS keys.Keys) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupAuthCodeFlow(t, provider, serverURL, "profile groups openid", nil, &idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
|
||||
Want: oidcserver.Want{
|
||||
Scope: "profile groups openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: oidcserver.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
defer sv.Shutdown(t, ctx)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
ExtraScopes: "profile,groups",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
Issuer: sv.IssuerURL(),
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDToken: sv.LastTokenResponse().IDToken,
|
||||
RefreshToken: sv.LastTokenResponse().RefreshToken,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func runRootCmd(t *testing.T, ctx context.Context, b browser.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(logger.New(t), b, nil)
|
||||
type standaloneConfig struct {
|
||||
issuerURL string
|
||||
kubeConfigFilename string
|
||||
httpDriver browser.Interface
|
||||
now time.Time
|
||||
args []string
|
||||
}
|
||||
|
||||
func runStandalone(t *testing.T, ctx context.Context, cfg standaloneConfig) {
|
||||
cmd := di.NewCmdForHeadless(clock.Fake(cfg.now), os.Stdin, os.Stdout, logger.New(t), cfg.httpDriver)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin",
|
||||
"--v=1",
|
||||
"--kubeconfig", cfg.kubeConfigFilename,
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
}, args...), "HEAD")
|
||||
}, cfg.args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
|
||||
@@ -9,30 +9,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Open mocks base method
|
||||
// Open mocks base method.
|
||||
func (m *MockInterface) Open(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Open", arg0)
|
||||
@@ -40,7 +40,7 @@ func (m *MockInterface) Open(arg0 string) error {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Open indicates an expected call of Open
|
||||
// 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)
|
||||
|
||||
@@ -10,30 +10,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddBase64Encoded mocks base method
|
||||
// AddBase64Encoded mocks base method.
|
||||
func (m *MockInterface) AddBase64Encoded(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddBase64Encoded", arg0)
|
||||
@@ -41,13 +41,13 @@ func (m *MockInterface) AddBase64Encoded(arg0 string) error {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddBase64Encoded indicates an expected call of AddBase64Encoded
|
||||
// AddBase64Encoded indicates an expected call of AddBase64Encoded.
|
||||
func (mr *MockInterfaceMockRecorder) AddBase64Encoded(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBase64Encoded", reflect.TypeOf((*MockInterface)(nil).AddBase64Encoded), arg0)
|
||||
}
|
||||
|
||||
// AddFile mocks base method
|
||||
// AddFile mocks base method.
|
||||
func (m *MockInterface) AddFile(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddFile", arg0)
|
||||
@@ -55,19 +55,19 @@ func (m *MockInterface) AddFile(arg0 string) error {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddFile indicates an expected call of AddFile
|
||||
// AddFile indicates an expected call of AddFile.
|
||||
func (mr *MockInterfaceMockRecorder) AddFile(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFile", reflect.TypeOf((*MockInterface)(nil).AddFile), arg0)
|
||||
}
|
||||
|
||||
// SetRootCAs mocks base method
|
||||
// SetRootCAs mocks base method.
|
||||
func (m *MockInterface) SetRootCAs(arg0 *tls.Config) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetRootCAs", arg0)
|
||||
}
|
||||
|
||||
// SetRootCAs indicates an expected call of SetRootCAs
|
||||
// SetRootCAs indicates an expected call of SetRootCAs.
|
||||
func (mr *MockInterfaceMockRecorder) SetRootCAs(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRootCAs", reflect.TypeOf((*MockInterface)(nil).SetRootCAs), arg0)
|
||||
|
||||
@@ -8,17 +8,17 @@ import (
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Clock), "*"),
|
||||
wire.Bind(new(Interface), new(*Clock)),
|
||||
wire.Struct(new(Real), "*"),
|
||||
wire.Bind(new(Interface), new(*Real)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
type Clock struct{}
|
||||
type Real struct{}
|
||||
|
||||
// Now returns the current time.
|
||||
func (c *Clock) Now() time.Time {
|
||||
func (c *Real) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
87
pkg/adaptors/cmd/authentication.go
Normal file
87
pkg/adaptors/cmd/authentication.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type authenticationOptions struct {
|
||||
GrantType string
|
||||
ListenAddress []string
|
||||
ListenPort []int // deprecated
|
||||
SkipOpenBrowser bool
|
||||
OpenURLAfterAuthentication string
|
||||
RedirectURLHostname string
|
||||
AuthRequestExtraParams map[string]string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// determineListenAddress returns the addresses from the flags.
|
||||
// Note that --listen-address is always given due to the default value.
|
||||
// If --listen-port is not set, it returns --listen-address.
|
||||
// If --listen-port is set, it returns the strings of --listen-port.
|
||||
func (o *authenticationOptions) determineListenAddress() []string {
|
||||
if len(o.ListenPort) == 0 {
|
||||
return o.ListenAddress
|
||||
}
|
||||
var a []string
|
||||
for _, p := range o.ListenPort {
|
||||
a = append(a, fmt.Sprintf("127.0.0.1:%d", p))
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
var allGrantType = strings.Join([]string{
|
||||
"auto",
|
||||
"authcode",
|
||||
"authcode-keyboard",
|
||||
"password",
|
||||
}, "|")
|
||||
|
||||
func (o *authenticationOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("Authorization grant type to use. One of (%s)", allGrantType))
|
||||
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "[authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order")
|
||||
//TODO: remove the deprecated flag
|
||||
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "[authcode] deprecated: port to bind to the local server")
|
||||
if err := f.MarkDeprecated("listen-port", "use --listen-address instead"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "[authcode] Do not open the browser automatically")
|
||||
f.StringVar(&o.OpenURLAfterAuthentication, "open-url-after-authentication", "", "[authcode] If set, open the URL in the browser after authentication")
|
||||
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "[authcode] Hostname of the redirect URL")
|
||||
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "[authcode, authcode-keyboard] Extra query parameters to send with an authentication request")
|
||||
f.StringVar(&o.Username, "username", "", "[password] Username for resource owner password credentials grant")
|
||||
f.StringVar(&o.Password, "password", "", "[password] Password for resource owner password credentials grant")
|
||||
}
|
||||
|
||||
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
|
||||
switch {
|
||||
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
|
||||
s.AuthCodeBrowserOption = &authcode.BrowserOption{
|
||||
BindAddress: o.determineListenAddress(),
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
OpenURLAfterAuthentication: o.OpenURLAfterAuthentication,
|
||||
RedirectURLHostname: o.RedirectURLHostname,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
}
|
||||
case o.GrantType == "authcode-keyboard":
|
||||
s.AuthCodeKeyboardOption = &authcode.KeyboardOption{
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
}
|
||||
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
|
||||
s.ROPCOption = &ropc.Option{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
default:
|
||||
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
@@ -52,7 +53,7 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
cmd.Logger.Printf("kubelogin version %s", version)
|
||||
cmd.Logger.Printf("kubelogin version %s (%s %s_%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
@@ -26,8 +28,9 @@ func TestCmd_Run(t *testing.T) {
|
||||
args: []string{executable},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -40,8 +43,9 @@ func TestCmd_Run(t *testing.T) {
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -56,8 +60,9 @@ func TestCmd_Run(t *testing.T) {
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -68,12 +73,14 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--context", "hello.k8s.local",
|
||||
"--user", "google",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--certificate-authority-data", "BASE64ENCODED",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--grant-type", "authcode",
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--skip-open-browser",
|
||||
"--open-url-after-authentication", "https://example.com/success.html",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
@@ -82,11 +89,14 @@ func TestCmd_Run(t *testing.T) {
|
||||
KubeconfigContext: "hello.k8s.local",
|
||||
KubeconfigUser: "google",
|
||||
CACertFilename: "/path/to/cacert",
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
OpenURLAfterAuthentication: "https://example.com/success.html",
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -97,7 +107,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
|
||||
AuthCodeKeyboardOption: &authcode.KeyboardOption{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -111,7 +121,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
ROPCOption: &ropc.Option{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
@@ -127,7 +137,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
ROPCOption: &ropc.Option{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
@@ -190,8 +200,9 @@ func TestCmd_Run(t *testing.T) {
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -212,6 +223,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--skip-open-browser",
|
||||
"--open-url-after-authentication", "https://example.com/success.html",
|
||||
"--oidc-auth-request-extra-params", "ttl=86400",
|
||||
"--oidc-auth-request-extra-params", "reauth=true",
|
||||
"--username", "USER",
|
||||
@@ -227,10 +239,12 @@ func TestCmd_Run(t *testing.T) {
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
OpenURLAfterAuthentication: "https://example.com/success.html",
|
||||
RedirectURLHostname: "localhost",
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -248,7 +262,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{
|
||||
AuthCodeKeyboardOption: &authcode.KeyboardOption{
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400"},
|
||||
},
|
||||
},
|
||||
@@ -270,7 +284,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
ROPCOption: &ropc.Option{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
@@ -292,7 +306,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
ROPCOption: &ropc.Option{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
|
||||
@@ -14,24 +14,19 @@ type getTokenOptions struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
TokenCacheDir string
|
||||
tlsOptions tlsOptions
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
func (o *getTokenOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
func (o *getTokenOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
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)
|
||||
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for token cache")
|
||||
o.tlsOptions.addFlags(f)
|
||||
o.authenticationOptions.addFlags(f)
|
||||
}
|
||||
|
||||
type GetToken struct {
|
||||
@@ -66,9 +61,9 @@ func (cmd *GetToken) New() *cobra.Command {
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CACertFilename,
|
||||
CACertData: o.CACertData,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
CACertFilename: o.tlsOptions.CACertFilename,
|
||||
CACertData: o.tlsOptions.CACertData,
|
||||
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
|
||||
TokenCacheDir: o.TokenCacheDir,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
@@ -78,6 +73,7 @@ func (cmd *GetToken) New() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
c.Flags().SortFlags = false
|
||||
o.addFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const longDescription = `Login to the OpenID Connect provider.
|
||||
const rootDescription = `Log in to the OpenID Connect provider.
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
Run the following command to show the setup instruction:
|
||||
To show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
@@ -28,85 +24,16 @@ type rootOptions struct {
|
||||
Kubeconfig string
|
||||
Context string
|
||||
User string
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
tlsOptions tlsOptions
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
func (o *rootOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
func (o *rootOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
|
||||
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
|
||||
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
o.authenticationOptions.register(f)
|
||||
}
|
||||
|
||||
type authenticationOptions struct {
|
||||
GrantType string
|
||||
ListenAddress []string
|
||||
ListenPort []int // deprecated
|
||||
SkipOpenBrowser bool
|
||||
AuthRequestExtraParams map[string]string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// determineListenAddress returns the addresses from the flags.
|
||||
// Note that --listen-address is always given due to the default value.
|
||||
// If --listen-port is not set, it returns --listen-address.
|
||||
// If --listen-port is set, it returns the strings of --listen-port.
|
||||
func (o *authenticationOptions) determineListenAddress() []string {
|
||||
if len(o.ListenPort) == 0 {
|
||||
return o.ListenAddress
|
||||
}
|
||||
var a []string
|
||||
for _, p := range o.ListenPort {
|
||||
a = append(a, fmt.Sprintf("127.0.0.1:%d", p))
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
var allGrantType = strings.Join([]string{
|
||||
"auto",
|
||||
"authcode",
|
||||
"authcode-keyboard",
|
||||
"password",
|
||||
}, "|")
|
||||
|
||||
func (o *authenticationOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("The authorization grant type to use. One of (%s)", allGrantType))
|
||||
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "Address to bind to the local server. If multiple addresses are given, it will try binding in order")
|
||||
//TODO: remove the deprecated flag
|
||||
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "(Deprecated: use --listen-address)")
|
||||
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
|
||||
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "Extra query parameters to send with an authentication request")
|
||||
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
|
||||
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
|
||||
}
|
||||
|
||||
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
|
||||
switch {
|
||||
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
|
||||
s.AuthCodeOption = &authentication.AuthCodeOption{
|
||||
BindAddress: o.determineListenAddress(),
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
}
|
||||
case o.GrantType == "authcode-keyboard":
|
||||
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
}
|
||||
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
|
||||
s.ROPCOption = &authentication.ROPCOption{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
default:
|
||||
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
|
||||
}
|
||||
return
|
||||
f.StringVar(&o.Context, "context", "", "Name of the kubeconfig context to use")
|
||||
f.StringVar(&o.User, "user", "", "Name of the kubeconfig user to use. Prior to --context")
|
||||
o.tlsOptions.addFlags(f)
|
||||
o.authenticationOptions.addFlags(f)
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
@@ -116,10 +43,10 @@ type Root struct {
|
||||
|
||||
func (cmd *Root) New() *cobra.Command {
|
||||
var o rootOptions
|
||||
rootCmd := &cobra.Command{
|
||||
c := &cobra.Command{
|
||||
Use: "kubelogin",
|
||||
Short: "Login to the OpenID Connect provider",
|
||||
Long: longDescription,
|
||||
Short: "Log in to the OpenID Connect provider",
|
||||
Long: rootDescription,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
|
||||
@@ -130,8 +57,9 @@ func (cmd *Root) New() *cobra.Command {
|
||||
KubeconfigFilename: o.Kubeconfig,
|
||||
KubeconfigContext: kubeconfig.ContextName(o.Context),
|
||||
KubeconfigUser: kubeconfig.UserName(o.User),
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
CACertFilename: o.tlsOptions.CACertFilename,
|
||||
CACertData: o.tlsOptions.CACertData,
|
||||
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
|
||||
@@ -140,7 +68,8 @@ func (cmd *Root) New() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(rootCmd.Flags())
|
||||
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
|
||||
return rootCmd
|
||||
c.Flags().SortFlags = false
|
||||
o.addFlags(c.Flags())
|
||||
cmd.Logger.AddFlags(c.PersistentFlags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -13,22 +13,17 @@ type setupOptions struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
tlsOptions tlsOptions
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
func (o *setupOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
func (o *setupOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider")
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.StringVar(&o.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)
|
||||
o.tlsOptions.addFlags(f)
|
||||
o.authenticationOptions.addFlags(f)
|
||||
}
|
||||
|
||||
type Setup struct {
|
||||
@@ -51,9 +46,9 @@ func (cmd *Setup) New() *cobra.Command {
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CACertFilename,
|
||||
CACertData: o.CACertData,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
CACertFilename: o.tlsOptions.CACertFilename,
|
||||
CACertData: o.tlsOptions.CACertData,
|
||||
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
if c.Flags().Lookup("listen-address").Changed {
|
||||
@@ -69,6 +64,7 @@ func (cmd *Setup) New() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
c.Flags().SortFlags = false
|
||||
o.addFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
15
pkg/adaptors/cmd/tls.go
Normal file
15
pkg/adaptors/cmd/tls.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import "github.com/spf13/pflag"
|
||||
|
||||
type tlsOptions struct {
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded cert for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package credentialpluginwriter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/stdio"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_credentialpluginwriter/mock_credentialpluginwriter.go github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter Interface
|
||||
@@ -29,21 +29,23 @@ type Output struct {
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type Writer struct{}
|
||||
type Writer struct {
|
||||
Stdout stdio.Stdout
|
||||
}
|
||||
|
||||
// Write writes the ExecCredential to standard output for kubectl.
|
||||
func (*Writer) Write(out Output) error {
|
||||
ec := &v1beta1.ExecCredential{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
func (w *Writer) Write(out Output) error {
|
||||
ec := &clientauthenticationv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Kind: "ExecCredential",
|
||||
},
|
||||
Status: &v1beta1.ExecCredentialStatus{
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
Token: out.Token,
|
||||
ExpirationTimestamp: &v1.Time{Time: out.Expiry},
|
||||
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
|
||||
},
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e := json.NewEncoder(w.Stdout)
|
||||
if err := e.Encode(ec); err != nil {
|
||||
return xerrors.Errorf("could not write the ExecCredential: %w", err)
|
||||
}
|
||||
|
||||
@@ -10,30 +10,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
// Write mocks base method.
|
||||
func (m *MockInterface) Write(arg0 credentialpluginwriter.Output) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Write", arg0)
|
||||
@@ -41,7 +41,7 @@ func (m *MockInterface) Write(arg0 credentialpluginwriter.Output) error {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Write indicates an expected call of Write
|
||||
// Write indicates an expected call of Write.
|
||||
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)
|
||||
|
||||
@@ -10,30 +10,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetCurrentAuthProvider mocks base method
|
||||
// GetCurrentAuthProvider mocks base method.
|
||||
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
|
||||
@@ -42,13 +42,13 @@ func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.Cont
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
|
||||
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider.
|
||||
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// UpdateAuthProvider mocks base method
|
||||
// UpdateAuthProvider mocks base method.
|
||||
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
|
||||
@@ -56,7 +56,7 @@ func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
|
||||
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider.
|
||||
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
|
||||
|
||||
@@ -4,17 +4,27 @@ package oidcclient
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/clock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/logging"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type NewFunc func(ctx context.Context, config Config) (Interface, error)
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Factory), "*"),
|
||||
wire.Bind(new(FactoryInterface), new(*Factory)),
|
||||
)
|
||||
|
||||
type FactoryInterface interface {
|
||||
New(ctx context.Context, config Config) (Interface, error)
|
||||
}
|
||||
|
||||
// Config represents a configuration of OpenID Connect client.
|
||||
type Config struct {
|
||||
@@ -24,11 +34,15 @@ type Config struct {
|
||||
ExtraScopes []string // optional
|
||||
CertPool certpool.Interface
|
||||
SkipTLSVerify bool
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
type Factory struct {
|
||||
Clock clock.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
// New returns an instance of adaptors.Interface with the given configuration.
|
||||
func New(ctx context.Context, config Config) (Interface, error) {
|
||||
func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
|
||||
config.CertPool.SetRootCAs(&tlsConfig)
|
||||
@@ -38,7 +52,7 @@ func New(ctx context.Context, config Config) (Interface, error) {
|
||||
}
|
||||
loggingTransport := &logging.Transport{
|
||||
Base: baseTransport,
|
||||
Logger: config.Logger,
|
||||
Logger: f.Logger,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: loggingTransport,
|
||||
@@ -49,6 +63,10 @@ func New(ctx context.Context, config Config) (Interface, error) {
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("oidc discovery error: %w", err)
|
||||
}
|
||||
supportedPKCEMethods, err := extractSupportedPKCEMethods(provider)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not determine supported PKCE methods: %w", err)
|
||||
}
|
||||
return &client{
|
||||
httpClient: httpClient,
|
||||
provider: provider,
|
||||
@@ -58,6 +76,18 @@ func New(ctx context.Context, config Config) (Interface, error) {
|
||||
ClientSecret: config.ClientSecret,
|
||||
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
|
||||
},
|
||||
logger: config.Logger,
|
||||
clock: f.Clock,
|
||||
logger: f.Logger,
|
||||
supportedPKCEMethods: supportedPKCEMethods,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func extractSupportedPKCEMethods(provider *oidc.Provider) ([]string, error) {
|
||||
var d struct {
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||
}
|
||||
if err := provider.Claims(&d); err != nil {
|
||||
return nil, fmt.Errorf("invalid discovery document: %w", err)
|
||||
}
|
||||
return d.CodeChallengeMethodsSupported, nil
|
||||
}
|
||||
|
||||
@@ -8,48 +8,49 @@ import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
oidcclient "github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
oidc "github.com/int128/kubelogin/pkg/oidc"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ExchangeAuthCode mocks base method
|
||||
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidcclient.TokenSet, error) {
|
||||
// ExchangeAuthCode mocks base method.
|
||||
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExchangeAuthCode", arg0, arg1)
|
||||
ret0, _ := ret[0].(*oidcclient.TokenSet)
|
||||
ret0, _ := ret[0].(*oidc.TokenSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode
|
||||
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode.
|
||||
func (mr *MockInterfaceMockRecorder) ExchangeAuthCode(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthCode", reflect.TypeOf((*MockInterface)(nil).ExchangeAuthCode), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetAuthCodeURL mocks base method
|
||||
// GetAuthCodeURL mocks base method.
|
||||
func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthCodeURL", arg0)
|
||||
@@ -57,53 +58,67 @@ func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL
|
||||
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL.
|
||||
func (mr *MockInterfaceMockRecorder) GetAuthCodeURL(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthCodeURL", reflect.TypeOf((*MockInterface)(nil).GetAuthCodeURL), arg0)
|
||||
}
|
||||
|
||||
// GetTokenByAuthCode mocks base method
|
||||
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidcclient.TokenSet, error) {
|
||||
// GetTokenByAuthCode mocks base method.
|
||||
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidc.TokenSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*oidcclient.TokenSet)
|
||||
ret0, _ := ret[0].(*oidc.TokenSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode
|
||||
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode.
|
||||
func (mr *MockInterfaceMockRecorder) GetTokenByAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByAuthCode", reflect.TypeOf((*MockInterface)(nil).GetTokenByAuthCode), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// GetTokenByROPC mocks base method
|
||||
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidcclient.TokenSet, error) {
|
||||
// GetTokenByROPC mocks base method.
|
||||
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidc.TokenSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*oidcclient.TokenSet)
|
||||
ret0, _ := ret[0].(*oidc.TokenSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTokenByROPC indicates an expected call of GetTokenByROPC
|
||||
// GetTokenByROPC indicates an expected call of GetTokenByROPC.
|
||||
func (mr *MockInterfaceMockRecorder) GetTokenByROPC(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByROPC", reflect.TypeOf((*MockInterface)(nil).GetTokenByROPC), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Refresh mocks base method
|
||||
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.TokenSet, error) {
|
||||
// Refresh mocks base method.
|
||||
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
|
||||
ret0, _ := ret[0].(*oidcclient.TokenSet)
|
||||
ret0, _ := ret[0].(*oidc.TokenSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Refresh indicates an expected call of Refresh
|
||||
// Refresh indicates an expected call of Refresh.
|
||||
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
|
||||
}
|
||||
|
||||
// SupportedPKCEMethods mocks base method.
|
||||
func (m *MockInterface) SupportedPKCEMethods() []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SupportedPKCEMethods")
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SupportedPKCEMethods indicates an expected call of SupportedPKCEMethods.
|
||||
func (mr *MockInterfaceMockRecorder) SupportedPKCEMethods() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedPKCEMethods", reflect.TypeOf((*MockInterface)(nil).SupportedPKCEMethods))
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/google/wire"
|
||||
gooidc "github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/clock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/pkce"
|
||||
"github.com/int128/oauth2cli"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -16,57 +18,47 @@ import (
|
||||
|
||||
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient Interface
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Value(NewFunc(New)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
GetAuthCodeURL(in AuthCodeURLInput) string
|
||||
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error)
|
||||
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error)
|
||||
GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error)
|
||||
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
|
||||
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error)
|
||||
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error)
|
||||
GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error)
|
||||
Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error)
|
||||
SupportedPKCEMethods() []string
|
||||
}
|
||||
|
||||
type AuthCodeURLInput struct {
|
||||
State string
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
PKCEParams pkce.Params
|
||||
RedirectURI string
|
||||
AuthRequestExtraParams map[string]string
|
||||
}
|
||||
|
||||
type ExchangeAuthCodeInput struct {
|
||||
Code string
|
||||
CodeVerifier string
|
||||
Nonce string
|
||||
RedirectURI string
|
||||
Code string
|
||||
PKCEParams pkce.Params
|
||||
Nonce string
|
||||
RedirectURI string
|
||||
}
|
||||
|
||||
type GetTokenByAuthCodeInput struct {
|
||||
BindAddress []string
|
||||
State string
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
CodeVerifier string
|
||||
PKCEParams pkce.Params
|
||||
RedirectURLHostname string
|
||||
AuthRequestExtraParams map[string]string
|
||||
}
|
||||
|
||||
// TokenSet represents an output DTO of
|
||||
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
|
||||
type TokenSet struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenClaims jwt.Claims
|
||||
LocalServerSuccessHTML string
|
||||
}
|
||||
|
||||
type client struct {
|
||||
httpClient *http.Client
|
||||
provider *oidc.Provider
|
||||
oauth2Config oauth2.Config
|
||||
logger logger.Interface
|
||||
httpClient *http.Client
|
||||
provider *gooidc.Provider
|
||||
oauth2Config oauth2.Config
|
||||
clock clock.Interface
|
||||
logger logger.Interface
|
||||
supportedPKCEMethods []string
|
||||
}
|
||||
|
||||
func (c *client) wrapContext(ctx context.Context) context.Context {
|
||||
@@ -77,27 +69,19 @@ func (c *client) wrapContext(ctx context.Context) context.Context {
|
||||
}
|
||||
|
||||
// GetTokenByAuthCode performs the authorization code flow.
|
||||
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error) {
|
||||
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
config := oauth2cli.Config{
|
||||
OAuth2Config: c.oauth2Config,
|
||||
State: in.State,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{
|
||||
oauth2.AccessTypeOffline,
|
||||
oidc.Nonce(in.Nonce),
|
||||
oauth2.SetAuthURLParam("code_challenge", in.CodeChallenge),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", in.CodeChallengeMethod),
|
||||
},
|
||||
TokenRequestOptions: []oauth2.AuthCodeOption{
|
||||
oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier),
|
||||
},
|
||||
OAuth2Config: c.oauth2Config,
|
||||
State: in.State,
|
||||
AuthCodeOptions: authorizationRequestOptions(in.Nonce, in.PKCEParams, in.AuthRequestExtraParams),
|
||||
TokenRequestOptions: tokenRequestOptions(in.PKCEParams),
|
||||
LocalServerBindAddress: in.BindAddress,
|
||||
LocalServerReadyChan: localServerReadyChan,
|
||||
RedirectURLHostname: in.RedirectURLHostname,
|
||||
LocalServerSuccessHTML: in.LocalServerSuccessHTML,
|
||||
Logf: c.logger.V(1).Infof,
|
||||
}
|
||||
for key, value := range in.AuthRequestExtraParams {
|
||||
config.AuthCodeOptions = append(config.AuthCodeOptions, oauth2.SetAuthURLParam(key, value))
|
||||
}
|
||||
|
||||
token, err := oauth2cli.GetToken(ctx, config)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("oauth2 error: %w", err)
|
||||
@@ -109,32 +93,55 @@ func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeIn
|
||||
func (c *client) GetAuthCodeURL(in AuthCodeURLInput) string {
|
||||
cfg := c.oauth2Config
|
||||
cfg.RedirectURL = in.RedirectURI
|
||||
opts := []oauth2.AuthCodeOption{
|
||||
oauth2.AccessTypeOffline,
|
||||
oidc.Nonce(in.Nonce),
|
||||
oauth2.SetAuthURLParam("code_challenge", in.CodeChallenge),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", in.CodeChallengeMethod),
|
||||
}
|
||||
for key, value := range in.AuthRequestExtraParams {
|
||||
opts = append(opts, oauth2.SetAuthURLParam(key, value))
|
||||
}
|
||||
opts := authorizationRequestOptions(in.Nonce, in.PKCEParams, in.AuthRequestExtraParams)
|
||||
return cfg.AuthCodeURL(in.State, opts...)
|
||||
}
|
||||
|
||||
// ExchangeAuthCode exchanges the authorization code and token.
|
||||
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error) {
|
||||
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
cfg := c.oauth2Config
|
||||
cfg.RedirectURL = in.RedirectURI
|
||||
token, err := cfg.Exchange(ctx, in.Code, oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier))
|
||||
opts := tokenRequestOptions(in.PKCEParams)
|
||||
token, err := cfg.Exchange(ctx, in.Code, opts...)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("exchange error: %w", err)
|
||||
}
|
||||
return c.verifyToken(ctx, token, in.Nonce)
|
||||
}
|
||||
|
||||
func authorizationRequestOptions(n string, p pkce.Params, e map[string]string) []oauth2.AuthCodeOption {
|
||||
o := []oauth2.AuthCodeOption{
|
||||
oauth2.AccessTypeOffline,
|
||||
gooidc.Nonce(n),
|
||||
}
|
||||
if !p.IsZero() {
|
||||
o = append(o,
|
||||
oauth2.SetAuthURLParam("code_challenge", p.CodeChallenge),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", p.CodeChallengeMethod),
|
||||
)
|
||||
}
|
||||
for key, value := range e {
|
||||
o = append(o, oauth2.SetAuthURLParam(key, value))
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func tokenRequestOptions(p pkce.Params) (o []oauth2.AuthCodeOption) {
|
||||
if !p.IsZero() {
|
||||
o = append(o, oauth2.SetAuthURLParam("code_verifier", p.CodeVerifier))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SupportedPKCEMethods returns the PKCE methods supported by the provider.
|
||||
// This may return nil if PKCE is not supported.
|
||||
func (c *client) SupportedPKCEMethods() []string {
|
||||
return c.supportedPKCEMethods
|
||||
}
|
||||
|
||||
// GetTokenByROPC performs the resource owner password credentials flow.
|
||||
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error) {
|
||||
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
|
||||
if err != nil {
|
||||
@@ -144,7 +151,7 @@ func (c *client) GetTokenByROPC(ctx context.Context, username, password string)
|
||||
}
|
||||
|
||||
// Refresh sends a refresh token request and returns a token set.
|
||||
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
|
||||
func (c *client) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
currentToken := &oauth2.Token{
|
||||
Expiry: time.Now(),
|
||||
@@ -160,12 +167,12 @@ func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, e
|
||||
|
||||
// verifyToken verifies the token with the certificates of the provider and the nonce.
|
||||
// If the nonce is an empty string, it does not verify the nonce.
|
||||
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*TokenSet, error) {
|
||||
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*oidc.TokenSet, error) {
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
|
||||
verifier := c.provider.Verifier(&gooidc.Config{ClientID: c.oauth2Config.ClientID, Now: c.clock.Now})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, idToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the ID token: %w", err)
|
||||
@@ -177,7 +184,7 @@ func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce str
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the token: %w", err)
|
||||
}
|
||||
return &TokenSet{
|
||||
return &oidc.TokenSet{
|
||||
IDToken: idToken,
|
||||
IDTokenClaims: jwt.Claims{
|
||||
Subject: verifiedIDToken.Subject,
|
||||
|
||||
@@ -9,30 +9,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ReadPassword mocks base method
|
||||
// ReadPassword mocks base method.
|
||||
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadPassword", arg0)
|
||||
@@ -41,13 +41,13 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadPassword indicates an expected call of ReadPassword
|
||||
// ReadPassword indicates an expected call of ReadPassword.
|
||||
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
|
||||
}
|
||||
|
||||
// ReadString mocks base method
|
||||
// ReadString mocks base method.
|
||||
func (m *MockInterface) ReadString(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadString", arg0)
|
||||
@@ -56,7 +56,7 @@ func (m *MockInterface) ReadString(arg0 string) (string, error) {
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadString indicates an expected call of ReadString
|
||||
// ReadString indicates an expected call of ReadString.
|
||||
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/stdio"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
@@ -26,14 +27,16 @@ type Interface interface {
|
||||
ReadPassword(prompt string) (string, error)
|
||||
}
|
||||
|
||||
type Reader struct{}
|
||||
type Reader struct {
|
||||
Stdin stdio.Stdin
|
||||
}
|
||||
|
||||
// ReadString reads a string from the stdin.
|
||||
func (*Reader) ReadString(prompt string) (string, error) {
|
||||
func (x *Reader) ReadString(prompt string) (string, error) {
|
||||
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
|
||||
return "", xerrors.Errorf("write error: %w", err)
|
||||
}
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
r := bufio.NewReader(x.Stdin)
|
||||
s, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("read error: %w", err)
|
||||
|
||||
17
pkg/adaptors/stdio/stdio.go
Normal file
17
pkg/adaptors/stdio/stdio.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Package stdio wraps os.Stdin and os.Stdout for testing.
|
||||
package stdio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.InterfaceValue(new(Stdin), os.Stdin),
|
||||
wire.InterfaceValue(new(Stdout), os.Stdout),
|
||||
)
|
||||
|
||||
type Stdout io.Writer
|
||||
type Stdin io.Reader
|
||||
@@ -10,30 +10,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// FindByKey mocks base method
|
||||
// FindByKey mocks base method.
|
||||
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.Value, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
|
||||
@@ -42,13 +42,13 @@ func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindByKey indicates an expected call of FindByKey
|
||||
// FindByKey indicates an expected call of FindByKey.
|
||||
func (mr *MockInterfaceMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockInterface)(nil).FindByKey), arg0, arg1)
|
||||
}
|
||||
|
||||
// Save mocks base method
|
||||
// Save mocks base method.
|
||||
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.Value) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
|
||||
@@ -56,7 +56,7 @@ func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.V
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Save indicates an expected call of Save
|
||||
// Save indicates an expected call of Save.
|
||||
func (mr *MockInterfaceMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInterface)(nil).Save), arg0, arg1, arg2)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/reader"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/stdio"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
@@ -27,15 +28,16 @@ func NewCmd() cmd.Interface {
|
||||
NewCmdForHeadless,
|
||||
|
||||
// dependencies for production
|
||||
clock.Set,
|
||||
stdio.Set,
|
||||
logger.Set,
|
||||
browser.Set,
|
||||
credentialpluginwriter.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
|
||||
func NewCmdForHeadless(logger.Interface, browser.Interface, credentialpluginwriter.Interface) cmd.Interface {
|
||||
func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interface, browser.Interface) cmd.Interface {
|
||||
wire.Build(
|
||||
// use-cases
|
||||
authentication.Set,
|
||||
@@ -46,11 +48,11 @@ func NewCmdForHeadless(logger.Interface, browser.Interface, credentialpluginwrit
|
||||
// adaptors
|
||||
cmd.Set,
|
||||
reader.Set,
|
||||
clock.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
oidcclient.Set,
|
||||
certpool.Set,
|
||||
credentialpluginwriter.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,55 +15,72 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/reader"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/stdio"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Injectors from di.go:
|
||||
|
||||
// NewCmd returns an instance of adaptors.Cmd.
|
||||
func NewCmd() cmd.Interface {
|
||||
clockReal := &clock.Real{}
|
||||
stdin := _wireFileValue
|
||||
stdout := _wireOsFileValue
|
||||
loggerInterface := logger.New()
|
||||
browserBrowser := &browser.Browser{}
|
||||
writer := &credentialpluginwriter.Writer{}
|
||||
cmdInterface := NewCmdForHeadless(loggerInterface, browserBrowser, writer)
|
||||
cmdInterface := NewCmdForHeadless(clockReal, stdin, stdout, loggerInterface, browserBrowser)
|
||||
return cmdInterface
|
||||
}
|
||||
|
||||
func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browser.Interface, credentialpluginwriterInterface credentialpluginwriter.Interface) cmd.Interface {
|
||||
newFunc := _wireNewFuncValue
|
||||
clockClock := &clock.Clock{}
|
||||
authCode := &authentication.AuthCode{
|
||||
var (
|
||||
_wireFileValue = os.Stdin
|
||||
_wireOsFileValue = os.Stdout
|
||||
)
|
||||
|
||||
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
|
||||
func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout stdio.Stdout, loggerInterface logger.Interface, browserInterface browser.Interface) cmd.Interface {
|
||||
factory := &oidcclient.Factory{
|
||||
Clock: clockInterface,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
authcodeBrowser := &authcode.Browser{
|
||||
Browser: browserInterface,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
readerReader := &reader.Reader{}
|
||||
authCodeKeyboard := &authentication.AuthCodeKeyboard{
|
||||
readerReader := &reader.Reader{
|
||||
Stdin: stdin,
|
||||
}
|
||||
keyboard := &authcode.Keyboard{
|
||||
Reader: readerReader,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
ropc := &authentication.ROPC{
|
||||
ropcROPC := &ropc.ROPC{
|
||||
Reader: readerReader,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
authenticationAuthentication := &authentication.Authentication{
|
||||
NewOIDCClient: newFunc,
|
||||
OIDCClient: factory,
|
||||
Logger: loggerInterface,
|
||||
Clock: clockClock,
|
||||
AuthCode: authCode,
|
||||
AuthCodeKeyboard: authCodeKeyboard,
|
||||
ROPC: ropc,
|
||||
Clock: clockInterface,
|
||||
AuthCodeBrowser: authcodeBrowser,
|
||||
AuthCodeKeyboard: keyboard,
|
||||
ROPC: ropcROPC,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
certpoolNewFunc := _wireCertpoolNewFuncValue
|
||||
newFunc := _wireNewFuncValue
|
||||
standaloneStandalone := &standalone.Standalone{
|
||||
Authentication: authenticationAuthentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
NewCertPool: certpoolNewFunc,
|
||||
NewCertPool: newFunc,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
root := &cmd.Root{
|
||||
@@ -71,11 +88,14 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
writer := &credentialpluginwriter.Writer{
|
||||
Stdout: stdout,
|
||||
}
|
||||
getToken := &credentialplugin.GetToken{
|
||||
Authentication: authenticationAuthentication,
|
||||
TokenCacheRepository: repository,
|
||||
NewCertPool: certpoolNewFunc,
|
||||
Writer: credentialpluginwriterInterface,
|
||||
NewCertPool: newFunc,
|
||||
Writer: writer,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdGetToken := &cmd.GetToken{
|
||||
@@ -84,7 +104,7 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
|
||||
}
|
||||
setupSetup := &setup.Setup{
|
||||
Authentication: authenticationAuthentication,
|
||||
NewCertPool: certpoolNewFunc,
|
||||
NewCertPool: newFunc,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdSetup := &cmd.Setup{
|
||||
@@ -100,6 +120,5 @@ func NewCmdForHeadless(loggerInterface logger.Interface, browserInterface browse
|
||||
}
|
||||
|
||||
var (
|
||||
_wireNewFuncValue = oidcclient.NewFunc(oidcclient.New)
|
||||
_wireCertpoolNewFuncValue = certpool.NewFunc(certpool.New)
|
||||
_wireNewFuncValue = certpool.NewFunc(certpool.New)
|
||||
)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_computeS256(t *testing.T) {
|
||||
// Testdata described at:
|
||||
// https://tools.ietf.org/html/rfc7636#appendix-B
|
||||
b := []byte{
|
||||
116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
|
||||
187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
|
||||
132, 141, 121,
|
||||
}
|
||||
p := computeS256(b)
|
||||
if want := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; want != p.CodeVerifier {
|
||||
t.Errorf("CodeVerifier wants %s but was %s", want, p.CodeVerifier)
|
||||
}
|
||||
if want := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; want != p.CodeChallenge {
|
||||
t.Errorf("CodeChallenge wants %s but was %s", want, p.CodeChallenge)
|
||||
}
|
||||
if p.CodeChallengeMethod != "S256" {
|
||||
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
)
|
||||
|
||||
type timeProvider time.Time
|
||||
@@ -2,13 +2,21 @@ package oidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// TokenSet represents an output DTO of
|
||||
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
|
||||
type TokenSet struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenClaims jwt.Claims
|
||||
}
|
||||
|
||||
func NewState() (string, error) {
|
||||
b, err := random32()
|
||||
if err != nil {
|
||||
@@ -25,21 +33,6 @@ func NewNonce() (string, error) {
|
||||
return base64URLEncode(b), nil
|
||||
}
|
||||
|
||||
type PKCEParams struct {
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
CodeVerifier string
|
||||
}
|
||||
|
||||
func NewPKCEParams() (*PKCEParams, error) {
|
||||
b, err := random32()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a random: %w", err)
|
||||
}
|
||||
s := computeS256(b)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func random32() ([]byte, error) {
|
||||
b := make([]byte, 32)
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
|
||||
@@ -48,17 +41,6 @@ func random32() ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func computeS256(b []byte) PKCEParams {
|
||||
v := base64URLEncode(b)
|
||||
s := sha256.New()
|
||||
_, _ = s.Write([]byte(v))
|
||||
return PKCEParams{
|
||||
CodeChallenge: base64URLEncode(s.Sum(nil)),
|
||||
CodeChallengeMethod: "S256",
|
||||
CodeVerifier: v,
|
||||
}
|
||||
}
|
||||
|
||||
func base64URLEncode(b []byte) string {
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
|
||||
}
|
||||
74
pkg/pkce/pkce.go
Normal file
74
pkg/pkce/pkce.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Package pkce provides generation of the PKCE parameters.
|
||||
// See also https://tools.ietf.org/html/rfc7636.
|
||||
package pkce
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var Plain Params
|
||||
|
||||
const (
|
||||
// code challenge methods defined as https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
methodS256 = "S256"
|
||||
)
|
||||
|
||||
// Params represents a set of the PKCE parameters.
|
||||
type Params struct {
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
CodeVerifier string
|
||||
}
|
||||
|
||||
func (p Params) IsZero() bool {
|
||||
return p == Params{}
|
||||
}
|
||||
|
||||
// New returns a parameters supported by the provider.
|
||||
// You need to pass the code challenge methods defined in RFC7636.
|
||||
// It returns Plain if no method is available.
|
||||
func New(methods []string) (Params, error) {
|
||||
for _, method := range methods {
|
||||
if method == methodS256 {
|
||||
return NewS256()
|
||||
}
|
||||
}
|
||||
return Plain, nil
|
||||
}
|
||||
|
||||
// NewS256 generates a parameters for S256.
|
||||
func NewS256() (Params, error) {
|
||||
b, err := random32()
|
||||
if err != nil {
|
||||
return Plain, xerrors.Errorf("could not generate a random: %w", err)
|
||||
}
|
||||
return computeS256(b), nil
|
||||
}
|
||||
|
||||
func random32() ([]byte, error) {
|
||||
b := make([]byte, 32)
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
|
||||
return nil, xerrors.Errorf("read error: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func computeS256(b []byte) Params {
|
||||
v := base64URLEncode(b)
|
||||
s := sha256.New()
|
||||
_, _ = s.Write([]byte(v))
|
||||
return Params{
|
||||
CodeChallenge: base64URLEncode(s.Sum(nil)),
|
||||
CodeChallengeMethod: methodS256,
|
||||
CodeVerifier: v,
|
||||
}
|
||||
}
|
||||
|
||||
func base64URLEncode(b []byte) string {
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
|
||||
}
|
||||
61
pkg/pkce/pkce_test.go
Normal file
61
pkg/pkce/pkce_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package pkce
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Run("S256", func(t *testing.T) {
|
||||
p, err := New([]string{"plain", "S256"})
|
||||
if err != nil {
|
||||
t.Fatalf("New error: %s", err)
|
||||
}
|
||||
if p.CodeChallengeMethod != "S256" {
|
||||
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
|
||||
}
|
||||
if p.CodeChallenge == "" {
|
||||
t.Errorf("CodeChallenge wants non-empty but was empty")
|
||||
}
|
||||
if p.CodeVerifier == "" {
|
||||
t.Errorf("CodeVerifier wants non-empty but was empty")
|
||||
}
|
||||
})
|
||||
t.Run("plain", func(t *testing.T) {
|
||||
p, err := New([]string{"plain"})
|
||||
if err != nil {
|
||||
t.Fatalf("New error: %s", err)
|
||||
}
|
||||
if !p.IsZero() {
|
||||
t.Errorf("IsZero wants true but was false")
|
||||
}
|
||||
})
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
p, err := New(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New error: %s", err)
|
||||
}
|
||||
if !p.IsZero() {
|
||||
t.Errorf("IsZero wants true but was false")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_computeS256(t *testing.T) {
|
||||
// Testdata described at:
|
||||
// https://tools.ietf.org/html/rfc7636#appendix-B
|
||||
b := []byte{
|
||||
116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
|
||||
187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
|
||||
132, 141, 121,
|
||||
}
|
||||
p := computeS256(b)
|
||||
if want := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; want != p.CodeVerifier {
|
||||
t.Errorf("CodeVerifier wants %s but was %s", want, p.CodeVerifier)
|
||||
}
|
||||
if want := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; want != p.CodeChallenge {
|
||||
t.Errorf("CodeChallenge wants %s but was %s", want, p.CodeChallenge)
|
||||
}
|
||||
if p.CodeChallengeMethod != "S256" {
|
||||
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
|
||||
}
|
||||
}
|
||||
29
pkg/testing/httpserver/httpserver.go
Normal file
29
pkg/testing/httpserver/httpserver.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/BrowserSuccessHTML", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("content-type", "text/html")
|
||||
_, _ = w.Write([]byte(authcode.BrowserSuccessHTML))
|
||||
})
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("content-type", "text/html")
|
||||
_, _ = w.Write([]byte(`
|
||||
<html>
|
||||
<body>
|
||||
<ul>
|
||||
<li><a href="BrowserSuccessHTML">BrowserSuccessHTML</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
})
|
||||
log.Printf("http://localhost:8000")
|
||||
log.Fatal(http.ListenAndServe("127.0.0.1:8000", nil))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package authentication
|
||||
package authcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,19 +6,28 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/domain/oidc"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/pkce"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// AuthCode provides the authentication code flow.
|
||||
type AuthCode struct {
|
||||
type BrowserOption struct {
|
||||
SkipOpenBrowser bool
|
||||
BindAddress []string
|
||||
OpenURLAfterAuthentication string
|
||||
RedirectURLHostname string
|
||||
AuthRequestExtraParams map[string]string
|
||||
}
|
||||
|
||||
// Browser provides the authentication code flow using the browser.
|
||||
type Browser struct {
|
||||
Browser browser.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.Interface) (*Output, error) {
|
||||
u.Logger.V(1).Infof("performing the authentication code flow")
|
||||
func (u *Browser) Do(ctx context.Context, o *BrowserOption, client oidcclient.Interface) (*oidc.TokenSet, error) {
|
||||
u.Logger.V(1).Infof("starting the authentication code flow using the browser")
|
||||
state, err := oidc.NewState()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a state: %w", err)
|
||||
@@ -27,22 +36,26 @@ func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
|
||||
}
|
||||
p, err := oidc.NewPKCEParams()
|
||||
p, err := pkce.New(client.SupportedPKCEMethods())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
|
||||
}
|
||||
successHTML := BrowserSuccessHTML
|
||||
if o.OpenURLAfterAuthentication != "" {
|
||||
successHTML = BrowserRedirectHTML(o.OpenURLAfterAuthentication)
|
||||
}
|
||||
in := oidcclient.GetTokenByAuthCodeInput{
|
||||
BindAddress: o.BindAddress,
|
||||
State: state,
|
||||
Nonce: nonce,
|
||||
CodeChallenge: p.CodeChallenge,
|
||||
CodeChallengeMethod: p.CodeChallengeMethod,
|
||||
CodeVerifier: p.CodeVerifier,
|
||||
PKCEParams: p,
|
||||
RedirectURLHostname: o.RedirectURLHostname,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
LocalServerSuccessHTML: successHTML,
|
||||
}
|
||||
readyChan := make(chan string, 1)
|
||||
defer close(readyChan)
|
||||
var out Output
|
||||
var out *oidc.TokenSet
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
select {
|
||||
@@ -54,6 +67,7 @@ func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.
|
||||
u.Logger.Printf("Please visit the following URL in your browser: %s", url)
|
||||
return nil
|
||||
}
|
||||
u.Logger.V(1).Infof("opening %s in the browser", url)
|
||||
if err := u.Browser.Open(url); err != nil {
|
||||
u.Logger.Printf(`error: could not open the browser: %s
|
||||
|
||||
@@ -70,15 +84,13 @@ Please visit the following URL in your browser manually: %s`, err, url)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authorization code flow error: %w", err)
|
||||
}
|
||||
out = Output{
|
||||
IDToken: tokenSet.IDToken,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
}
|
||||
out = tokenSet
|
||||
u.Logger.V(1).Infof("got a token set by the authorization code flow")
|
||||
return nil
|
||||
})
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
u.Logger.V(1).Infof("finished the authorization code flow via the browser")
|
||||
return out, nil
|
||||
}
|
||||
60
pkg/usecases/authentication/authcode/browser_html.go
Normal file
60
pkg/usecases/authentication/authcode/browser_html.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package authcode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// BrowserSuccessHTML is the success page on browser based authentication.
|
||||
const BrowserSuccessHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authenticated</title>
|
||||
<script>
|
||||
window.close()
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #eee;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.placeholder {
|
||||
margin: 2em;
|
||||
padding: 2em;
|
||||
background-color: #fff;
|
||||
border-radius: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="placeholder">
|
||||
<h1>Authenticated</h1>
|
||||
<p>You have logged in to the cluster. You can close this window.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func BrowserRedirectHTML(target string) string {
|
||||
targetURL, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`invalid URL is set: %s`, err)
|
||||
}
|
||||
return fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;URL=%s">
|
||||
<meta charset="UTF-8">
|
||||
<title>Authenticated</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="%s">redirecting...</a>
|
||||
</body>
|
||||
</html>
|
||||
`, targetURL, targetURL)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package authentication
|
||||
package authcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
)
|
||||
|
||||
func TestAuthCode_Do(t *testing.T) {
|
||||
func TestBrowser_Do(t *testing.T) {
|
||||
dummyTokenClaims := jwt.Claims{
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
@@ -27,36 +28,45 @@ func TestAuthCode_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:8000"},
|
||||
SkipOpenBrowser: true,
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
o := &BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:8000"},
|
||||
SkipOpenBrowser: true,
|
||||
OpenURLAfterAuthentication: "https://example.com/success.html",
|
||||
RedirectURLHostname: "localhost",
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().SupportedPKCEMethods()
|
||||
mockOIDCClient.EXPECT().
|
||||
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Do(func(_ context.Context, in oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
|
||||
if diff := cmp.Diff(o.BindAddress, in.BindAddress); diff != "" {
|
||||
t.Errorf("BindAddress mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(BrowserRedirectHTML("https://example.com/success.html"), in.LocalServerSuccessHTML); diff != "" {
|
||||
t.Errorf("LocalServerSuccessHTML mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(o.RedirectURLHostname, in.RedirectURLHostname); diff != "" {
|
||||
t.Errorf("RedirectURLHostname mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(o.AuthRequestExtraParams, in.AuthRequestExtraParams); diff != "" {
|
||||
t.Errorf("AuthRequestExtraParams mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
readyChan <- "LOCAL_SERVER_URL"
|
||||
}).
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
u := AuthCode{
|
||||
u := Browser{
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
got, err := u.Do(ctx, o, mockOIDCClient)
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
want := &oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -71,16 +81,17 @@ func TestAuthCode_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &AuthCodeOption{
|
||||
o := &BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:8000"},
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().SupportedPKCEMethods()
|
||||
mockOIDCClient.EXPECT().
|
||||
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
|
||||
readyChan <- "LOCAL_SERVER_URL"
|
||||
}).
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -88,7 +99,7 @@ func TestAuthCode_Do(t *testing.T) {
|
||||
mockBrowser := mock_browser.NewMockInterface(ctrl)
|
||||
mockBrowser.EXPECT().
|
||||
Open("LOCAL_SERVER_URL")
|
||||
u := AuthCode{
|
||||
u := Browser{
|
||||
Logger: logger.New(t),
|
||||
Browser: mockBrowser,
|
||||
}
|
||||
@@ -96,7 +107,7 @@ func TestAuthCode_Do(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
want := &oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -1,4 +1,4 @@
|
||||
package authentication
|
||||
package authcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,21 +6,26 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/reader"
|
||||
"github.com/int128/kubelogin/pkg/domain/oidc"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/pkce"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const authCodeKeyboardPrompt = "Enter code: "
|
||||
const keyboardPrompt = "Enter code: "
|
||||
const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
|
||||
|
||||
// AuthCodeKeyboard provides the authorization code flow with keyboard interactive.
|
||||
type AuthCodeKeyboard struct {
|
||||
type KeyboardOption struct {
|
||||
AuthRequestExtraParams map[string]string
|
||||
}
|
||||
|
||||
// Keyboard provides the authorization code flow with keyboard interactive.
|
||||
type Keyboard struct {
|
||||
Reader reader.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (u *AuthCodeKeyboard) Do(ctx context.Context, o *AuthCodeKeyboardOption, client oidcclient.Interface) (*Output, error) {
|
||||
u.Logger.V(1).Infof("performing the authorization code flow with keyboard interactive")
|
||||
func (u *Keyboard) Do(ctx context.Context, o *KeyboardOption, client oidcclient.Interface) (*oidc.TokenSet, error) {
|
||||
u.Logger.V(1).Infof("starting the authorization code flow with keyboard interactive")
|
||||
state, err := oidc.NewState()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a state: %w", err)
|
||||
@@ -29,36 +34,33 @@ func (u *AuthCodeKeyboard) Do(ctx context.Context, o *AuthCodeKeyboardOption, cl
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
|
||||
}
|
||||
p, err := oidc.NewPKCEParams()
|
||||
p, err := pkce.New(client.SupportedPKCEMethods())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
|
||||
}
|
||||
authCodeURL := client.GetAuthCodeURL(oidcclient.AuthCodeURLInput{
|
||||
State: state,
|
||||
Nonce: nonce,
|
||||
CodeChallenge: p.CodeChallenge,
|
||||
CodeChallengeMethod: p.CodeChallengeMethod,
|
||||
PKCEParams: p,
|
||||
RedirectURI: oobRedirectURI,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
})
|
||||
u.Logger.Printf("Open %s", authCodeURL)
|
||||
code, err := u.Reader.ReadString(authCodeKeyboardPrompt)
|
||||
u.Logger.Printf("Please visit the following URL in your browser: %s", authCodeURL)
|
||||
code, err := u.Reader.ReadString(keyboardPrompt)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not read an authorization code: %w", err)
|
||||
}
|
||||
|
||||
u.Logger.V(1).Infof("exchanging the code and token")
|
||||
tokenSet, err := client.ExchangeAuthCode(ctx, oidcclient.ExchangeAuthCodeInput{
|
||||
Code: code,
|
||||
CodeVerifier: p.CodeVerifier,
|
||||
Nonce: nonce,
|
||||
RedirectURI: oobRedirectURI,
|
||||
Code: code,
|
||||
PKCEParams: p,
|
||||
Nonce: nonce,
|
||||
RedirectURI: oobRedirectURI,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not exchange the authorization code: %w", err)
|
||||
}
|
||||
return &Output{
|
||||
IDToken: tokenSet.IDToken,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
}, nil
|
||||
u.Logger.V(1).Infof("finished the authorization code flow with keyboard interactive")
|
||||
return tokenSet, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package authentication
|
||||
package authcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/reader/mock_reader"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
)
|
||||
|
||||
var nonNil = gomock.Not(gomock.Nil())
|
||||
|
||||
func TestAuthCodeKeyboard_Do(t *testing.T) {
|
||||
func TestKeyboard_Do(t *testing.T) {
|
||||
dummyTokenClaims := jwt.Claims{
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
@@ -29,10 +30,11 @@ func TestAuthCodeKeyboard_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &AuthCodeKeyboardOption{
|
||||
o := &KeyboardOption{
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().SupportedPKCEMethods()
|
||||
mockOIDCClient.EXPECT().
|
||||
GetAuthCodeURL(nonNil).
|
||||
Do(func(in oidcclient.AuthCodeURLInput) {
|
||||
@@ -48,16 +50,16 @@ func TestAuthCodeKeyboard_Do(t *testing.T) {
|
||||
t.Errorf("Code wants YOUR_AUTH_CODE but was %s", in.Code)
|
||||
}
|
||||
}).
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
mockReader := mock_reader.NewMockInterface(ctrl)
|
||||
mockReader.EXPECT().
|
||||
ReadString(authCodeKeyboardPrompt).
|
||||
ReadString(keyboardPrompt).
|
||||
Return("YOUR_AUTH_CODE", nil)
|
||||
u := AuthCodeKeyboard{
|
||||
u := Keyboard{
|
||||
Reader: mockReader,
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
@@ -65,7 +67,7 @@ func TestAuthCodeKeyboard_Do(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
want := &oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/clock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -18,9 +21,9 @@ import (
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Authentication), "*"),
|
||||
wire.Bind(new(Interface), new(*Authentication)),
|
||||
wire.Struct(new(AuthCode), "*"),
|
||||
wire.Struct(new(AuthCodeKeyboard), "*"),
|
||||
wire.Struct(new(ROPC), "*"),
|
||||
wire.Struct(new(authcode.Browser), "*"),
|
||||
wire.Struct(new(authcode.Keyboard), "*"),
|
||||
wire.Struct(new(ropc.ROPC), "*"),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
@@ -41,37 +44,17 @@ type Input struct {
|
||||
}
|
||||
|
||||
type GrantOptionSet struct {
|
||||
AuthCodeOption *AuthCodeOption
|
||||
AuthCodeKeyboardOption *AuthCodeKeyboardOption
|
||||
ROPCOption *ROPCOption
|
||||
}
|
||||
|
||||
type AuthCodeOption struct {
|
||||
SkipOpenBrowser bool
|
||||
BindAddress []string
|
||||
AuthRequestExtraParams map[string]string
|
||||
}
|
||||
|
||||
type AuthCodeKeyboardOption struct {
|
||||
AuthRequestExtraParams map[string]string
|
||||
}
|
||||
|
||||
type ROPCOption struct {
|
||||
Username string
|
||||
Password string // If empty, read a password using Reader.ReadPassword()
|
||||
AuthCodeBrowserOption *authcode.BrowserOption
|
||||
AuthCodeKeyboardOption *authcode.KeyboardOption
|
||||
ROPCOption *ropc.Option
|
||||
}
|
||||
|
||||
// Output represents an output DTO of the Authentication use-case.
|
||||
type Output struct {
|
||||
AlreadyHasValidIDToken bool
|
||||
IDToken string
|
||||
IDTokenClaims jwt.Claims
|
||||
RefreshToken string
|
||||
TokenSet oidc.TokenSet
|
||||
}
|
||||
|
||||
const usernamePrompt = "Username: "
|
||||
const passwordPrompt = "Password: "
|
||||
|
||||
// Authentication provides the internal use-case of authentication.
|
||||
//
|
||||
// If the IDToken is not set, it performs the authentication flow.
|
||||
@@ -86,12 +69,12 @@ const passwordPrompt = "Password: "
|
||||
// If the Password is not set, it asks a password by the prompt.
|
||||
//
|
||||
type Authentication struct {
|
||||
NewOIDCClient oidcclient.NewFunc
|
||||
OIDCClient oidcclient.FactoryInterface
|
||||
Logger logger.Interface
|
||||
Clock clock.Interface
|
||||
AuthCode *AuthCode
|
||||
AuthCodeKeyboard *AuthCodeKeyboard
|
||||
ROPC *ROPC
|
||||
AuthCodeBrowser *authcode.Browser
|
||||
AuthCodeKeyboard *authcode.Keyboard
|
||||
ROPC *ropc.ROPC
|
||||
}
|
||||
|
||||
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
|
||||
@@ -108,23 +91,24 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
|
||||
u.Logger.V(1).Infof("you already have a valid token until %s", claims.Expiry)
|
||||
return &Output{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: in.IDToken,
|
||||
RefreshToken: in.RefreshToken,
|
||||
IDTokenClaims: *claims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: in.IDToken,
|
||||
RefreshToken: in.RefreshToken,
|
||||
IDTokenClaims: *claims,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
u.Logger.V(1).Infof("you have an expired token at %s", claims.Expiry)
|
||||
}
|
||||
|
||||
u.Logger.V(1).Infof("initializing an OpenID Connect client")
|
||||
client, err := u.NewOIDCClient(ctx, oidcclient.Config{
|
||||
client, err := u.OIDCClient.New(ctx, oidcclient.Config{
|
||||
IssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
CertPool: in.CertPool,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
Logger: u.Logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("oidc error: %w", err)
|
||||
@@ -135,22 +119,36 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
|
||||
out, err := client.Refresh(ctx, in.RefreshToken)
|
||||
if err == nil {
|
||||
return &Output{
|
||||
IDToken: out.IDToken,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
RefreshToken: out.RefreshToken,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: out.IDToken,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
RefreshToken: out.RefreshToken,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
u.Logger.V(1).Infof("could not refresh the token: %s", err)
|
||||
}
|
||||
|
||||
if in.GrantOptionSet.AuthCodeOption != nil {
|
||||
return u.AuthCode.Do(ctx, in.GrantOptionSet.AuthCodeOption, client)
|
||||
if in.GrantOptionSet.AuthCodeBrowserOption != nil {
|
||||
tokenSet, err := u.AuthCodeBrowser.Do(ctx, in.GrantOptionSet.AuthCodeBrowserOption, client)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("authcode-browser error: %w", err)
|
||||
}
|
||||
return &Output{TokenSet: *tokenSet}, nil
|
||||
}
|
||||
if in.GrantOptionSet.AuthCodeKeyboardOption != nil {
|
||||
return u.AuthCodeKeyboard.Do(ctx, in.GrantOptionSet.AuthCodeKeyboardOption, client)
|
||||
tokenSet, err := u.AuthCodeKeyboard.Do(ctx, in.GrantOptionSet.AuthCodeKeyboardOption, client)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("authcode-keyboard error: %w", err)
|
||||
}
|
||||
return &Output{TokenSet: *tokenSet}, nil
|
||||
}
|
||||
if in.GrantOptionSet.ROPCOption != nil {
|
||||
return u.ROPC.Do(ctx, in.GrantOptionSet.ROPCOption, client)
|
||||
tokenSet, err := u.ROPC.Do(ctx, in.GrantOptionSet.ROPCOption, client)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("ropc error: %w", err)
|
||||
}
|
||||
return &Output{TokenSet: *tokenSet}, nil
|
||||
}
|
||||
return nil, xerrors.Errorf("any authorization grant must be set")
|
||||
}
|
||||
|
||||
@@ -7,19 +7,18 @@ import (
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/clock"
|
||||
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
testingLogger "github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var cmpIgnoreLogger = cmpopts.IgnoreInterfaces(struct{ logger.Interface }{})
|
||||
|
||||
func TestAuthentication_Do(t *testing.T) {
|
||||
timeout := 5 * time.Second
|
||||
expiryTime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
@@ -55,15 +54,17 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}
|
||||
want := &Output{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: cachedIDToken,
|
||||
IDTokenClaims: jwt.Claims{
|
||||
Subject: "SUBJECT",
|
||||
Expiry: expiryTime,
|
||||
Pretty: `{
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: cachedIDToken,
|
||||
IDTokenClaims: jwt.Claims{
|
||||
Subject: "SUBJECT",
|
||||
Expiry: expiryTime,
|
||||
Pretty: `{
|
||||
"exp": 1577934245,
|
||||
"iss": "https://issuer.example.com",
|
||||
"sub": "SUBJECT"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
@@ -86,22 +87,20 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Refresh(ctx, "VALID_REFRESH_TOKEN").
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
}, nil)
|
||||
u := Authentication{
|
||||
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
|
||||
want := oidcclient.Config{
|
||||
OIDCClient: &oidcclientFactory{
|
||||
t: t,
|
||||
client: mockOIDCClient,
|
||||
want: oidcclient.Config{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
}
|
||||
if diff := cmp.Diff(want, got, cmpIgnoreLogger); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
return mockOIDCClient, nil
|
||||
},
|
||||
},
|
||||
Logger: testingLogger.New(t),
|
||||
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
|
||||
@@ -111,23 +110,25 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken/AuthCode", func(t *testing.T) {
|
||||
t.Run("HasExpiredRefreshToken/Browser", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
GrantOptionSet: GrantOptionSet{
|
||||
AuthCodeOption: &AuthCodeOption{
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: []string{"127.0.0.1:8000"},
|
||||
SkipOpenBrowser: true,
|
||||
},
|
||||
@@ -139,6 +140,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().SupportedPKCEMethods()
|
||||
mockOIDCClient.EXPECT().
|
||||
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, xerrors.New("token has expired"))
|
||||
@@ -147,26 +149,24 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
|
||||
readyChan <- "LOCAL_SERVER_URL"
|
||||
}).
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
}, nil)
|
||||
u := Authentication{
|
||||
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
|
||||
want := oidcclient.Config{
|
||||
OIDCClient: &oidcclientFactory{
|
||||
t: t,
|
||||
client: mockOIDCClient,
|
||||
want: oidcclient.Config{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
}
|
||||
if diff := cmp.Diff(want, got, cmpIgnoreLogger); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
return mockOIDCClient, nil
|
||||
},
|
||||
},
|
||||
Logger: testingLogger.New(t),
|
||||
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
|
||||
AuthCode: &AuthCode{
|
||||
AuthCodeBrowser: &authcode.Browser{
|
||||
Logger: testingLogger.New(t),
|
||||
},
|
||||
}
|
||||
@@ -175,9 +175,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
@@ -191,7 +193,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
defer cancel()
|
||||
in := Input{
|
||||
GrantOptionSet: GrantOptionSet{
|
||||
ROPCOption: &ROPCOption{
|
||||
ROPCOption: &ropc.Option{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
@@ -203,25 +205,23 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
GetTokenByROPC(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
}, nil)
|
||||
u := Authentication{
|
||||
NewOIDCClient: func(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
|
||||
want := oidcclient.Config{
|
||||
OIDCClient: &oidcclientFactory{
|
||||
t: t,
|
||||
client: mockOIDCClient,
|
||||
want: oidcclient.Config{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
}
|
||||
if diff := cmp.Diff(want, got, cmpIgnoreLogger); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
return mockOIDCClient, nil
|
||||
},
|
||||
},
|
||||
Logger: testingLogger.New(t),
|
||||
ROPC: &ROPC{
|
||||
ROPC: &ropc.ROPC{
|
||||
Logger: testingLogger.New(t),
|
||||
},
|
||||
}
|
||||
@@ -230,12 +230,27 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyClaims,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type oidcclientFactory struct {
|
||||
t *testing.T
|
||||
client oidcclient.Interface
|
||||
want oidcclient.Config
|
||||
}
|
||||
|
||||
func (f *oidcclientFactory) New(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
|
||||
if diff := cmp.Diff(f.want, got); diff != "" {
|
||||
f.t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
return f.client, nil
|
||||
}
|
||||
|
||||
@@ -11,30 +11,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Do mocks base method
|
||||
// Do mocks base method.
|
||||
func (m *MockInterface) Do(arg0 context.Context, arg1 authentication.Input) (*authentication.Output, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Do", arg0, arg1)
|
||||
@@ -43,7 +43,7 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 authentication.Input) (*au
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Do indicates an expected call of Do
|
||||
// Do indicates an expected call of Do.
|
||||
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package authentication
|
||||
package ropc
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,17 +6,26 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/reader"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const usernamePrompt = "Username: "
|
||||
const passwordPrompt = "Password: "
|
||||
|
||||
type Option struct {
|
||||
Username string
|
||||
Password string // If empty, read a password using Reader.ReadPassword()
|
||||
}
|
||||
|
||||
// ROPC provides the resource owner password credentials flow.
|
||||
type ROPC struct {
|
||||
Reader reader.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (u *ROPC) Do(ctx context.Context, in *ROPCOption, client oidcclient.Interface) (*Output, error) {
|
||||
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
|
||||
func (u *ROPC) Do(ctx context.Context, in *Option, client oidcclient.Interface) (*oidc.TokenSet, error) {
|
||||
u.Logger.V(1).Infof("starting the resource owner password credentials flow")
|
||||
if in.Username == "" {
|
||||
var err error
|
||||
in.Username, err = u.Reader.ReadString(usernamePrompt)
|
||||
@@ -35,9 +44,6 @@ func (u *ROPC) Do(ctx context.Context, in *ROPCOption, client oidcclient.Interfa
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("resource owner password credentials flow error: %w", err)
|
||||
}
|
||||
return &Output{
|
||||
IDToken: tokenSet.IDToken,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
}, nil
|
||||
u.Logger.V(1).Infof("finished the resource owner password credentials flow")
|
||||
return tokenSet, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package authentication
|
||||
package ropc
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/reader/mock_reader"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
@@ -28,11 +28,11 @@ func TestROPC_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &ROPCOption{}
|
||||
o := &Option{}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
GetTokenByROPC(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
@@ -48,7 +48,7 @@ func TestROPC_Do(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
want := &oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -63,14 +63,14 @@ func TestROPC_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &ROPCOption{
|
||||
o := &Option{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
GetTokenByROPC(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -82,7 +82,7 @@ func TestROPC_Do(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
want := &oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -97,13 +97,13 @@ func TestROPC_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &ROPCOption{
|
||||
o := &Option{
|
||||
Username: "USER",
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
GetTokenByROPC(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidcclient.TokenSet{
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -118,7 +118,7 @@ func TestROPC_Do(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
want := &oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
@@ -133,7 +133,7 @@ func TestROPC_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
o := &ROPCOption{
|
||||
o := &Option{
|
||||
Username: "USER",
|
||||
}
|
||||
mockEnv := mock_reader.NewMockInterface(ctrl)
|
||||
@@ -54,7 +54,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
return xerrors.Errorf("could not get a token: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("writing the token to client-go")
|
||||
if err := u.Writer.Write(credentialpluginwriter.Output{Token: out.IDToken, Expiry: out.IDTokenClaims.Expiry}); err != nil {
|
||||
if err := u.Writer.Write(credentialpluginwriter.Output{Token: out.TokenSet.IDToken, Expiry: out.TokenSet.IDTokenClaims.Expiry}); err != nil {
|
||||
return xerrors.Errorf("could not write the token to client-go: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -101,16 +101,16 @@ func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("you got a token: %s", out.IDTokenClaims.Pretty)
|
||||
u.Logger.V(1).Infof("you got a token: %s", out.TokenSet.IDTokenClaims.Pretty)
|
||||
if out.AlreadyHasValidIDToken {
|
||||
u.Logger.V(1).Infof("you already have a valid token until %s", out.IDTokenClaims.Expiry)
|
||||
u.Logger.V(1).Infof("you already have a valid token until %s", out.TokenSet.IDTokenClaims.Expiry)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
u.Logger.V(1).Infof("you got a valid token until %s", out.IDTokenClaims.Expiry)
|
||||
u.Logger.V(1).Infof("you got a valid token until %s", out.TokenSet.IDTokenClaims.Expiry)
|
||||
newTokenCacheValue := tokencache.Value{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
IDToken: out.TokenSet.IDToken,
|
||||
RefreshToken: out.TokenSet.RefreshToken,
|
||||
}
|
||||
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, tokenCacheKey, newTokenCacheValue); err != nil {
|
||||
return nil, xerrors.Errorf("could not write the token cache: %w", err)
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache/mock_tokencache"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
|
||||
@@ -57,9 +58,11 @@ func TestGetToken_Do(t *testing.T) {
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}).
|
||||
Return(&authentication.Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
},
|
||||
}, nil)
|
||||
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
@@ -127,8 +130,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
}).
|
||||
Return(&authentication.Output{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
},
|
||||
}, nil)
|
||||
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
|
||||
@@ -11,30 +11,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Do mocks base method
|
||||
// Do mocks base method.
|
||||
func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Do", arg0, arg1)
|
||||
@@ -42,7 +42,7 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) er
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Do indicates an expected call of Do
|
||||
// Do indicates an expected call of Do.
|
||||
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
|
||||
|
||||
@@ -18,19 +18,9 @@ You got a token with the following claims:
|
||||
|
||||
## 3. Bind a cluster role
|
||||
|
||||
Apply the following manifest:
|
||||
Run the following command:
|
||||
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: oidc-cluster-admin
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: {{ .IssuerURL }}#{{ .Subject }}
|
||||
kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='{{ .IssuerURL }}#{{ .Subject }}'
|
||||
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
@@ -49,7 +39,7 @@ Run the following command:
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
{{- range .Args }}
|
||||
--exec-arg={{ . }}
|
||||
--exec-arg={{ . }} \
|
||||
{{- end }}
|
||||
|
||||
## 6. Verify cluster access
|
||||
@@ -113,11 +103,11 @@ func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
|
||||
}
|
||||
|
||||
v := stage2Vars{
|
||||
IDTokenPrettyJSON: out.IDTokenClaims.Pretty,
|
||||
IDTokenPrettyJSON: out.TokenSet.IDTokenClaims.Pretty,
|
||||
IssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
Args: makeCredentialPluginArgs(in),
|
||||
Subject: out.IDTokenClaims.Subject,
|
||||
Subject: out.TokenSet.IDTokenClaims.Subject,
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := stage2Tpl.Execute(&b, &v); err != nil {
|
||||
@@ -147,8 +137,8 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
}
|
||||
|
||||
if in.GrantOptionSet.AuthCodeOption != nil {
|
||||
if in.GrantOptionSet.AuthCodeOption.SkipOpenBrowser {
|
||||
if in.GrantOptionSet.AuthCodeBrowserOption != nil {
|
||||
if in.GrantOptionSet.AuthCodeBrowserOption.SkipOpenBrowser {
|
||||
args = append(args, "--skip-open-browser")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +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/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
|
||||
@@ -45,12 +46,14 @@ func TestSetup_DoStage2(t *testing.T) {
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}).
|
||||
Return(&authentication.Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: jwt.Claims{
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
Pretty: "PRETTY_JSON",
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: jwt.Claims{
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
Pretty: "PRETTY_JSON",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
u := Setup{
|
||||
|
||||
@@ -11,30 +11,30 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Do mocks base method
|
||||
// Do mocks base method.
|
||||
func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Do", arg0, arg1)
|
||||
@@ -42,7 +42,7 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Do indicates an expected call of Do
|
||||
// Do indicates an expected call of Do.
|
||||
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
|
||||
|
||||
@@ -2,8 +2,6 @@ package standalone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool"
|
||||
@@ -30,12 +28,23 @@ type Input struct {
|
||||
KubeconfigFilename string // Default to the environment variable or global config as kubectl
|
||||
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
|
||||
KubeconfigUser kubeconfig.UserName // Default to the user of the context
|
||||
CACertFilename string // If set, use the CA cert
|
||||
CACertFilename string // optional
|
||||
CACertData string // optional
|
||||
SkipTLSVerify bool
|
||||
GrantOptionSet authentication.GrantOptionSet
|
||||
}
|
||||
|
||||
const oidcConfigErrorMessage = `You need to set up the kubeconfig for OpenID Connect authentication.
|
||||
const oidcConfigErrorMessage = `No configuration found.
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
To show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
`
|
||||
|
||||
const deprecationMessage = `NOTE: You can use the credential plugin mode for better user experience.
|
||||
Kubectl automatically runs kubelogin and you do not need to run kubelogin explicitly.
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
`
|
||||
|
||||
@@ -60,9 +69,7 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
|
||||
u.Logger.Printf(oidcConfigErrorMessage)
|
||||
return xerrors.Errorf("could not find the current authentication provider: %w", err)
|
||||
}
|
||||
if err := u.showDeprecation(in, authProvider); err != nil {
|
||||
return xerrors.Errorf("could not show deprecation message: %w", err)
|
||||
}
|
||||
u.Logger.Printf(deprecationMessage)
|
||||
u.Logger.V(1).Infof("using the authentication provider of the user %s", authProvider.UserName)
|
||||
u.Logger.V(1).Infof("a token will be written to %s", authProvider.LocationOfOrigin)
|
||||
certPool := u.NewCertPool()
|
||||
@@ -78,7 +85,12 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
|
||||
}
|
||||
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{
|
||||
@@ -95,79 +107,18 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("you got a token: %s", out.IDTokenClaims.Pretty)
|
||||
u.Logger.V(1).Infof("you got a token: %s", out.TokenSet.IDTokenClaims.Pretty)
|
||||
if out.AlreadyHasValidIDToken {
|
||||
u.Logger.Printf("You already have a valid token until %s", out.IDTokenClaims.Expiry)
|
||||
u.Logger.Printf("You already have a valid token until %s", out.TokenSet.IDTokenClaims.Expiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Logger.Printf("You got a valid token until %s", out.IDTokenClaims.Expiry)
|
||||
authProvider.IDToken = out.IDToken
|
||||
authProvider.RefreshToken = out.RefreshToken
|
||||
u.Logger.Printf("You got a valid token until %s", out.TokenSet.IDTokenClaims.Expiry)
|
||||
authProvider.IDToken = out.TokenSet.IDToken
|
||||
authProvider.RefreshToken = out.TokenSet.RefreshToken
|
||||
u.Logger.V(1).Infof("writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
|
||||
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
|
||||
return xerrors.Errorf("could not update the kubeconfig: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var deprecationTpl = template.Must(template.New("").Parse(
|
||||
`IMPORTANT NOTICE:
|
||||
The credential plugin mode is available since v1.14.0.
|
||||
Kubectl will automatically run kubelogin and you do not need to run kubelogin explicitly.
|
||||
|
||||
You can switch to the credential plugin mode by 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 {
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error {
|
||||
var args []string
|
||||
args = append(args, "--oidc-issuer-url="+p.IDPIssuerURL)
|
||||
args = append(args, "--oidc-client-id="+p.ClientID)
|
||||
if p.ClientSecret != "" {
|
||||
args = append(args, "--oidc-client-secret="+p.ClientSecret)
|
||||
}
|
||||
for _, extraScope := range p.ExtraScopes {
|
||||
args = append(args, "--oidc-extra-scope="+extraScope)
|
||||
}
|
||||
if p.IDPCertificateAuthority != "" {
|
||||
args = append(args, "--certificate-authority="+p.IDPCertificateAuthority)
|
||||
}
|
||||
if p.IDPCertificateAuthorityData != "" {
|
||||
args = append(args, "--certificate-authority-data="+p.IDPCertificateAuthorityData)
|
||||
}
|
||||
if in.CACertFilename != "" {
|
||||
args = append(args, "--certificate-authority="+in.CACertFilename)
|
||||
}
|
||||
if in.GrantOptionSet.ROPCOption != nil {
|
||||
if in.GrantOptionSet.ROPCOption.Username != "" {
|
||||
args = append(args, "--username="+in.GrantOptionSet.ROPCOption.Username)
|
||||
}
|
||||
}
|
||||
|
||||
v := deprecationVars{
|
||||
Args: args,
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := deprecationTpl.Execute(&b, &v); err != nil {
|
||||
return xerrors.Errorf("template error: %w", err)
|
||||
}
|
||||
u.Logger.Printf("%s", b.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig/mock_kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/domain/jwt"
|
||||
"github.com/int128/kubelogin/pkg/jwt"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
|
||||
@@ -34,6 +35,7 @@ func TestStandalone_Do(t *testing.T) {
|
||||
KubeconfigContext: "theContext",
|
||||
KubeconfigUser: "theUser",
|
||||
CACertFilename: "/path/to/cert1",
|
||||
CACertData: "BASE64ENCODED1",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
@@ -44,7 +46,7 @@ func TestStandalone_Do(t *testing.T) {
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDPCertificateAuthority: "/path/to/cert2",
|
||||
IDPCertificateAuthorityData: "BASE64ENCODED",
|
||||
IDPCertificateAuthorityData: "BASE64ENCODED2",
|
||||
}
|
||||
mockCertPool := mock_certpool.NewMockInterface(ctrl)
|
||||
mockCertPool.EXPECT().
|
||||
@@ -52,7 +54,9 @@ func TestStandalone_Do(t *testing.T) {
|
||||
mockCertPool.EXPECT().
|
||||
AddFile("/path/to/cert2")
|
||||
mockCertPool.EXPECT().
|
||||
AddBase64Encoded("BASE64ENCODED")
|
||||
AddBase64Encoded("BASE64ENCODED1")
|
||||
mockCertPool.EXPECT().
|
||||
AddBase64Encoded("BASE64ENCODED2")
|
||||
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
|
||||
mockKubeconfig.EXPECT().
|
||||
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
|
||||
@@ -65,7 +69,7 @@ func TestStandalone_Do(t *testing.T) {
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDPCertificateAuthority: "/path/to/cert2",
|
||||
IDPCertificateAuthorityData: "BASE64ENCODED",
|
||||
IDPCertificateAuthorityData: "BASE64ENCODED2",
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
@@ -80,9 +84,11 @@ func TestStandalone_Do(t *testing.T) {
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}).
|
||||
Return(&authentication.Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
},
|
||||
}, nil)
|
||||
u := Standalone{
|
||||
Authentication: mockAuthentication,
|
||||
@@ -124,8 +130,10 @@ func TestStandalone_Do(t *testing.T) {
|
||||
}).
|
||||
Return(&authentication.Output{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
},
|
||||
}, nil)
|
||||
u := Standalone{
|
||||
Authentication: mockAuthentication,
|
||||
@@ -232,9 +240,11 @@ func TestStandalone_Do(t *testing.T) {
|
||||
CertPool: mockCertPool,
|
||||
}).
|
||||
Return(&authentication.Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
TokenSet: oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
},
|
||||
}, nil)
|
||||
u := Standalone{
|
||||
Authentication: mockAuthentication,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: kubelogin
|
||||
version: git
|
||||
summary: Log in to the OpenID Connect provider
|
||||
description: |
|
||||
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
|
||||
confinement: strict
|
||||
base: core18
|
||||
parts:
|
||||
kubelogin:
|
||||
plugin: nil
|
||||
source: .
|
||||
source-type: git
|
||||
build-snaps: [go]
|
||||
override-build: |
|
||||
make CIRCLE_TAG=$SNAPCRAFT_PROJECT_VERSION
|
||||
cp -av kubelogin $SNAPCRAFT_PART_INSTALL/bin
|
||||
apps:
|
||||
kubelogin:
|
||||
command: bin/kubelogin
|
||||
109
system_test/Makefile
Normal file
109
system_test/Makefile
Normal file
@@ -0,0 +1,109 @@
|
||||
CLUSTER_NAME := kubelogin-system-test
|
||||
OUTPUT_DIR := $(CURDIR)/output
|
||||
|
||||
PATH := $(PATH):$(OUTPUT_DIR)/bin
|
||||
export PATH
|
||||
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
|
||||
export KUBECONFIG
|
||||
|
||||
# run the login script instead of opening chrome
|
||||
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
|
||||
export BROWSER
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
# see the setup instruction
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--oidc-extra-scope=email \
|
||||
--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# set up the kubeconfig
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--exec-arg=--oidc-extra-scope=email \
|
||||
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# make sure we can access the cluster
|
||||
kubectl --user=oidc cluster-info
|
||||
# switch the current context
|
||||
kubectl config set-context --current --user=oidc
|
||||
# make sure we can access the cluster
|
||||
kubectl cluster-info
|
||||
|
||||
.PHONY: setup
|
||||
setup: build dex cluster setup-chrome
|
||||
|
||||
.PHONY: setup-chrome
|
||||
setup-chrome: $(OUTPUT_DIR)/ca.crt
|
||||
# add the dex server certificate to the trust store
|
||||
mkdir -p ~/.pki/nssdb
|
||||
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
|
||||
|
||||
# build binaries
|
||||
.PHONY: build
|
||||
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
|
||||
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
|
||||
go build -o $@ ..
|
||||
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
|
||||
go build -o $@ ./chromelogin
|
||||
|
||||
# create a Dex server
|
||||
.PHONY: dex
|
||||
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
|
||||
docker create --name dex-server -p 10443:10443 --network kind quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
|
||||
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
|
||||
docker cp $(OUTPUT_DIR)/server.key dex-server:/
|
||||
docker cp dex.yaml dex-server:/
|
||||
docker start dex-server
|
||||
docker logs dex-server
|
||||
|
||||
$(OUTPUT_DIR)/ca.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
|
||||
$(OUTPUT_DIR)/server.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
|
||||
|
||||
# create a Kubernetes cluster
|
||||
.PHONY: cluster
|
||||
cluster: dex create-cluster
|
||||
# add the Dex container IP to /etc/hosts of kube-apiserver
|
||||
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
|
||||
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
|
||||
# wait for kube-apiserver oidc initialization
|
||||
# (oidc authenticator will retry oidc discovery every 10s)
|
||||
sleep 10
|
||||
|
||||
.PHONY: create-cluster
|
||||
create-cluster: $(OUTPUT_DIR)/ca.crt
|
||||
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-system-test-dex-ca.crt
|
||||
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
|
||||
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
|
||||
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=admin@example.com
|
||||
|
||||
# clean up the resources
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -r $(OUTPUT_DIR)
|
||||
.PHONY: delete-cluster
|
||||
delete-cluster:
|
||||
kind delete cluster --name $(CLUSTER_NAME)
|
||||
.PHONY: delete-dex
|
||||
delete-dex:
|
||||
docker stop dex-server
|
||||
docker rm dex-server
|
||||
112
system_test/README.md
Normal file
112
system_test/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# kubelogin/system_test
|
||||
|
||||
This is an automated test for verifying behavior of the plugin with a real Kubernetes cluster and OIDC provider.
|
||||
|
||||
|
||||
## Purpose
|
||||
|
||||
This test checks the following points:
|
||||
|
||||
1. User can set up Kubernetes OIDC authentication using [setup guide](../docs/setup.md).
|
||||
1. User can log in to an OIDC provider on a browser.
|
||||
1. User can access the cluster using a token returned from the plugin.
|
||||
|
||||
It depends on the following components:
|
||||
|
||||
- Kubernetes cluster (Kind)
|
||||
- OIDC provider (Dex)
|
||||
- Browser (Chrome)
|
||||
- kubectl command
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
Let's take a look at the diagram.
|
||||
|
||||

|
||||
|
||||
It prepares the following resources:
|
||||
|
||||
1. Generate a pair of CA certificate and TLS server certificate for Dex.
|
||||
1. Run Dex on a container.
|
||||
1. Create a Kubernetes cluster using Kind.
|
||||
1. Mutate `/etc/hosts` of the CI machine to access Dex.
|
||||
1. Mutate `/etc/hosts` of the kube-apiserver pod to access Dex.
|
||||
|
||||
It performs the test by the following steps:
|
||||
|
||||
1. Run kubectl.
|
||||
1. kubectl automatically runs kubelogin.
|
||||
1. kubelogin automatically runs [chromelogin](chromelogin).
|
||||
1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
|
||||
1. kubelogin gets an authorization code from the browser.
|
||||
1. kubelogin gets a token.
|
||||
1. kubectl accesses an API with the token.
|
||||
1. kube-apiserver verifies the token by Dex.
|
||||
1. Check if kubectl exited with code 0.
|
||||
|
||||
|
||||
## Run locally
|
||||
|
||||
You need to set up the following components:
|
||||
|
||||
- Docker
|
||||
- Kind
|
||||
- Chrome or Chromium
|
||||
|
||||
You need to add the following line to `/etc/hosts` so that the browser can access the Dex.
|
||||
|
||||
```
|
||||
127.0.0.1 dex-server
|
||||
```
|
||||
|
||||
Run the test.
|
||||
|
||||
```shell script
|
||||
# run the test
|
||||
make
|
||||
|
||||
# clean up
|
||||
make delete-cluster
|
||||
make delete-dex
|
||||
```
|
||||
|
||||
|
||||
## Technical consideration
|
||||
|
||||
### Network and DNS
|
||||
|
||||
Consider the following issues:
|
||||
|
||||
- kube-apiserver runs on the host network of the kind container.
|
||||
- kube-apiserver cannot resolve a service name by kube-dns.
|
||||
- kube-apiserver cannot access a cluster IP.
|
||||
- kube-apiserver can access another container via the Docker network.
|
||||
- Chrome requires exactly match of domain name between Dex URL and a server certificate.
|
||||
|
||||
Consequently,
|
||||
|
||||
- kube-apiserver accesses Dex by resolving `/etc/hosts` and via the Docker network.
|
||||
- kubelogin and Chrome accesses Dex by resolving `/etc/hosts` and via the Docker network.
|
||||
|
||||
### TLS server certificate
|
||||
|
||||
Consider the following issues:
|
||||
|
||||
- kube-apiserver requires `--oidc-issuer` is HTTPS URL.
|
||||
- kube-apiserver requires a CA certificate at startup, if `--oidc-ca-file` is given.
|
||||
- kube-apiserver mounts `/usr/local/share/ca-certificates` from the kind container.
|
||||
- It is possible to mount a file from the CI machine.
|
||||
- It is not possible to issue a certificate using Let's Encrypt in runtime.
|
||||
- Chrome requires a valid certificate in `~/.pki/nssdb`.
|
||||
|
||||
As a result,
|
||||
|
||||
- kube-apiserver uses the CA certificate of `/usr/local/share/ca-certificates/dex-ca.crt`. See the `extraMounts` section of [`cluster.yaml`](cluster.yaml).
|
||||
- kubelogin uses the CA certificate in `output/ca.crt`.
|
||||
- Chrome uses the CA certificate in `~/.pki/nssdb`.
|
||||
|
||||
### Test environment
|
||||
|
||||
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
|
||||
- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.
|
||||
20
system_test/cluster.yaml
Normal file
20
system_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-system-test-dex-ca.crt
|
||||
containerPath: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
Reference in New Issue
Block a user