mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-28 16:00:19 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6726d851cb | ||
|
|
21e03dc294 | ||
|
|
5f1ed82a85 | ||
|
|
abb1a564f4 | ||
|
|
6d4eee5d1d | ||
|
|
4c10146639 | ||
|
|
3121e55498 | ||
|
|
a2a6ea229d | ||
|
|
e7819f15eb | ||
|
|
6099a60aad | ||
|
|
e31ad59e63 | ||
|
|
355d9cf224 | ||
|
|
fb5cfcf18f | ||
|
|
31fadd2569 | ||
|
|
9f55437307 | ||
|
|
aa1f445672 | ||
|
|
0c160f9db2 | ||
|
|
8c7903b2db | ||
|
|
898e8a12de | ||
|
|
606f1cd0b6 | ||
|
|
562b998ca7 | ||
|
|
6c9d198ef5 | ||
|
|
5ebecc534e | ||
|
|
ca273c358d | ||
|
|
ccc6b772db | ||
|
|
1681d84fae | ||
|
|
6f62b25c40 | ||
|
|
71a7467e64 | ||
|
|
5c78b7823b | ||
|
|
361c376c95 | ||
|
|
c66570c030 | ||
|
|
afb25f511c | ||
|
|
a836ef0e92 |
34
.github/workflows/acceptance-test.yaml
vendored
Normal file
34
.github/workflows/acceptance-test.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: acceptance-test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/acceptance-test.yaml
|
||||
- acceptance_test/**
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/acceptance-test.yaml
|
||||
- acceptance_test/**
|
||||
|
||||
jobs:
|
||||
test-makefile:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: go.sum
|
||||
- run: make -C acceptance_test check
|
||||
- run: make -C acceptance_test
|
||||
env:
|
||||
OIDC_ISSUER_URL: https://accounts.google.com
|
||||
OIDC_CLIENT_ID: REDACTED.apps.googleusercontent.com
|
||||
YOUR_EMAIL: REDACTED@gmail.com
|
||||
- run: make -C acceptance_test delete-cluster
|
||||
- run: make -C acceptance_test clean
|
||||
7
.github/workflows/docker.yaml
vendored
7
.github/workflows/docker.yaml
vendored
@@ -44,12 +44,12 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
image: ghcr.io/${{ github.repository }}/cache
|
||||
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
|
||||
- uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
|
||||
- uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||
- uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
- uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||
id: build
|
||||
with:
|
||||
push: true
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
cache-from: ${{ steps.cache.outputs.cache-from }}
|
||||
@@ -60,6 +60,7 @@ jobs:
|
||||
linux/ppc64le
|
||||
|
||||
test:
|
||||
if: github.event_name == 'push'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
10
.github/workflows/go.yaml
vendored
10
.github/workflows/go.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: go.sum
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: go.sum
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: tools/go.mod
|
||||
cache-dependency-path: tools/go.sum
|
||||
@@ -70,10 +70,10 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: tools/go.mod
|
||||
cache-dependency-path: tools/go.sum
|
||||
- run: go mod tidy
|
||||
- run: make generate
|
||||
- uses: int128/update-generated-files-action@7eb71af1ae8e30d970ea5512d23fd2f4b0eae44c # v2.56.0
|
||||
- uses: int128/update-generated-files-action@65b9a7ae3ededc5679d78343f58fbebcf1ebd785 # v2.57.0
|
||||
|
||||
5
.github/workflows/release.yaml
vendored
5
.github/workflows/release.yaml
vendored
@@ -47,6 +47,9 @@ jobs:
|
||||
- runs-on: windows-latest
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
- runs-on: windows-latest
|
||||
GOOS: windows
|
||||
GOARCH: arm64
|
||||
runs-on: ${{ matrix.platform.runs-on }}
|
||||
env:
|
||||
GOOS: ${{ matrix.platform.GOOS }}
|
||||
@@ -55,7 +58,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
11
.github/workflows/system-test.yaml
vendored
11
.github/workflows/system-test.yaml
vendored
@@ -23,16 +23,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
|
||||
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
# for certutil
|
||||
- run: sudo apt-get update
|
||||
# Install certutil.
|
||||
# https://packages.ubuntu.com/xenial/libnss3-tools
|
||||
- run: sudo apt update
|
||||
- run: sudo apt install -y libnss3-tools
|
||||
- run: mkdir -p ~/.pki/nssdb
|
||||
# Install keyring related packages.
|
||||
# https://github.com/zalando/go-keyring/issues/45
|
||||
- run: sudo apt-get install --no-install-recommends -y libnss3-tools dbus-x11 gnome-keyring
|
||||
|
||||
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
|
||||
|
||||
|
||||
@@ -60,3 +60,9 @@ spec:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
- bin: kubelogin.exe
|
||||
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_windows_arm64.zip" .TagName }}
|
||||
selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: arm64
|
||||
99
README.md
99
README.md
@@ -13,7 +13,6 @@ Take a look at the diagram:
|
||||
|
||||

|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setup
|
||||
@@ -31,28 +30,28 @@ kubectl krew install oidc-login
|
||||
choco install kubelogin
|
||||
```
|
||||
|
||||
If you install via GitHub releases, you need to put the `kubelogin` binary on your path under the name `kubectl-oidc_login` so that the [kubectl plugin mechanism](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/) can find it when you invoke `kubectl oidc-login`. The other install methods do this for you.
|
||||
If you install via GitHub releases, save the binary as the name `kubectl-oidc_login` on your path.
|
||||
When you invoke `kubectl oidc-login`, kubectl finds it by the [naming convention of kubectl plugins](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/).
|
||||
The other install methods do this for you.
|
||||
|
||||
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
|
||||
The kubeconfig looks like:
|
||||
Your kubeconfig looks like this:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
See [setup guide](docs/setup.md) for more.
|
||||
|
||||
See the [setup guide](docs/setup.md) for more.
|
||||
|
||||
### Run
|
||||
|
||||
@@ -65,33 +64,46 @@ kubectl get pods
|
||||
Kubectl executes kubelogin before calling the Kubernetes APIs.
|
||||
Kubelogin automatically opens the browser, and you can log in to the provider.
|
||||
|
||||
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
|
||||
After the authentication, kubelogin returns the credentials to kubectl.
|
||||
Kubectl then calls the Kubernetes APIs with the credentials.
|
||||
|
||||
After authentication, kubelogin returns the credentials to kubectl and kubectl then calls the Kubernetes APIs with these credentials.
|
||||
|
||||
```
|
||||
```console
|
||||
% kubectl get pods
|
||||
Open http://localhost:8000 for authentication
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
Kubelogin writes the ID token and refresh token to the token cache file.
|
||||
Kubelogin stores the ID token and refresh token to the cache.
|
||||
If the ID token is valid, it just returns it.
|
||||
If the ID token has expired, it will refresh the token using the refresh token.
|
||||
If the refresh token has expired, it will perform re-authentication.
|
||||
|
||||
If the cached ID token is valid, kubelogin just returns it.
|
||||
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
|
||||
If the refresh token has expired, kubelogin will perform re-authentication (you will have to login via browser again).
|
||||
## Troubleshooting
|
||||
|
||||
### Token cache
|
||||
|
||||
### Troubleshoot
|
||||
If the OS keyring is available, kubelogin stores the token cache to the OS keyring.
|
||||
Otherwise, kubelogin stores the token cache to the file system.
|
||||
See the [token cache](docs/usage.md#token-cache) for details.
|
||||
|
||||
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
|
||||
Kubelogin will ask you to login via browser again if the token cache file does not exist i.e., it starts with a clean slate
|
||||
|
||||
You can dump claims of an ID token by `setup` command.
|
||||
You can log out by deleting the token cache.
|
||||
|
||||
```console
|
||||
% kubectl oidc-login setup --oidc-issuer-url https://accounts.google.com --oidc-client-id REDACTED --oidc-client-secret REDACTED
|
||||
% kubectl oidc-login clean
|
||||
Deleted the token cache at /home/user/.kube/cache/oidc-login
|
||||
Deleted the token cache in the keyring
|
||||
```
|
||||
|
||||
Kubelogin will ask you to log in via the browser again.
|
||||
If the browser has a cookie for the provider, you need to log out from the provider or clear the cookie.
|
||||
|
||||
### ID token claims
|
||||
|
||||
You can run `setup` command to dump the claims of an ID token from the provider.
|
||||
|
||||
```console
|
||||
% kubectl oidc-login setup --oidc-issuer-url=ISSUER_URL --oidc-client-id=REDACTED
|
||||
...
|
||||
You got a token with the following claims:
|
||||
|
||||
@@ -103,23 +115,22 @@ You got a token with the following claims:
|
||||
}
|
||||
```
|
||||
|
||||
You can increase the log level by `-v1` option.
|
||||
You can set `-v1` option to increase the log level.
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- -v1
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- -v1
|
||||
```
|
||||
|
||||
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
|
||||
|
||||
You can run the [acceptance test](acceptance_test) to verify if kubelogin works with your provider.
|
||||
|
||||
## Docs
|
||||
|
||||
@@ -129,11 +140,7 @@ You can verify kubelogin works with your provider using [acceptance test](accept
|
||||
- [System test](system_test)
|
||||
- [Acceptance_test for identity providers](acceptance_test)
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
This is an open source software licensed under Apache License 2.0.
|
||||
Feel free to open issues and pull requests for improving code and documents.
|
||||
|
||||
This software is developed with [GoLand](https://www.jetbrains.com/go/) licensed for open source development.
|
||||
Special thanks for the support.
|
||||
|
||||
@@ -4,33 +4,38 @@ OUTPUT_DIR := $(CURDIR)/output
|
||||
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
|
||||
export KUBECONFIG
|
||||
|
||||
# create a Kubernetes cluster
|
||||
.PHONY: cluster
|
||||
cluster:
|
||||
# create a 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
|
||||
|
||||
# Set up the 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
|
||||
|
||||
# Set up kubectl.
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-api-version=client.authentication.k8s.io/v1 \
|
||||
--exec-interactive-mode=Never \
|
||||
--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
|
||||
|
||||
# Switch the default user.
|
||||
kubectl config set-context --current --user=oidc
|
||||
|
||||
# clean up the resources
|
||||
# Show the kubeconfig.
|
||||
kubectl config view
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -r $(OUTPUT_DIR)
|
||||
|
||||
.PHONY: delete-cluster
|
||||
delete-cluster:
|
||||
kind delete cluster --name $(CLUSTER_NAME)
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# kubelogin/acceptance_test
|
||||
|
||||
This is a manual test for verifying Kubernetes OIDC authentication with your OIDC provider.
|
||||
|
||||
This is a manual test to verify if the Kubernetes OIDC authentication works with your OIDC provider.
|
||||
|
||||
## Purpose
|
||||
|
||||
This test checks the following points:
|
||||
|
||||
1. You can set up your OIDC provider using [setup guide](../docs/setup.md).
|
||||
1. You can set up your OIDC provider using the [setup guide](../docs/setup.md).
|
||||
1. The plugin works with your OIDC provider.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisite
|
||||
@@ -22,7 +20,7 @@ make -C ..
|
||||
```
|
||||
|
||||
You need to set up your provider.
|
||||
See [setup guide](../docs/setup.md) for more.
|
||||
See the [setup guide](../docs/setup.md) for more.
|
||||
|
||||
You need to install the following tools:
|
||||
|
||||
@@ -44,7 +42,6 @@ For example, you can create a cluster with Google account authentication.
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 278 KiB |
121
docs/setup.md
121
docs/setup.md
@@ -10,9 +10,16 @@ Let's see the following steps:
|
||||
1. Set up the kubeconfig
|
||||
1. Verify cluster access
|
||||
|
||||
|
||||
## 1. Set up the OIDC provider
|
||||
|
||||
Kubelogin supports the following authentication flows:
|
||||
|
||||
- Authorization code flow
|
||||
- Device authorization grant
|
||||
- Resource owner password credentials grant
|
||||
|
||||
See the [usage](usage.md) for the details.
|
||||
|
||||
### Google Identity Platform
|
||||
|
||||
You can log in with a Google account.
|
||||
@@ -24,11 +31,10 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
|
||||
Check the client ID and secret.
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://accounts.google.com`
|
||||
`YOUR_CLIENT_ID` | `xxx.apps.googleusercontent.com`
|
||||
`YOUR_CLIENT_SECRET` | random string
|
||||
| Variable | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| `ISSUER_URL` | `https://accounts.google.com` |
|
||||
| `YOUR_CLIENT_ID` | `xxx.apps.googleusercontent.com` |
|
||||
|
||||
### Keycloak
|
||||
|
||||
@@ -39,8 +45,8 @@ Open Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Client ID: `YOUR_CLIENT_ID`
|
||||
- Valid Redirect URLs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
|
||||
You can associate client roles by adding the following mapper:
|
||||
@@ -56,11 +62,10 @@ For example, if you have `admin` role of the client, you will get a JWT with the
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
`YOUR_CLIENT_ID` | `YOUR_CLIENT_ID`
|
||||
`YOUR_CLIENT_SECRET` | random string
|
||||
| Variable | Value |
|
||||
| ---------------- | ----------------------------------------------------- |
|
||||
| `ISSUER_URL` | `https://keycloak.example.com/auth/realms/YOUR_REALM` |
|
||||
| `YOUR_CLIENT_ID` | `YOUR_CLIENT_ID` |
|
||||
|
||||
### Dex with GitHub
|
||||
|
||||
@@ -77,29 +82,29 @@ Deploy [Dex](https://github.com/dexidp/dex) with the following config:
|
||||
```yaml
|
||||
issuer: https://dex.example.com
|
||||
connectors:
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: YOUR_GITHUB_CLIENT_ID
|
||||
clientSecret: YOUR_GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://dex.example.com/callback
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: YOUR_GITHUB_CLIENT_ID
|
||||
clientSecret: YOUR_GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://dex.example.com/callback
|
||||
staticClients:
|
||||
- id: YOUR_CLIENT_ID
|
||||
name: Kubernetes
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
secret: YOUR_DEX_CLIENT_SECRET
|
||||
- id: YOUR_CLIENT_ID
|
||||
name: Kubernetes
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
secret: YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://dex.example.com`
|
||||
`YOUR_CLIENT_ID` | `YOUR_CLIENT_ID`
|
||||
`YOUR_CLIENT_SECRET` | `YOUR_DEX_CLIENT_SECRET`
|
||||
| Variable | Value |
|
||||
| -------------------- | ------------------------- |
|
||||
| `ISSUER_URL` | `https://dex.example.com` |
|
||||
| `YOUR_CLIENT_ID` | `YOUR_CLIENT_ID` |
|
||||
| `YOUR_CLIENT_SECRET` | `YOUR_DEX_CLIENT_SECRET` |
|
||||
|
||||
### Okta
|
||||
|
||||
@@ -112,17 +117,17 @@ Open your Okta organization and create an application with the following options
|
||||
- Application type: Native
|
||||
- Initiate login URI: `http://localhost:8000`
|
||||
- Login redirect URIs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- Allowed grant types: Authorization Code
|
||||
- Client authentication: Use PKCE (for public clients)
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://YOUR_ORGANIZATION.okta.com`
|
||||
`YOUR_CLIENT_ID` | random string
|
||||
| Variable | Value |
|
||||
| ---------------- | ------------------------------------ |
|
||||
| `ISSUER_URL` | `https://YOUR_ORGANIZATION.okta.com` |
|
||||
| `YOUR_CLIENT_ID` | random string |
|
||||
|
||||
You do not need to set `YOUR_CLIENT_SECRET`.
|
||||
|
||||
@@ -135,17 +140,17 @@ Login with an account that has permissions to create applications.
|
||||
Create an OIDC application with the following configuration:
|
||||
|
||||
- Redirect URIs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- Grant type: Authorization Code
|
||||
- PKCE Enforcement: Required
|
||||
|
||||
Leverage the following variables in the next steps.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://auth.pingone.com/<PingOne Tenant Id>/as`
|
||||
`YOUR_CLIENT_ID` | random string
|
||||
| Variable | Value |
|
||||
| ---------------- | ------------------------------------------------- |
|
||||
| `ISSUER_URL` | `https://auth.pingone.com/<PingOne Tenant Id>/as` |
|
||||
| `YOUR_CLIENT_ID` | random string |
|
||||
|
||||
`YOUR_CLIENT_SECRET` is not required for this configuration.
|
||||
|
||||
@@ -156,8 +161,7 @@ Run the following command:
|
||||
```sh
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=ISSUER_URL \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
It launches the browser and navigates to `http://localhost:8000`.
|
||||
@@ -170,7 +174,6 @@ See also the full options.
|
||||
kubectl oidc-login setup --help
|
||||
```
|
||||
|
||||
|
||||
## 3. Bind a cluster role
|
||||
|
||||
Here bind `cluster-admin` role to you.
|
||||
@@ -181,7 +184,6 @@ kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin
|
||||
|
||||
As well as you can create a custom cluster role and bind it.
|
||||
|
||||
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following flags to kube-apiserver:
|
||||
@@ -193,41 +195,20 @@ Add the following flags to kube-apiserver:
|
||||
|
||||
See [Kubernetes Authenticating: OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for the all flags.
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: ISSUER_URL
|
||||
oidcClientID: YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
If you are using [kube-aws](https://github.com/kubernetes-incubator/kube-aws), append the following settings to the `cluster.yaml`:
|
||||
|
||||
```yaml
|
||||
oidc:
|
||||
enabled: true
|
||||
issuerUrl: ISSUER_URL
|
||||
clientId: YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add `oidc` user to the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-api-version=client.authentication.k8s.io/v1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=ISSUER_URL \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Kubelogin supports the standalone mode as well.
|
||||
It writes the token to the kubeconfig (typically `~/.kube/config`) after authentication.
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
Configure your kubeconfig like:
|
||||
@@ -53,16 +52,16 @@ Your kubeconfig looks like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: YOUR_CLIENT_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: https://issuer.example.com
|
||||
id-token: ey... # kubelogin will add or update the ID token here
|
||||
refresh-token: ey... # kubelogin will add or update the refresh token here
|
||||
name: oidc
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: YOUR_CLIENT_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: https://issuer.example.com
|
||||
id-token: ey... # kubelogin will add or update the ID token here
|
||||
refresh-token: ey... # kubelogin will add or update the refresh token here
|
||||
name: oidc
|
||||
```
|
||||
|
||||
If the ID token is valid, kubelogin does nothing.
|
||||
@@ -75,7 +74,6 @@ You already have a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
|
||||
@@ -94,15 +92,15 @@ If you set multiple files, kubelogin will find the file which has the current au
|
||||
Kubelogin supports the following keys of `auth-provider` in a kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
|
||||
`client-id` | Read (Mandatory) | Client ID of the provider.
|
||||
`client-secret` | Read (Mandatory) | Client Secret of the provider.
|
||||
`idp-certificate-authority` | Read | CA certificate path of the provider.
|
||||
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
|
||||
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
|
||||
`id-token` | Write | ID token got from the provider.
|
||||
`refresh-token` | Write | Refresh token got from the provider.
|
||||
| Key | Direction | Value |
|
||||
| -------------------------------- | ---------------- | ---------------------------------------------------- |
|
||||
| `idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider. |
|
||||
| `client-id` | Read (Mandatory) | Client ID of the provider. |
|
||||
| `client-secret` | Read (Mandatory) | Client Secret of the provider. |
|
||||
| `idp-certificate-authority` | Read | CA certificate path of the provider. |
|
||||
| `idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider. |
|
||||
| `extra-scopes` | Read | Scopes to request to the provider (comma separated). |
|
||||
| `id-token` | Write | ID token got from the provider. |
|
||||
| `refresh-token` | Write | Refresh token got from the provider. |
|
||||
|
||||
See also [usage.md](usage.md).
|
||||
|
||||
160
docs/usage.md
160
docs/usage.md
@@ -11,15 +11,16 @@ 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
|
||||
--oidc-use-pkce Force PKCE usage
|
||||
--oidc-use-access-token Instead of using the id_token, use the access_token to authenticate to Kubernetes
|
||||
--token-cache-dir string Path to a directory for token cache (default "~/.kube/cache/oidc-login")
|
||||
--force-refresh If set, refresh the ID token regardless of its expiration time
|
||||
--token-cache-dir string Path to a directory of the token cache (default "~/.kube/cache/oidc-login")
|
||||
--token-cache-storage string Storage for the token cache. One of (auto|keyring|disk) (default "auto")
|
||||
--certificate-authority stringArray Path to a cert file for the certificate authority
|
||||
--certificate-authority-data stringArray 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
|
||||
--insecure-skip-tls-verify [SECURITY RISK] If set, the server's certificate will not be checked for validity
|
||||
--tls-renegotiation-once If set, allow a remote server to request renegotiation once per connection
|
||||
--tls-renegotiation-freely If set, allow a remote server to repeatedly request renegotiation
|
||||
--oidc-pkce-method string PKCE code challenge method. Automatically determined by default. One of (auto|no|S256) (default "auto")
|
||||
--grant-type string Authorization grant type to use. One of (auto|authcode|authcode-keyboard|password|device-code) (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
|
||||
@@ -51,7 +52,6 @@ Global Flags:
|
||||
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
### Authentication timeout
|
||||
@@ -61,7 +61,7 @@ This prevents a process from remaining forever.
|
||||
You can change the timeout by the following flag:
|
||||
|
||||
```yaml
|
||||
- --authentication-timeout-sec=60
|
||||
- --authentication-timeout-sec=60
|
||||
```
|
||||
|
||||
For now this timeout works only for the authorization code flow.
|
||||
@@ -71,17 +71,31 @@ For now this timeout works only for the authorization code flow.
|
||||
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
|
||||
|
||||
```yaml
|
||||
- --oidc-extra-scope=email
|
||||
- --oidc-extra-scope=profile
|
||||
- --oidc-extra-scope=email
|
||||
- --oidc-extra-scope=profile
|
||||
```
|
||||
|
||||
### PKCE
|
||||
|
||||
Kubelogin automatically uses the PKCE if the provider supports it.
|
||||
It determines the code challenge method by the `code_challenge_methods_supported` claim of the OpenID Connect Discovery document.
|
||||
|
||||
If your provider does not return a valid `code_challenge_methods_supported` claim,
|
||||
you can enforce the code challenge method by `--oidc-pkce-method`.
|
||||
|
||||
```yaml
|
||||
- --oidc-pkce-method=S256
|
||||
```
|
||||
|
||||
For the most providers, you don't need to set this option explicitly.
|
||||
|
||||
### CA certificate
|
||||
|
||||
You can use your self-signed certificate for the provider.
|
||||
|
||||
```yaml
|
||||
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
|
||||
- --certificate-authority-data=LS0t...
|
||||
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
|
||||
- --certificate-authority-data=LS0t...
|
||||
```
|
||||
|
||||
### HTTP proxy
|
||||
@@ -89,6 +103,20 @@ You can use your self-signed certificate for the provider.
|
||||
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
|
||||
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
|
||||
|
||||
### Token cache
|
||||
|
||||
Kubelogin stores the token cache to the OS keyring if available.
|
||||
It depends on [zalando/go-keyring](https://github.com/zalando/go-keyring) for the keyring storage.
|
||||
|
||||
If you encounter a problem, try `--token-cache-storage` to set the storage.
|
||||
|
||||
```yaml
|
||||
# Force to use the OS keyring
|
||||
- --token-cache-storage=keyring
|
||||
# Force to use the file system
|
||||
- --token-cache-storage=disk
|
||||
```
|
||||
|
||||
### Home directory expansion
|
||||
|
||||
If a value in the following options begins with a tilde character `~`, it is expanded to the home directory.
|
||||
@@ -98,18 +126,18 @@ If a value in the following options begins with a tilde character `~`, it is exp
|
||||
- `--local-server-key`
|
||||
- `--token-cache-dir`
|
||||
|
||||
|
||||
## Authentication flows
|
||||
|
||||
Kubelogin support the following flows:
|
||||
|
||||
- Authorization code flow
|
||||
- Authorization code flow with a keyboard
|
||||
- Resource owner password credentials grant flow
|
||||
- [Authorization code flow](#authorization-code-flow)
|
||||
- [Authorization code flow with a keyboard](#authorization-code-flow-with-a-keyboard)
|
||||
- [Device authorization grant](#device-authorization-grant)
|
||||
- [Resource owner password credentials grant](#resource-owner-password-credentials-grant)
|
||||
|
||||
### Authorization code flow
|
||||
|
||||
Kubelogin performs the authorization code flow by default.
|
||||
Kubelogin performs the [authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) by default.
|
||||
|
||||
It starts the local server at port 8000 or 18000 by default.
|
||||
You need to register the following redirect URIs to the provider:
|
||||
@@ -120,50 +148,51 @@ You need to register the following redirect URIs to the provider:
|
||||
You can change the listening address.
|
||||
|
||||
```yaml
|
||||
- --listen-address=127.0.0.1:12345
|
||||
- --listen-address=127.0.0.1:23456
|
||||
- --listen-address=127.0.0.1:12345
|
||||
- --listen-address=127.0.0.1:23456
|
||||
```
|
||||
|
||||
You can specify a certificate for the local webserver if HTTPS is required by your identity provider.
|
||||
|
||||
```yaml
|
||||
- --local-server-cert=localhost.crt
|
||||
- --local-server-key=localhost.key
|
||||
- --local-server-cert=localhost.crt
|
||||
- --local-server-key=localhost.key
|
||||
```
|
||||
|
||||
You can change the hostname of redirect URI from the default value `localhost`.
|
||||
|
||||
```yaml
|
||||
- --oidc-redirect-url-hostname=127.0.0.1
|
||||
- --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
|
||||
- --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
|
||||
- --open-url-after-authentication=https://example.com/success.html
|
||||
```
|
||||
|
||||
You can skip opening the browser if you encounter some environment problem.
|
||||
If you encounter a problem with the browser, you can change the browser command or skip opening the browser.
|
||||
|
||||
```yaml
|
||||
- --skip-open-browser
|
||||
# Change the browser command
|
||||
- --browser-command=google-chrome
|
||||
# Do not open the browser
|
||||
- --skip-open-browser
|
||||
```
|
||||
|
||||
For Linux users, you change the default browser by `BROWSER` environment variable.
|
||||
|
||||
### Authorization code flow with a keyboard
|
||||
|
||||
If you cannot access the browser, instead use the authorization code flow with a keyboard.
|
||||
|
||||
```yaml
|
||||
- --grant-type=authcode-keyboard
|
||||
- --grant-type=authcode-keyboard
|
||||
```
|
||||
|
||||
Kubelogin will show the URL and prompt.
|
||||
@@ -179,34 +208,55 @@ The default of redirect URI is `urn:ietf:wg:oauth:2.0:oob`.
|
||||
You can overwrite it.
|
||||
|
||||
```yaml
|
||||
- oidc-redirect-url-authcode-keyboard=http://localhost
|
||||
- oidc-redirect-url-authcode-keyboard=http://localhost
|
||||
```
|
||||
|
||||
You can add extra parameters to the authentication request.
|
||||
|
||||
```yaml
|
||||
- --oidc-auth-request-extra-params=ttl=86400
|
||||
- --oidc-auth-request-extra-params=ttl=86400
|
||||
```
|
||||
|
||||
### Resource owner password credentials grant flow
|
||||
### Device authorization grant
|
||||
|
||||
Kubelogin performs the resource owner password credentials grant flow
|
||||
Kubelogin performs the [device authorization grant](https://tools.ietf.org/html/rfc8628) when `--grant-type=device-code` is set.
|
||||
|
||||
```yaml
|
||||
- --grant-type=device-code
|
||||
```
|
||||
|
||||
It automatically opens the browser.
|
||||
If the provider returns the `verification_uri_complete` parameter, you don't need to enter the code.
|
||||
Otherwise, you need to enter the code shown.
|
||||
|
||||
If you encounter a problem with the browser, you can change the browser command or skip opening the browser.
|
||||
|
||||
```yaml
|
||||
# Change the browser command
|
||||
- --browser-command=google-chrome
|
||||
# Do not open the browser
|
||||
- --skip-open-browser
|
||||
```
|
||||
|
||||
### Resource owner password credentials grant
|
||||
|
||||
Kubelogin performs the resource owner password credentials grant
|
||||
when `--grant-type=password` or `--username` is set.
|
||||
|
||||
Note that most OIDC providers do not support this flow.
|
||||
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
|
||||
Note that most OIDC providers do not support this grant.
|
||||
Keycloak supports this grant but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
|
||||
|
||||
You can set the username and password.
|
||||
|
||||
```yaml
|
||||
- --username=USERNAME
|
||||
- --password=PASSWORD
|
||||
- --username=USERNAME
|
||||
- --password=PASSWORD
|
||||
```
|
||||
|
||||
If the password is not set, kubelogin will show the prompt for the password.
|
||||
|
||||
```yaml
|
||||
- --username=USERNAME
|
||||
- --username=USERNAME
|
||||
```
|
||||
|
||||
```
|
||||
@@ -217,7 +267,7 @@ Password:
|
||||
If the username is not set, kubelogin will show the prompt for the username and password.
|
||||
|
||||
```yaml
|
||||
- --grant-type=password
|
||||
- --grant-type=password
|
||||
```
|
||||
|
||||
```
|
||||
@@ -233,25 +283,25 @@ The kubeconfig looks like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: docker
|
||||
args:
|
||||
- run
|
||||
- --rm
|
||||
- -v
|
||||
- /tmp/.token-cache:/.token-cache
|
||||
- -p
|
||||
- 8000:8000
|
||||
- ghcr.io/int128/kubelogin
|
||||
- get-token
|
||||
- --token-cache-dir=/.token-cache
|
||||
- --listen-address=0.0.0.0:8000
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1
|
||||
command: docker
|
||||
args:
|
||||
- run
|
||||
- --rm
|
||||
- -v
|
||||
- /tmp/.token-cache:/.token-cache
|
||||
- -p
|
||||
- 8000:8000
|
||||
- ghcr.io/int128/kubelogin
|
||||
- get-token
|
||||
- --token-cache-dir=/.token-cache
|
||||
- --listen-address=0.0.0.0:8000
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Known limitations:
|
||||
|
||||
16
go.mod
16
go.mod
@@ -1,9 +1,9 @@
|
||||
module github.com/int128/kubelogin
|
||||
|
||||
go 1.23.4
|
||||
go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/chromedp/chromedp v0.12.1
|
||||
github.com/coreos/go-oidc/v3 v3.12.0
|
||||
github.com/gofrs/flock v0.12.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
@@ -15,18 +15,21 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/zalando/go-keyring v0.2.6
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/term v0.28.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.32.0
|
||||
k8s.io/client-go v0.32.0
|
||||
k8s.io/apimachinery v0.32.1
|
||||
k8s.io/client-go v0.32.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
|
||||
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250120090109-d38428e4d9c8 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
@@ -34,6 +37,7 @@ require (
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -41,7 +45,7 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
|
||||
34
go.sum
34
go.sum
@@ -1,14 +1,18 @@
|
||||
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
|
||||
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
|
||||
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
|
||||
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
|
||||
github.com/chromedp/cdproto v0.0.0-20250120090109-d38428e4d9c8 h1:Q2byC+xLgH/Z7hExJ8G/jVqsvCfGhMmNgM1ysZARA3o=
|
||||
github.com/chromedp/cdproto v0.0.0-20250120090109-d38428e4d9c8/go.mod h1:RTGuBeCeabAJGi3OZf71a6cGa7oYBfBP75VJZFLv6SU=
|
||||
github.com/chromedp/chromedp v0.12.1 h1:kBMblXk7xH5/6j3K9uk8d7/c+fzXWiUsCsPte0VMwOA=
|
||||
github.com/chromedp/chromedp v0.12.1/go.mod h1:F6+wdq9LKFDMoyxhq46ZLz4VLXrsrCAR3sFqJz4Nqc0=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
||||
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -33,6 +37,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -50,6 +56,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -75,8 +83,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
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=
|
||||
@@ -111,6 +119,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
|
||||
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -207,12 +217,12 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE=
|
||||
k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0=
|
||||
k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg=
|
||||
k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
||||
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
||||
k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
|
||||
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
|
||||
k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
|
||||
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
|
||||
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
|
||||
28
integration_test/clean_test.go
Normal file
28
integration_test/clean_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/integration_test/httpdriver"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/testing/clock"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
)
|
||||
|
||||
func TestClean(t *testing.T) {
|
||||
tokenCacheDir := t.TempDir()
|
||||
|
||||
cmd := di.NewCmdForHeadless(clock.Fake(time.Now()), os.Stdin, os.Stdout, logger.New(t), httpdriver.Zero(t))
|
||||
exitCode := cmd.Run(context.TODO(), []string{
|
||||
"kubelogin",
|
||||
"clean",
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--token-cache-storage", "disk",
|
||||
}, "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"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"
|
||||
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
@@ -43,42 +43,42 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
args: []string{"--certificate-authority", keypair.Server.CACertPath},
|
||||
},
|
||||
} {
|
||||
httpDriverOption := httpdriver.Option{
|
||||
httpDriverConfig := httpdriver.Config{
|
||||
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, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, tc.keyPair, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverConfig),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.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, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, tc.keyPair, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -86,13 +86,14 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
Password: "PASS1",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.Zero(t),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
@@ -101,104 +102,159 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
"--password", "PASS1",
|
||||
}, tc.args...),
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.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, testconfig.TestConfig{})
|
||||
svc := oidcserver.New(t, tc.keyPair, testconfig.Config{})
|
||||
|
||||
t.Run("NoCache", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{
|
||||
svc.SetConfig(testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverConfig),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{})
|
||||
svc.SetConfig(testconfig.Config{})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.Zero(t),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
t.Run("Refresh", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{
|
||||
svc.SetConfig(testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
RefreshToken: "REFRESH_TOKEN_1",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(3 * time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
IDTokenExpiry: now.Add(3 * time.Hour),
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverConfig),
|
||||
now: now.Add(2 * time.Hour),
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(3*time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(3*time.Hour))
|
||||
})
|
||||
t.Run("RefreshAgain", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{
|
||||
svc.SetConfig(testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
RefreshToken: "REFRESH_TOKEN_2",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(5 * time.Hour),
|
||||
IDTokenExpiry: now.Add(5 * time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpDriverConfig),
|
||||
now: now.Add(4 * time.Hour),
|
||||
stdout: &stdout,
|
||||
args: tc.args,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(5*time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(5*time.Hour))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("PKCE", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Not supported by provider", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: nil,
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("Enforce", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: nil,
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{"--oidc-use-pkce"},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("TLSData", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, keypair.Server, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -212,57 +268,34 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
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))
|
||||
})
|
||||
|
||||
t.Run("TLSData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.Server, testconfig.TestConfig{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
},
|
||||
})
|
||||
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"}),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{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))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "email profile openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Scope: "email profile openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{
|
||||
@@ -270,77 +303,80 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
"--oidc-extra-scope", "profile",
|
||||
},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("OpenURLAfterAuthentication", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
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"}),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{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))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("RedirectURLHostname", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://127.0.0.1:",
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://127.0.0.1:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{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))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("RedirectURLHTTPS", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "https://localhost:",
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "https://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{
|
||||
TLSConfig: keypair.Server.TLSConfig,
|
||||
BodyContains: "Authenticated",
|
||||
}),
|
||||
@@ -351,31 +387,32 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
"--local-server-key", keypair.Server.KeyPath,
|
||||
},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
|
||||
t.Run("ExtraParams", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
svc := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
CodeChallengeMethod: "S256",
|
||||
ExtraParams: map[string]string{
|
||||
"ttl": "86400",
|
||||
"reauth": "false",
|
||||
},
|
||||
},
|
||||
Response: testconfig.Response{
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
IDTokenExpiry: now.Add(time.Hour),
|
||||
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
runGetToken(t, ctx, getTokenConfig{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
|
||||
issuerURL: svc.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{BodyContains: "Authenticated"}),
|
||||
now: now,
|
||||
stdout: &stdout,
|
||||
args: []string{
|
||||
@@ -383,7 +420,7 @@ func TestCredentialPlugin(t *testing.T) {
|
||||
"--oidc-auth-request-extra-params", "reauth=false",
|
||||
},
|
||||
})
|
||||
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
assertCredentialPluginStdout(t, &stdout, svc.LastTokenResponse().IDToken, now.Add(time.Hour))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -398,10 +435,15 @@ type getTokenConfig struct {
|
||||
|
||||
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)
|
||||
t.Setenv(
|
||||
"KUBERNETES_EXEC_INFO",
|
||||
`{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1","spec":{"interactive":true}}`,
|
||||
)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin",
|
||||
"get-token",
|
||||
"--token-cache-dir", cfg.tokenCacheDir,
|
||||
"--token-cache-storage", "disk",
|
||||
"--oidc-issuer-url", cfg.issuerURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
@@ -412,22 +454,22 @@ func runGetToken(t *testing.T, ctx context.Context, cfg getTokenConfig) {
|
||||
}
|
||||
|
||||
func assertCredentialPluginStdout(t *testing.T, stdout io.Reader, token string, expiry time.Time) {
|
||||
var got clientauthenticationv1beta1.ExecCredential
|
||||
var got clientauthenticationv1.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{
|
||||
want := clientauthenticationv1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1",
|
||||
Kind: "ExecCredential",
|
||||
},
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
Status: &clientauthenticationv1.ExecCredentialStatus{
|
||||
Token: token,
|
||||
ExpirationTimestamp: &metav1.Time{Time: expiry},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
|
||||
t.Errorf("stdout mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
type Config 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}
|
||||
func New(ctx context.Context, t *testing.T, config Config) *client {
|
||||
return &client{ctx, t, config}
|
||||
}
|
||||
|
||||
// Zero returns a client which call is not expected.
|
||||
@@ -26,13 +26,13 @@ func Zero(t *testing.T) *zeroClient {
|
||||
}
|
||||
|
||||
type client struct {
|
||||
ctx context.Context
|
||||
t *testing.T
|
||||
o Option
|
||||
ctx context.Context
|
||||
t *testing.T
|
||||
config Config
|
||||
}
|
||||
|
||||
func (c *client) Open(url string) error {
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.o.TLSConfig}}
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.config.TLSConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
c.t.Errorf("could not create a request: %s", err)
|
||||
@@ -54,8 +54,8 @@ func (c *client) Open(url string) error {
|
||||
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)
|
||||
if !strings.Contains(body, c.config.BodyContains) {
|
||||
c.t.Errorf("body should contain %s but was %s", c.config.BodyContains, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/integration_test/oidcserver/service"
|
||||
@@ -28,10 +29,8 @@ type Handlers struct {
|
||||
}
|
||||
|
||||
func (h *Handlers) handleError(w http.ResponseWriter, r *http.Request, f func() error) {
|
||||
wr := &responseWriterRecorder{w, 200}
|
||||
err := f()
|
||||
if err == nil {
|
||||
h.t.Logf("%d %s %s", wr.statusCode, r.Method, r.RequestURI)
|
||||
return
|
||||
}
|
||||
if errResp := new(service.ErrorResponse); errors.As(err, &errResp) {
|
||||
@@ -48,16 +47,6 @@ func (h *Handlers) handleError(w http.ResponseWriter, r *http.Request, f func()
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
type responseWriterRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *responseWriterRecorder) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
func (h *Handlers) Discovery(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleError(w, r, func() error {
|
||||
discoveryResponse := h.provider.Discovery()
|
||||
@@ -98,8 +87,12 @@ func (h *Handlers) AuthenticateCode(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("authentication error: %w", err)
|
||||
}
|
||||
to := fmt.Sprintf("%s?state=%s&code=%s", redirectURI, state, code)
|
||||
http.Redirect(w, r, to, 302)
|
||||
redirectTo, err := url.Parse(redirectURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid redirect_uri: %w", err)
|
||||
}
|
||||
redirectTo.RawQuery = url.Values{"state": {state}, "code": {code}}.Encode()
|
||||
http.Redirect(w, r, redirectTo.String(), http.StatusFound)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,20 +17,20 @@ import (
|
||||
)
|
||||
|
||||
// New starts a server for the OpenID Connect provider.
|
||||
func New(t *testing.T, k keypair.KeyPair, c testconfig.TestConfig) service.Service {
|
||||
func New(t *testing.T, kp keypair.KeyPair, config testconfig.Config) service.Service {
|
||||
mux := http.NewServeMux()
|
||||
serverURL := startServer(t, mux, k)
|
||||
serverURL := startServer(t, mux, kp)
|
||||
|
||||
svc := service.New(t, serverURL, c)
|
||||
svc := service.New(t, serverURL, config)
|
||||
handler.Register(t, mux, svc)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, h http.Handler, k keypair.KeyPair) string {
|
||||
if k == keypair.None {
|
||||
sv := httptest.NewServer(h)
|
||||
t.Cleanup(sv.Close)
|
||||
return sv.URL
|
||||
func startServer(t *testing.T, h http.Handler, kp keypair.KeyPair) string {
|
||||
if kp == keypair.None {
|
||||
srv := httptest.NewServer(h)
|
||||
t.Cleanup(srv.Close)
|
||||
return srv.URL
|
||||
}
|
||||
|
||||
// Unfortunately, httptest package did not work with keypair.KeyPair.
|
||||
@@ -38,15 +38,15 @@ func startServer(t *testing.T, h http.Handler, k keypair.KeyPair) string {
|
||||
portAllocator := httptest.NewUnstartedServer(h)
|
||||
t.Cleanup(portAllocator.Close)
|
||||
serverURL := fmt.Sprintf("https://localhost:%d", portAllocator.Listener.Addr().(*net.TCPAddr).Port)
|
||||
sv := &http.Server{Handler: h}
|
||||
srv := &http.Server{Handler: h}
|
||||
go func() {
|
||||
err := sv.ServeTLS(portAllocator.Listener, k.CertPath, k.KeyPath)
|
||||
err := srv.ServeTLS(portAllocator.Listener, kp.CertPath, kp.KeyPath)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
if err := sv.Shutdown(context.TODO()); err != nil {
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
t.Errorf("could not shutdown the server: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/integration_test/oidcserver/testconfig"
|
||||
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
)
|
||||
|
||||
func New(t *testing.T, issuerURL string, config testconfig.TestConfig) Service {
|
||||
func New(t *testing.T, issuerURL string, config testconfig.Config) Service {
|
||||
return &service{
|
||||
config: config,
|
||||
t: t,
|
||||
@@ -23,7 +24,7 @@ func New(t *testing.T, issuerURL string, config testconfig.TestConfig) Service {
|
||||
}
|
||||
|
||||
type service struct {
|
||||
config testconfig.TestConfig
|
||||
config testconfig.Config
|
||||
t *testing.T
|
||||
issuerURL string
|
||||
lastAuthenticationRequest *AuthenticationRequest
|
||||
@@ -34,7 +35,7 @@ func (svc *service) IssuerURL() string {
|
||||
return svc.issuerURL
|
||||
}
|
||||
|
||||
func (svc *service) SetConfig(cfg testconfig.TestConfig) {
|
||||
func (svc *service) SetConfig(cfg testconfig.Config) {
|
||||
svc.config = cfg
|
||||
}
|
||||
|
||||
@@ -84,8 +85,8 @@ func (svc *service) AuthenticateCode(req AuthenticationRequest) (code string, er
|
||||
if !strings.HasPrefix(req.RedirectURI, svc.config.Want.RedirectURIPrefix) {
|
||||
svc.t.Errorf("redirectURI wants prefix `%s` but was `%s`", svc.config.Want.RedirectURIPrefix, req.RedirectURI)
|
||||
}
|
||||
if req.CodeChallengeMethod != svc.config.Want.CodeChallengeMethod {
|
||||
svc.t.Errorf("code_challenge_method wants `%s` but was `%s`", svc.config.Want.CodeChallengeMethod, req.CodeChallengeMethod)
|
||||
if diff := cmp.Diff(svc.config.Want.CodeChallengeMethod, req.CodeChallengeMethod); diff != "" {
|
||||
svc.t.Errorf("code_challenge_method mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for k, v := range svc.config.Want.ExtraParams {
|
||||
got := req.RawQuery.Get(k)
|
||||
|
||||
@@ -13,7 +13,7 @@ type Service interface {
|
||||
Provider
|
||||
|
||||
IssuerURL() string
|
||||
SetConfig(config testconfig.TestConfig)
|
||||
SetConfig(config testconfig.Config)
|
||||
LastTokenResponse() *TokenResponse
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import "time"
|
||||
type Want struct {
|
||||
Scope string
|
||||
RedirectURIPrefix string
|
||||
CodeChallengeMethod string // optional
|
||||
CodeChallengeMethod string
|
||||
ExtraParams map[string]string // optional
|
||||
Username string // optional
|
||||
Password string // optional
|
||||
@@ -17,12 +17,12 @@ type Want struct {
|
||||
type Response struct {
|
||||
IDTokenExpiry time.Time
|
||||
RefreshToken string
|
||||
RefreshError string // if set, Refresh() will return the error
|
||||
CodeChallengeMethodsSupported []string // optional
|
||||
RefreshError string // if set, Refresh() will return the error
|
||||
CodeChallengeMethodsSupported []string
|
||||
}
|
||||
|
||||
// TestConfig represents a configuration of the OpenID Connect provider.
|
||||
type TestConfig struct {
|
||||
// Config represents a configuration of the OpenID Connect provider.
|
||||
type Config struct {
|
||||
Want Want
|
||||
Response Response
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestStandalone(t *testing.T) {
|
||||
keyPair: keypair.Server,
|
||||
},
|
||||
} {
|
||||
httpDriverOption := httpdriver.Option{
|
||||
httpDriverOption := httpdriver.Config{
|
||||
TLSConfig: tc.keyPair.TLSConfig,
|
||||
BodyContains: "Authenticated",
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func TestStandalone(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, testconfig.TestConfig{
|
||||
sv := oidcserver.New(t, tc.keyPair, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -75,7 +75,7 @@ func TestStandalone(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, testconfig.TestConfig{
|
||||
sv := oidcserver.New(t, tc.keyPair, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -110,14 +110,14 @@ func TestStandalone(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, tc.keyPair, testconfig.TestConfig{})
|
||||
sv := oidcserver.New(t, tc.keyPair, testconfig.Config{})
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: sv.IssuerURL(),
|
||||
IDPCertificateAuthority: tc.keyPair.CACertPath,
|
||||
})
|
||||
|
||||
t.Run("NoToken", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{
|
||||
sv.SetConfig(testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -139,7 +139,7 @@ func TestStandalone(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{})
|
||||
sv.SetConfig(testconfig.Config{})
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
@@ -152,7 +152,7 @@ func TestStandalone(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("Refresh", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{
|
||||
sv.SetConfig(testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -175,7 +175,7 @@ func TestStandalone(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("RefreshAgain", func(t *testing.T) {
|
||||
sv.SetConfig(testconfig.TestConfig{
|
||||
sv.SetConfig(testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -204,7 +204,7 @@ func TestStandalone(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.Server, testconfig.TestConfig{
|
||||
sv := oidcserver.New(t, keypair.Server, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -220,7 +220,7 @@ func TestStandalone(t *testing.T) {
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig}),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{TLSConfig: keypair.Server.TLSConfig}),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
@@ -232,7 +232,7 @@ func TestStandalone(t *testing.T) {
|
||||
t.Run("env_KUBECONFIG", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -247,7 +247,7 @@ func TestStandalone(t *testing.T) {
|
||||
t.Setenv("KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{}),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
@@ -260,7 +260,7 @@ func TestStandalone(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.TestConfig{
|
||||
sv := oidcserver.New(t, keypair.None, testconfig.Config{
|
||||
Want: testconfig.Want{
|
||||
Scope: "profile groups openid",
|
||||
RedirectURIPrefix: "http://localhost:",
|
||||
@@ -276,7 +276,7 @@ func TestStandalone(t *testing.T) {
|
||||
runStandalone(t, ctx, standaloneConfig{
|
||||
issuerURL: sv.IssuerURL(),
|
||||
kubeConfigFilename: kubeConfigFilename,
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
|
||||
httpDriver: httpdriver.New(ctx, t, httpdriver.Config{}),
|
||||
now: now,
|
||||
})
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package service_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package service_mock
|
||||
|
||||
@@ -440,7 +440,7 @@ func (_c *MockService_Refresh_Call) RunAndReturn(run func(string) (*service.Toke
|
||||
}
|
||||
|
||||
// SetConfig provides a mock function with given fields: config
|
||||
func (_m *MockService) SetConfig(config testconfig.TestConfig) {
|
||||
func (_m *MockService) SetConfig(config testconfig.Config) {
|
||||
_m.Called(config)
|
||||
}
|
||||
|
||||
@@ -450,14 +450,14 @@ type MockService_SetConfig_Call struct {
|
||||
}
|
||||
|
||||
// SetConfig is a helper method to define mock.On call
|
||||
// - config testconfig.TestConfig
|
||||
// - config testconfig.Config
|
||||
func (_e *MockService_Expecter) SetConfig(config interface{}) *MockService_SetConfig_Call {
|
||||
return &MockService_SetConfig_Call{Call: _e.mock.On("SetConfig", config)}
|
||||
}
|
||||
|
||||
func (_c *MockService_SetConfig_Call) Run(run func(config testconfig.TestConfig)) *MockService_SetConfig_Call {
|
||||
func (_c *MockService_SetConfig_Call) Run(run func(config testconfig.Config)) *MockService_SetConfig_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(testconfig.TestConfig))
|
||||
run(args[0].(testconfig.Config))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -467,7 +467,7 @@ func (_c *MockService_SetConfig_Call) Return() *MockService_SetConfig_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_SetConfig_Call) RunAndReturn(run func(testconfig.TestConfig)) *MockService_SetConfig_Call {
|
||||
func (_c *MockService_SetConfig_Call) RunAndReturn(run func(testconfig.Config)) *MockService_SetConfig_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package cmd_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package reader_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package writer_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package browser_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package clock_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package logger_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package logger_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package logger_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package reader_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package stdio_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package stdio_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package jwt_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package loader_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package writer_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package client_mock
|
||||
|
||||
@@ -27,9 +27,9 @@ func (_m *MockFactoryInterface) EXPECT() *MockFactoryInterface_Expecter {
|
||||
return &MockFactoryInterface_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// New provides a mock function with given fields: ctx, p, tlsClientConfig
|
||||
func (_m *MockFactoryInterface) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (client.Interface, error) {
|
||||
ret := _m.Called(ctx, p, tlsClientConfig)
|
||||
// New provides a mock function with given fields: ctx, prov, tlsClientConfig
|
||||
func (_m *MockFactoryInterface) New(ctx context.Context, prov oidc.Provider, tlsClientConfig tlsclientconfig.Config) (client.Interface, error) {
|
||||
ret := _m.Called(ctx, prov, tlsClientConfig)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for New")
|
||||
@@ -38,10 +38,10 @@ func (_m *MockFactoryInterface) New(ctx context.Context, p oidc.Provider, tlsCli
|
||||
var r0 client.Interface
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, oidc.Provider, tlsclientconfig.Config) (client.Interface, error)); ok {
|
||||
return rf(ctx, p, tlsClientConfig)
|
||||
return rf(ctx, prov, tlsClientConfig)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, oidc.Provider, tlsclientconfig.Config) client.Interface); ok {
|
||||
r0 = rf(ctx, p, tlsClientConfig)
|
||||
r0 = rf(ctx, prov, tlsClientConfig)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(client.Interface)
|
||||
@@ -49,7 +49,7 @@ func (_m *MockFactoryInterface) New(ctx context.Context, p oidc.Provider, tlsCli
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, oidc.Provider, tlsclientconfig.Config) error); ok {
|
||||
r1 = rf(ctx, p, tlsClientConfig)
|
||||
r1 = rf(ctx, prov, tlsClientConfig)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -64,13 +64,13 @@ type MockFactoryInterface_New_Call struct {
|
||||
|
||||
// New is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - p oidc.Provider
|
||||
// - prov oidc.Provider
|
||||
// - tlsClientConfig tlsclientconfig.Config
|
||||
func (_e *MockFactoryInterface_Expecter) New(ctx interface{}, p interface{}, tlsClientConfig interface{}) *MockFactoryInterface_New_Call {
|
||||
return &MockFactoryInterface_New_Call{Call: _e.mock.On("New", ctx, p, tlsClientConfig)}
|
||||
func (_e *MockFactoryInterface_Expecter) New(ctx interface{}, prov interface{}, tlsClientConfig interface{}) *MockFactoryInterface_New_Call {
|
||||
return &MockFactoryInterface_New_Call{Call: _e.mock.On("New", ctx, prov, tlsClientConfig)}
|
||||
}
|
||||
|
||||
func (_c *MockFactoryInterface_New_Call) Run(run func(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config)) *MockFactoryInterface_New_Call {
|
||||
func (_c *MockFactoryInterface_New_Call) Run(run func(ctx context.Context, prov oidc.Provider, tlsClientConfig tlsclientconfig.Config)) *MockFactoryInterface_New_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(oidc.Provider), args[2].(tlsclientconfig.Config))
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package client_mock
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
oauth2dev "github.com/int128/oauth2dev"
|
||||
|
||||
oidc "github.com/int128/kubelogin/pkg/oidc"
|
||||
|
||||
pkce "github.com/int128/kubelogin/pkg/pkce"
|
||||
)
|
||||
|
||||
// MockInterface is an autogenerated mock type for the Interface type
|
||||
@@ -369,6 +371,51 @@ func (_c *MockInterface_GetTokenByROPC_Call) RunAndReturn(run func(context.Conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// NegotiatedPKCEMethod provides a mock function with no fields
|
||||
func (_m *MockInterface) NegotiatedPKCEMethod() pkce.Method {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NegotiatedPKCEMethod")
|
||||
}
|
||||
|
||||
var r0 pkce.Method
|
||||
if rf, ok := ret.Get(0).(func() pkce.Method); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(pkce.Method)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockInterface_NegotiatedPKCEMethod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NegotiatedPKCEMethod'
|
||||
type MockInterface_NegotiatedPKCEMethod_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// NegotiatedPKCEMethod is a helper method to define mock.On call
|
||||
func (_e *MockInterface_Expecter) NegotiatedPKCEMethod() *MockInterface_NegotiatedPKCEMethod_Call {
|
||||
return &MockInterface_NegotiatedPKCEMethod_Call{Call: _e.mock.On("NegotiatedPKCEMethod")}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_NegotiatedPKCEMethod_Call) Run(run func()) *MockInterface_NegotiatedPKCEMethod_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_NegotiatedPKCEMethod_Call) Return(_a0 pkce.Method) *MockInterface_NegotiatedPKCEMethod_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_NegotiatedPKCEMethod_Call) RunAndReturn(run func() pkce.Method) *MockInterface_NegotiatedPKCEMethod_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Refresh provides a mock function with given fields: ctx, refreshToken
|
||||
func (_m *MockInterface) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error) {
|
||||
ret := _m.Called(ctx, refreshToken)
|
||||
@@ -428,53 +475,6 @@ func (_c *MockInterface_Refresh_Call) RunAndReturn(run func(context.Context, str
|
||||
return _c
|
||||
}
|
||||
|
||||
// SupportedPKCEMethods provides a mock function with no fields
|
||||
func (_m *MockInterface) SupportedPKCEMethods() []string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SupportedPKCEMethods")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockInterface_SupportedPKCEMethods_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SupportedPKCEMethods'
|
||||
type MockInterface_SupportedPKCEMethods_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SupportedPKCEMethods is a helper method to define mock.On call
|
||||
func (_e *MockInterface_Expecter) SupportedPKCEMethods() *MockInterface_SupportedPKCEMethods_Call {
|
||||
return &MockInterface_SupportedPKCEMethods_Call{Call: _e.mock.On("SupportedPKCEMethods")}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_SupportedPKCEMethods_Call) Run(run func()) *MockInterface_SupportedPKCEMethods_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_SupportedPKCEMethods_Call) Return(_a0 []string) *MockInterface_SupportedPKCEMethods_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_SupportedPKCEMethods_Call) RunAndReturn(run func() []string) *MockInterface_SupportedPKCEMethods_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockInterface(t interface {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package logger_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package loader_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package repository_mock
|
||||
|
||||
@@ -24,9 +24,55 @@ func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
|
||||
return &MockInterface_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// FindByKey provides a mock function with given fields: dir, key
|
||||
func (_m *MockInterface) FindByKey(dir string, key tokencache.Key) (*oidc.TokenSet, error) {
|
||||
ret := _m.Called(dir, key)
|
||||
// DeleteAll provides a mock function with given fields: config
|
||||
func (_m *MockInterface) DeleteAll(config tokencache.Config) error {
|
||||
ret := _m.Called(config)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteAll")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(tokencache.Config) error); ok {
|
||||
r0 = rf(config)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockInterface_DeleteAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAll'
|
||||
type MockInterface_DeleteAll_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteAll is a helper method to define mock.On call
|
||||
// - config tokencache.Config
|
||||
func (_e *MockInterface_Expecter) DeleteAll(config interface{}) *MockInterface_DeleteAll_Call {
|
||||
return &MockInterface_DeleteAll_Call{Call: _e.mock.On("DeleteAll", config)}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_DeleteAll_Call) Run(run func(config tokencache.Config)) *MockInterface_DeleteAll_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(tokencache.Config))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_DeleteAll_Call) Return(_a0 error) *MockInterface_DeleteAll_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_DeleteAll_Call) RunAndReturn(run func(tokencache.Config) error) *MockInterface_DeleteAll_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// FindByKey provides a mock function with given fields: config, key
|
||||
func (_m *MockInterface) FindByKey(config tokencache.Config, key tokencache.Key) (*oidc.TokenSet, error) {
|
||||
ret := _m.Called(config, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FindByKey")
|
||||
@@ -34,19 +80,19 @@ func (_m *MockInterface) FindByKey(dir string, key tokencache.Key) (*oidc.TokenS
|
||||
|
||||
var r0 *oidc.TokenSet
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, tokencache.Key) (*oidc.TokenSet, error)); ok {
|
||||
return rf(dir, key)
|
||||
if rf, ok := ret.Get(0).(func(tokencache.Config, tokencache.Key) (*oidc.TokenSet, error)); ok {
|
||||
return rf(config, key)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, tokencache.Key) *oidc.TokenSet); ok {
|
||||
r0 = rf(dir, key)
|
||||
if rf, ok := ret.Get(0).(func(tokencache.Config, tokencache.Key) *oidc.TokenSet); ok {
|
||||
r0 = rf(config, key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oidc.TokenSet)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, tokencache.Key) error); ok {
|
||||
r1 = rf(dir, key)
|
||||
if rf, ok := ret.Get(1).(func(tokencache.Config, tokencache.Key) error); ok {
|
||||
r1 = rf(config, key)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -60,15 +106,15 @@ type MockInterface_FindByKey_Call struct {
|
||||
}
|
||||
|
||||
// FindByKey is a helper method to define mock.On call
|
||||
// - dir string
|
||||
// - config tokencache.Config
|
||||
// - key tokencache.Key
|
||||
func (_e *MockInterface_Expecter) FindByKey(dir interface{}, key interface{}) *MockInterface_FindByKey_Call {
|
||||
return &MockInterface_FindByKey_Call{Call: _e.mock.On("FindByKey", dir, key)}
|
||||
func (_e *MockInterface_Expecter) FindByKey(config interface{}, key interface{}) *MockInterface_FindByKey_Call {
|
||||
return &MockInterface_FindByKey_Call{Call: _e.mock.On("FindByKey", config, key)}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_FindByKey_Call) Run(run func(dir string, key tokencache.Key)) *MockInterface_FindByKey_Call {
|
||||
func (_c *MockInterface_FindByKey_Call) Run(run func(config tokencache.Config, key tokencache.Key)) *MockInterface_FindByKey_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(tokencache.Key))
|
||||
run(args[0].(tokencache.Config), args[1].(tokencache.Key))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -78,14 +124,14 @@ func (_c *MockInterface_FindByKey_Call) Return(_a0 *oidc.TokenSet, _a1 error) *M
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_FindByKey_Call) RunAndReturn(run func(string, tokencache.Key) (*oidc.TokenSet, error)) *MockInterface_FindByKey_Call {
|
||||
func (_c *MockInterface_FindByKey_Call) RunAndReturn(run func(tokencache.Config, tokencache.Key) (*oidc.TokenSet, error)) *MockInterface_FindByKey_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Lock provides a mock function with given fields: dir, key
|
||||
func (_m *MockInterface) Lock(dir string, key tokencache.Key) (io.Closer, error) {
|
||||
ret := _m.Called(dir, key)
|
||||
// Lock provides a mock function with given fields: config, key
|
||||
func (_m *MockInterface) Lock(config tokencache.Config, key tokencache.Key) (io.Closer, error) {
|
||||
ret := _m.Called(config, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Lock")
|
||||
@@ -93,19 +139,19 @@ func (_m *MockInterface) Lock(dir string, key tokencache.Key) (io.Closer, error)
|
||||
|
||||
var r0 io.Closer
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, tokencache.Key) (io.Closer, error)); ok {
|
||||
return rf(dir, key)
|
||||
if rf, ok := ret.Get(0).(func(tokencache.Config, tokencache.Key) (io.Closer, error)); ok {
|
||||
return rf(config, key)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, tokencache.Key) io.Closer); ok {
|
||||
r0 = rf(dir, key)
|
||||
if rf, ok := ret.Get(0).(func(tokencache.Config, tokencache.Key) io.Closer); ok {
|
||||
r0 = rf(config, key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(io.Closer)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, tokencache.Key) error); ok {
|
||||
r1 = rf(dir, key)
|
||||
if rf, ok := ret.Get(1).(func(tokencache.Config, tokencache.Key) error); ok {
|
||||
r1 = rf(config, key)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -119,15 +165,15 @@ type MockInterface_Lock_Call struct {
|
||||
}
|
||||
|
||||
// Lock is a helper method to define mock.On call
|
||||
// - dir string
|
||||
// - config tokencache.Config
|
||||
// - key tokencache.Key
|
||||
func (_e *MockInterface_Expecter) Lock(dir interface{}, key interface{}) *MockInterface_Lock_Call {
|
||||
return &MockInterface_Lock_Call{Call: _e.mock.On("Lock", dir, key)}
|
||||
func (_e *MockInterface_Expecter) Lock(config interface{}, key interface{}) *MockInterface_Lock_Call {
|
||||
return &MockInterface_Lock_Call{Call: _e.mock.On("Lock", config, key)}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Lock_Call) Run(run func(dir string, key tokencache.Key)) *MockInterface_Lock_Call {
|
||||
func (_c *MockInterface_Lock_Call) Run(run func(config tokencache.Config, key tokencache.Key)) *MockInterface_Lock_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(tokencache.Key))
|
||||
run(args[0].(tokencache.Config), args[1].(tokencache.Key))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -137,22 +183,22 @@ func (_c *MockInterface_Lock_Call) Return(_a0 io.Closer, _a1 error) *MockInterfa
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Lock_Call) RunAndReturn(run func(string, tokencache.Key) (io.Closer, error)) *MockInterface_Lock_Call {
|
||||
func (_c *MockInterface_Lock_Call) RunAndReturn(run func(tokencache.Config, tokencache.Key) (io.Closer, error)) *MockInterface_Lock_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: dir, key, tokenSet
|
||||
func (_m *MockInterface) Save(dir string, key tokencache.Key, tokenSet oidc.TokenSet) error {
|
||||
ret := _m.Called(dir, key, tokenSet)
|
||||
// Save provides a mock function with given fields: config, key, tokenSet
|
||||
func (_m *MockInterface) Save(config tokencache.Config, key tokencache.Key, tokenSet oidc.TokenSet) error {
|
||||
ret := _m.Called(config, key, tokenSet)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, tokencache.Key, oidc.TokenSet) error); ok {
|
||||
r0 = rf(dir, key, tokenSet)
|
||||
if rf, ok := ret.Get(0).(func(tokencache.Config, tokencache.Key, oidc.TokenSet) error); ok {
|
||||
r0 = rf(config, key, tokenSet)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -166,16 +212,16 @@ type MockInterface_Save_Call struct {
|
||||
}
|
||||
|
||||
// Save is a helper method to define mock.On call
|
||||
// - dir string
|
||||
// - config tokencache.Config
|
||||
// - key tokencache.Key
|
||||
// - tokenSet oidc.TokenSet
|
||||
func (_e *MockInterface_Expecter) Save(dir interface{}, key interface{}, tokenSet interface{}) *MockInterface_Save_Call {
|
||||
return &MockInterface_Save_Call{Call: _e.mock.On("Save", dir, key, tokenSet)}
|
||||
func (_e *MockInterface_Expecter) Save(config interface{}, key interface{}, tokenSet interface{}) *MockInterface_Save_Call {
|
||||
return &MockInterface_Save_Call{Call: _e.mock.On("Save", config, key, tokenSet)}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Save_Call) Run(run func(dir string, key tokencache.Key, tokenSet oidc.TokenSet)) *MockInterface_Save_Call {
|
||||
func (_c *MockInterface_Save_Call) Run(run func(config tokencache.Config, key tokencache.Key, tokenSet oidc.TokenSet)) *MockInterface_Save_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(tokencache.Key), args[2].(oidc.TokenSet))
|
||||
run(args[0].(tokencache.Config), args[1].(tokencache.Key), args[2].(oidc.TokenSet))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -185,7 +231,7 @@ func (_c *MockInterface_Save_Call) Return(_a0 error) *MockInterface_Save_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Save_Call) RunAndReturn(run func(string, tokencache.Key, oidc.TokenSet) error) *MockInterface_Save_Call {
|
||||
func (_c *MockInterface_Save_Call) RunAndReturn(run func(tokencache.Config, tokencache.Key, oidc.TokenSet) error) *MockInterface_Save_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package authentication_mock
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package clean_mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
clean "github.com/int128/kubelogin/pkg/usecases/clean"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockInterface is an autogenerated mock type for the Interface type
|
||||
type MockInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockInterface_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
|
||||
return &MockInterface_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Do provides a mock function with given fields: ctx, in
|
||||
func (_m *MockInterface) Do(ctx context.Context, in clean.Input) error {
|
||||
ret := _m.Called(ctx, in)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Do")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, clean.Input) error); ok {
|
||||
r0 = rf(ctx, in)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockInterface_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do'
|
||||
type MockInterface_Do_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Do is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in clean.Input
|
||||
func (_e *MockInterface_Expecter) Do(ctx interface{}, in interface{}) *MockInterface_Do_Call {
|
||||
return &MockInterface_Do_Call{Call: _e.mock.On("Do", ctx, in)}
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Do_Call) Run(run func(ctx context.Context, in clean.Input)) *MockInterface_Do_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(clean.Input))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Do_Call) Return(_a0 error) *MockInterface_Do_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockInterface_Do_Call) RunAndReturn(run func(context.Context, clean.Input) error) *MockInterface_Do_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockInterface(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockInterface {
|
||||
mock := &MockInterface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package credentialplugin_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package setup_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package standalone_mock
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.50.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package io_mock
|
||||
|
||||
|
||||
56
pkg/cmd/clean.go
Normal file
56
pkg/cmd/clean.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/usecases/clean"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type cleanOptions struct {
|
||||
tokenCacheOptions tokenCacheOptions
|
||||
}
|
||||
|
||||
func (o *cleanOptions) addFlags(f *pflag.FlagSet) {
|
||||
o.tokenCacheOptions.addFlags(f)
|
||||
}
|
||||
|
||||
func (o *cleanOptions) expandHomedir() {
|
||||
o.tokenCacheOptions.expandHomedir()
|
||||
}
|
||||
|
||||
type Clean struct {
|
||||
Clean clean.Interface
|
||||
}
|
||||
|
||||
func (cmd *Clean) New() *cobra.Command {
|
||||
var o cleanOptions
|
||||
c := &cobra.Command{
|
||||
Use: "clean [flags]",
|
||||
Short: "Delete the token cache",
|
||||
Long: `Delete the token cache.
|
||||
|
||||
This deletes both the OS keyring and the directory by default.
|
||||
If you encounter an error of keyring, try --token-cache-storage=disk.
|
||||
`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
o.expandHomedir()
|
||||
tokenCacheConfig, err := o.tokenCacheOptions.tokenCacheConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
in := clean.Input{
|
||||
TokenCacheConfig: tokenCacheConfig,
|
||||
}
|
||||
if err := cmd.Clean.Do(c.Context(), in); err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.Flags().SortFlags = false
|
||||
o.addFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/google/wire"
|
||||
@@ -18,23 +16,14 @@ var Set = wire.NewSet(
|
||||
wire.Struct(new(Root), "*"),
|
||||
wire.Struct(new(GetToken), "*"),
|
||||
wire.Struct(new(Setup), "*"),
|
||||
wire.Struct(new(Clean), "*"),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
}
|
||||
|
||||
func getDefaultTokenCacheDir(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
var defaultListenAddress = []string{"127.0.0.1:8000", "127.0.0.1:18000"}
|
||||
var defaultTokenCacheDir = filepath.Join(
|
||||
getDefaultTokenCacheDir("KUBECACHEDIR", filepath.Join("~", ".kube", "cache")),
|
||||
"oidc-login")
|
||||
|
||||
const defaultAuthenticationTimeoutSec = 180
|
||||
|
||||
@@ -43,6 +32,7 @@ type Cmd struct {
|
||||
Root *Root
|
||||
GetToken *GetToken
|
||||
Setup *Setup
|
||||
Clean *Clean
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
@@ -60,6 +50,9 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
setupCmd := cmd.Setup.New()
|
||||
rootCmd.AddCommand(setupCmd)
|
||||
|
||||
cleanCmd := cmd.Clean.New()
|
||||
rootCmd.AddCommand(cleanCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/tlsclientconfig"
|
||||
"github.com/int128/kubelogin/pkg/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
@@ -113,11 +114,14 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
},
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
|
||||
Storage: tokencache.StorageAuto,
|
||||
},
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
@@ -135,16 +139,20 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--token-cache-storage", "disk",
|
||||
"-v1",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
},
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
|
||||
Storage: tokencache.StorageDisk,
|
||||
},
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
@@ -162,12 +170,15 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--oidc-use-access-token=true",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
UseAccessToken: true,
|
||||
},
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
|
||||
Storage: tokencache.StorageAuto,
|
||||
},
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
@@ -188,11 +199,14 @@ func TestCmd_Run(t *testing.T) {
|
||||
"--token-cache-dir", "~/.kube/oidc-cache",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: filepath.Join(userHomeDir, ".kube/oidc-cache"),
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
},
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: filepath.Join(userHomeDir, ".kube/oidc-cache"),
|
||||
Storage: tokencache.StorageAuto,
|
||||
},
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
|
||||
@@ -17,10 +17,10 @@ type getTokenOptions struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
UsePKCE bool
|
||||
UseAccessToken bool
|
||||
TokenCacheDir string
|
||||
tokenCacheOptions tokenCacheOptions
|
||||
tlsOptions tlsOptions
|
||||
pkceOptions pkceOptions
|
||||
authenticationOptions authenticationOptions
|
||||
ForceRefresh bool
|
||||
}
|
||||
@@ -30,19 +30,18 @@ func (o *getTokenOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE usage")
|
||||
f.BoolVar(&o.UseAccessToken, "oidc-use-access-token", false, "Instead of using the id_token, use the access_token to authenticate to Kubernetes")
|
||||
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for token cache")
|
||||
f.BoolVar(&o.ForceRefresh, "force-refresh", false, "If set, refresh the ID token regardless of its expiration time")
|
||||
o.tokenCacheOptions.addFlags(f)
|
||||
o.tlsOptions.addFlags(f)
|
||||
o.pkceOptions.addFlags(f)
|
||||
o.authenticationOptions.addFlags(f)
|
||||
}
|
||||
|
||||
func (o *getTokenOptions) expandHomedir() error {
|
||||
o.TokenCacheDir = expandHomedir(o.TokenCacheDir)
|
||||
func (o *getTokenOptions) expandHomedir() {
|
||||
o.tokenCacheOptions.expandHomedir()
|
||||
o.authenticationOptions.expandHomedir()
|
||||
o.tlsOptions.expandHomedir()
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetToken struct {
|
||||
@@ -68,26 +67,32 @@ func (cmd *GetToken) New() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
if err := o.expandHomedir(); err != nil {
|
||||
return err
|
||||
}
|
||||
o.expandHomedir()
|
||||
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get-token: %w", err)
|
||||
}
|
||||
tokenCacheConfig, err := o.tokenCacheOptions.tokenCacheConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get-token: %w", err)
|
||||
}
|
||||
pkceMethod, err := o.pkceOptions.pkceMethod()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get-token: %w", err)
|
||||
}
|
||||
in := credentialplugin.Input{
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
UsePKCE: o.UsePKCE,
|
||||
PKCEMethod: pkceMethod,
|
||||
UseAccessToken: o.UseAccessToken,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
},
|
||||
TokenCacheDir: o.TokenCacheDir,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
|
||||
ForceRefresh: o.ForceRefresh,
|
||||
ForceRefresh: o.ForceRefresh,
|
||||
TokenCacheConfig: tokenCacheConfig,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
|
||||
}
|
||||
if err := cmd.GetToken.Do(c.Context(), in); err != nil {
|
||||
return fmt.Errorf("get-token: %w", err)
|
||||
|
||||
40
pkg/cmd/pkce.go
Normal file
40
pkg/cmd/pkce.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var allPKCEMethods = strings.Join([]string{"auto", "no", "S256"}, "|")
|
||||
|
||||
type pkceOptions struct {
|
||||
UsePKCE bool
|
||||
PKCEMethod string
|
||||
}
|
||||
|
||||
func (o *pkceOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE S256 code challenge method")
|
||||
if err := f.MarkDeprecated("oidc-use-pkce", "use --oidc-pkce-method instead. For the most providers, you don't need to set the flag."); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.StringVar(&o.PKCEMethod, "oidc-pkce-method", "auto", fmt.Sprintf("PKCE code challenge method. Automatically determined by default. One of (%s)", allPKCEMethods))
|
||||
}
|
||||
|
||||
func (o *pkceOptions) pkceMethod() (oidc.PKCEMethod, error) {
|
||||
if o.UsePKCE {
|
||||
return oidc.PKCEMethodS256, nil
|
||||
}
|
||||
switch o.PKCEMethod {
|
||||
case "auto":
|
||||
return oidc.PKCEMethodAuto, nil
|
||||
case "no":
|
||||
return oidc.PKCEMethodNo, nil
|
||||
case "S256":
|
||||
return oidc.PKCEMethodS256, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("oidc-pkce-method must be one of (%s)", allPKCEMethods)
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ type setupOptions struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
UsePKCE bool
|
||||
UseAccessToken bool
|
||||
tlsOptions tlsOptions
|
||||
pkceOptions pkceOptions
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ func (o *setupOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE usage")
|
||||
f.BoolVar(&o.UseAccessToken, "oidc-use-access-token", false, "Instead of using the id_token, use the access_token to authenticate to Kubernetes")
|
||||
o.tlsOptions.addFlags(f)
|
||||
o.pkceOptions.addFlags(f)
|
||||
o.authenticationOptions.addFlags(f)
|
||||
}
|
||||
|
||||
@@ -46,19 +46,26 @@ func (cmd *Setup) New() *cobra.Command {
|
||||
if err != nil {
|
||||
return fmt.Errorf("setup: %w", err)
|
||||
}
|
||||
pkceMethod, err := o.pkceOptions.pkceMethod()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setup: %w", err)
|
||||
}
|
||||
in := setup.Stage2Input{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
UsePKCE: o.UsePKCE,
|
||||
UseAccessToken: o.UseAccessToken,
|
||||
PKCEMethod: pkceMethod,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
|
||||
}
|
||||
if c.Flags().Lookup("listen-address").Changed {
|
||||
in.ListenAddressArgs = o.authenticationOptions.ListenAddress
|
||||
}
|
||||
if c.Flags().Lookup("oidc-pkce-method").Changed {
|
||||
in.PKCEMethodArg = o.pkceOptions.PKCEMethod
|
||||
}
|
||||
if in.IssuerURL == "" || in.ClientID == "" {
|
||||
cmd.Setup.DoStage1()
|
||||
return nil
|
||||
|
||||
@@ -18,7 +18,7 @@ type tlsOptions struct {
|
||||
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringArrayVar(&o.CACertFilename, "certificate-authority", nil, "Path to a cert file for the certificate authority")
|
||||
f.StringArrayVar(&o.CACertData, "certificate-authority-data", nil, "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")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "[SECURITY RISK] If set, the server's certificate will not be checked for validity")
|
||||
f.BoolVar(&o.RenegotiateOnceAsClient, "tls-renegotiation-once", false, "If set, allow a remote server to request renegotiation once per connection")
|
||||
f.BoolVar(&o.RenegotiateFreelyAsClient, "tls-renegotiation-freely", false, "If set, allow a remote server to repeatedly request renegotiation")
|
||||
}
|
||||
|
||||
52
pkg/cmd/tokencache.go
Normal file
52
pkg/cmd/tokencache.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/tokencache"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func getDefaultTokenCacheDir() string {
|
||||
// https://github.com/int128/kubelogin/pull/975
|
||||
if kubeCacheDir, ok := os.LookupEnv("KUBECACHEDIR"); ok {
|
||||
return filepath.Join(kubeCacheDir, "oidc-login")
|
||||
}
|
||||
return filepath.Join("~", ".kube", "cache", "oidc-login")
|
||||
}
|
||||
|
||||
var allTokenCacheStorage = strings.Join([]string{"auto", "keyring", "disk"}, "|")
|
||||
|
||||
type tokenCacheOptions struct {
|
||||
TokenCacheDir string
|
||||
TokenCacheStorage string
|
||||
}
|
||||
|
||||
func (o *tokenCacheOptions) addFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.TokenCacheDir, "token-cache-dir", getDefaultTokenCacheDir(), "Path to a directory of the token cache")
|
||||
f.StringVar(&o.TokenCacheStorage, "token-cache-storage", "auto", fmt.Sprintf("Storage for the token cache. One of (%s)", allTokenCacheStorage))
|
||||
}
|
||||
|
||||
func (o *tokenCacheOptions) expandHomedir() {
|
||||
o.TokenCacheDir = expandHomedir(o.TokenCacheDir)
|
||||
}
|
||||
|
||||
func (o *tokenCacheOptions) tokenCacheConfig() (tokencache.Config, error) {
|
||||
config := tokencache.Config{
|
||||
Directory: o.TokenCacheDir,
|
||||
}
|
||||
switch o.TokenCacheStorage {
|
||||
case "auto":
|
||||
config.Storage = tokencache.StorageAuto
|
||||
case "keyring":
|
||||
config.Storage = tokencache.StorageKeyring
|
||||
case "disk":
|
||||
config.Storage = tokencache.StorageDisk
|
||||
default:
|
||||
return tokencache.Config{}, fmt.Errorf("token-cache-storage must be one of (%s)", allTokenCacheStorage)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/tlsclientconfig/loader"
|
||||
"github.com/int128/kubelogin/pkg/tokencache/repository"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/clean"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
@@ -47,6 +48,7 @@ func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interf
|
||||
standalone.Set,
|
||||
credentialplugin.Set,
|
||||
setup.Set,
|
||||
clean.Set,
|
||||
|
||||
// infrastructure
|
||||
cmd.Set,
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/devicecode"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
|
||||
"github.com/int128/kubelogin/pkg/usecases/clean"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
@@ -96,7 +97,9 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
|
||||
Standalone: standaloneStandalone,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
repositoryRepository := &repository.Repository{}
|
||||
repositoryRepository := &repository.Repository{
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
reader3 := &reader2.Reader{}
|
||||
writer3 := &writer2.Writer{
|
||||
Stdout: stdout,
|
||||
@@ -120,10 +123,18 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
|
||||
cmdSetup := &cmd.Setup{
|
||||
Setup: setupSetup,
|
||||
}
|
||||
cleanClean := &clean.Clean{
|
||||
TokenCacheRepository: repositoryRepository,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdClean := &cmd.Clean{
|
||||
Clean: cleanClean,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Root: root,
|
||||
GetToken: cmdGetToken,
|
||||
Setup: cmdSetup,
|
||||
Clean: cmdClean,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
return cmdCmd
|
||||
|
||||
@@ -20,11 +20,11 @@ type Interface interface {
|
||||
GetAuthCodeURL(in AuthCodeURLInput) string
|
||||
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error)
|
||||
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error)
|
||||
NegotiatedPKCEMethod() pkce.Method
|
||||
GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error)
|
||||
GetDeviceAuthorization(ctx context.Context) (*oauth2dev.AuthorizationResponse, error)
|
||||
ExchangeDeviceCode(ctx context.Context, authResponse *oauth2dev.AuthorizationResponse) (*oidc.TokenSet, error)
|
||||
Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error)
|
||||
SupportedPKCEMethods() []string
|
||||
}
|
||||
|
||||
type AuthCodeURLInput struct {
|
||||
@@ -60,7 +60,7 @@ type client struct {
|
||||
oauth2Config oauth2.Config
|
||||
clock clock.Interface
|
||||
logger logger.Interface
|
||||
supportedPKCEMethods []string
|
||||
negotiatedPKCEMethod pkce.Method
|
||||
deviceAuthorizationEndpoint string
|
||||
useAccessToken bool
|
||||
}
|
||||
@@ -116,34 +116,33 @@ func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput)
|
||||
return c.verifyToken(ctx, token, in.Nonce)
|
||||
}
|
||||
|
||||
func authorizationRequestOptions(n string, p pkce.Params, e map[string]string) []oauth2.AuthCodeOption {
|
||||
o := []oauth2.AuthCodeOption{
|
||||
func authorizationRequestOptions(nonce string, pkceParams pkce.Params, extraParams map[string]string) []oauth2.AuthCodeOption {
|
||||
opts := []oauth2.AuthCodeOption{
|
||||
oauth2.AccessTypeOffline,
|
||||
gooidc.Nonce(n),
|
||||
gooidc.Nonce(nonce),
|
||||
}
|
||||
if !p.IsZero() {
|
||||
o = append(o,
|
||||
oauth2.SetAuthURLParam("code_challenge", p.CodeChallenge),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", p.CodeChallengeMethod),
|
||||
)
|
||||
if pkceParams.CodeChallenge != "" {
|
||||
opts = append(opts, oauth2.SetAuthURLParam("code_challenge", pkceParams.CodeChallenge))
|
||||
}
|
||||
for key, value := range e {
|
||||
o = append(o, oauth2.SetAuthURLParam(key, value))
|
||||
if pkceParams.CodeChallengeMethod != "" {
|
||||
opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", pkceParams.CodeChallengeMethod))
|
||||
}
|
||||
return o
|
||||
for key, value := range extraParams {
|
||||
opts = append(opts, oauth2.SetAuthURLParam(key, value))
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func tokenRequestOptions(p pkce.Params) (o []oauth2.AuthCodeOption) {
|
||||
if !p.IsZero() {
|
||||
o = append(o, oauth2.SetAuthURLParam("code_verifier", p.CodeVerifier))
|
||||
func tokenRequestOptions(pkceParams pkce.Params) []oauth2.AuthCodeOption {
|
||||
var opts []oauth2.AuthCodeOption
|
||||
if pkceParams.CodeVerifier != "" {
|
||||
opts = append(opts, oauth2.SetAuthURLParam("code_verifier", pkceParams.CodeVerifier))
|
||||
}
|
||||
return
|
||||
return opts
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c *client) NegotiatedPKCEMethod() pkce.Method {
|
||||
return c.negotiatedPKCEMethod
|
||||
}
|
||||
|
||||
// GetTokenByROPC performs the resource owner password credentials flow.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
gooidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/google/wire"
|
||||
@@ -24,7 +25,7 @@ var Set = wire.NewSet(
|
||||
)
|
||||
|
||||
type FactoryInterface interface {
|
||||
New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error)
|
||||
New(ctx context.Context, prov oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error)
|
||||
}
|
||||
|
||||
type Factory struct {
|
||||
@@ -34,7 +35,7 @@ type Factory struct {
|
||||
}
|
||||
|
||||
// New returns an instance of infrastructure.Interface with the given configuration.
|
||||
func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) {
|
||||
func (f *Factory) New(ctx context.Context, prov oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) {
|
||||
rawTLSClientConfig, err := f.Loader.Load(tlsClientConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load the TLS client config: %w", err)
|
||||
@@ -52,7 +53,7 @@ func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsc
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||
provider, err := gooidc.NewProvider(ctx, p.IssuerURL)
|
||||
provider, err := gooidc.NewProvider(ctx, prov.IssuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc discovery error: %w", err)
|
||||
}
|
||||
@@ -60,9 +61,6 @@ func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsc
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine supported PKCE methods: %w", err)
|
||||
}
|
||||
if len(supportedPKCEMethods) == 0 && p.UsePKCE {
|
||||
supportedPKCEMethods = []string{pkce.MethodS256}
|
||||
}
|
||||
deviceAuthorizationEndpoint, err := extractDeviceAuthorizationEndpoint(provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine device authorization endpoint: %w", err)
|
||||
@@ -72,34 +70,48 @@ func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsc
|
||||
provider: provider,
|
||||
oauth2Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Scopes: append(p.ExtraScopes, gooidc.ScopeOpenID),
|
||||
ClientID: prov.ClientID,
|
||||
ClientSecret: prov.ClientSecret,
|
||||
Scopes: append(prov.ExtraScopes, gooidc.ScopeOpenID),
|
||||
},
|
||||
clock: f.Clock,
|
||||
logger: f.Logger,
|
||||
supportedPKCEMethods: supportedPKCEMethods,
|
||||
negotiatedPKCEMethod: determinePKCEMethod(supportedPKCEMethods, prov.PKCEMethod),
|
||||
deviceAuthorizationEndpoint: deviceAuthorizationEndpoint,
|
||||
useAccessToken: p.UseAccessToken,
|
||||
useAccessToken: prov.UseAccessToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func determinePKCEMethod(supportedMethods []string, preferredMethod oidc.PKCEMethod) pkce.Method {
|
||||
switch preferredMethod {
|
||||
case oidc.PKCEMethodNo:
|
||||
return pkce.NoMethod
|
||||
case oidc.PKCEMethodS256:
|
||||
return pkce.MethodS256
|
||||
default:
|
||||
if slices.Contains(supportedMethods, "S256") {
|
||||
return pkce.MethodS256
|
||||
}
|
||||
return pkce.NoMethod
|
||||
}
|
||||
}
|
||||
|
||||
func extractSupportedPKCEMethods(provider *gooidc.Provider) ([]string, error) {
|
||||
var d struct {
|
||||
var claims struct {
|
||||
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||
}
|
||||
if err := provider.Claims(&d); err != nil {
|
||||
if err := provider.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("invalid discovery document: %w", err)
|
||||
}
|
||||
return d.CodeChallengeMethodsSupported, nil
|
||||
return claims.CodeChallengeMethodsSupported, nil
|
||||
}
|
||||
|
||||
func extractDeviceAuthorizationEndpoint(provider *gooidc.Provider) (string, error) {
|
||||
var d struct {
|
||||
var claims struct {
|
||||
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
|
||||
}
|
||||
if err := provider.Claims(&d); err != nil {
|
||||
if err := provider.Claims(&claims); err != nil {
|
||||
return "", fmt.Errorf("invalid discovery document: %w", err)
|
||||
}
|
||||
return d.DeviceAuthorizationEndpoint, nil
|
||||
return claims.DeviceAuthorizationEndpoint, nil
|
||||
}
|
||||
|
||||
@@ -15,10 +15,19 @@ type Provider struct {
|
||||
ClientID string
|
||||
ClientSecret string // optional
|
||||
ExtraScopes []string // optional
|
||||
UsePKCE bool // optional
|
||||
UseAccessToken bool // optional
|
||||
PKCEMethod PKCEMethod
|
||||
UseAccessToken bool
|
||||
}
|
||||
|
||||
// PKCEMethod represents a preferred method of PKCE.
|
||||
type PKCEMethod int
|
||||
|
||||
const (
|
||||
PKCEMethodAuto PKCEMethod = iota
|
||||
PKCEMethodNo
|
||||
PKCEMethodS256
|
||||
)
|
||||
|
||||
// TokenSet represents a set of ID token and refresh token.
|
||||
type TokenSet struct {
|
||||
IDToken string
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var Plain Params
|
||||
type Method int
|
||||
|
||||
const (
|
||||
// code challenge methods defined as https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
MethodS256 = "S256"
|
||||
// Code challenge methods defined as https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
NoMethod Method = iota
|
||||
MethodS256
|
||||
)
|
||||
|
||||
// Params represents a set of the PKCE parameters.
|
||||
@@ -24,27 +25,21 @@ type Params struct {
|
||||
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()
|
||||
}
|
||||
// It returns a zero value if no method is available.
|
||||
func New(method Method) (Params, error) {
|
||||
if method == MethodS256 {
|
||||
return NewS256()
|
||||
}
|
||||
return Plain, nil
|
||||
return Params{}, nil
|
||||
}
|
||||
|
||||
// NewS256 generates a parameters for S256.
|
||||
func NewS256() (Params, error) {
|
||||
b, err := random32()
|
||||
if err != nil {
|
||||
return Plain, fmt.Errorf("could not generate a random: %w", err)
|
||||
return Params{}, fmt.Errorf("could not generate a random: %w", err)
|
||||
}
|
||||
return computeS256(b), nil
|
||||
}
|
||||
@@ -63,7 +58,7 @@ func computeS256(b []byte) Params {
|
||||
_, _ = s.Write([]byte(v))
|
||||
return Params{
|
||||
CodeChallenge: base64URLEncode(s.Sum(nil)),
|
||||
CodeChallengeMethod: MethodS256,
|
||||
CodeChallengeMethod: "S256",
|
||||
CodeVerifier: v,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,40 +2,33 @@ package pkce
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Run("S256", func(t *testing.T) {
|
||||
p, err := New([]string{"plain", "S256"})
|
||||
params, err := New(MethodS256)
|
||||
if err != nil {
|
||||
t.Fatalf("New error: %s", err)
|
||||
}
|
||||
if p.CodeChallengeMethod != "S256" {
|
||||
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
|
||||
if params.CodeChallengeMethod != "S256" {
|
||||
t.Errorf("CodeChallengeMethod wants S256 but was %s", params.CodeChallengeMethod)
|
||||
}
|
||||
if p.CodeChallenge == "" {
|
||||
if params.CodeChallenge == "" {
|
||||
t.Errorf("CodeChallenge wants non-empty but was empty")
|
||||
}
|
||||
if p.CodeVerifier == "" {
|
||||
if params.CodeVerifier == "" {
|
||||
t.Errorf("CodeVerifier wants non-empty but was empty")
|
||||
}
|
||||
})
|
||||
t.Run("plain", func(t *testing.T) {
|
||||
p, err := New([]string{"plain"})
|
||||
t.Run("NoMethod", func(t *testing.T) {
|
||||
params, err := New(NoMethod)
|
||||
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")
|
||||
if diff := cmp.Diff(Params{}, params); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -12,8 +13,10 @@ import (
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/infrastructure/logger"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/tokencache"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
@@ -23,9 +26,10 @@ var Set = wire.NewSet(
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
FindByKey(dir string, key tokencache.Key) (*oidc.TokenSet, error)
|
||||
Save(dir string, key tokencache.Key, tokenSet oidc.TokenSet) error
|
||||
Lock(dir string, key tokencache.Key) (io.Closer, error)
|
||||
FindByKey(config tokencache.Config, key tokencache.Key) (*oidc.TokenSet, error)
|
||||
Save(config tokencache.Config, key tokencache.Key, tokenSet oidc.TokenSet) error
|
||||
Lock(config tokencache.Config, key tokencache.Key) (io.Closer, error)
|
||||
DeleteAll(config tokencache.Config) error
|
||||
}
|
||||
|
||||
type entity struct {
|
||||
@@ -35,23 +39,74 @@ type entity struct {
|
||||
|
||||
// Repository provides access to the token cache on the local filesystem.
|
||||
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
|
||||
type Repository struct{}
|
||||
type Repository struct {
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (r *Repository) FindByKey(dir string, key tokencache.Key) (*oidc.TokenSet, error) {
|
||||
filename, err := computeFilename(key)
|
||||
// keyringService is used to namespace the keyring access.
|
||||
// Some implementations may also display this string when prompting the user
|
||||
// for allowing access.
|
||||
const keyringService = "kubelogin"
|
||||
|
||||
// keyringItemPrefix is used as the prefix in the keyring items.
|
||||
const keyringItemPrefix = "kubelogin/tokencache/"
|
||||
|
||||
func (r *Repository) FindByKey(config tokencache.Config, key tokencache.Key) (*oidc.TokenSet, error) {
|
||||
checksum, err := computeChecksum(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not compute the key: %w", err)
|
||||
}
|
||||
p := filepath.Join(dir, filename)
|
||||
f, err := os.Open(p)
|
||||
switch config.Storage {
|
||||
case tokencache.StorageAuto:
|
||||
t, err := readFromKeyring(checksum)
|
||||
if errors.Is(err, keyring.ErrUnsupportedPlatform) ||
|
||||
errors.Is(err, keyring.ErrNotFound) {
|
||||
return readFromFile(config, checksum)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
case tokencache.StorageDisk:
|
||||
return readFromFile(config, checksum)
|
||||
case tokencache.StorageKeyring:
|
||||
return readFromKeyring(checksum)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown storage mode: %v", config.Storage)
|
||||
}
|
||||
}
|
||||
|
||||
func readFromFile(config tokencache.Config, checksum string) (*oidc.TokenSet, error) {
|
||||
p := filepath.Join(config.Directory, checksum)
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open file %s: %w", p, err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
t, err := decodeKey(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("file %s: %w", p, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func readFromKeyring(checksum string) (*oidc.TokenSet, error) {
|
||||
p := keyringItemPrefix + checksum
|
||||
s, err := keyring.Get(keyringService, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keyring secret %s: %w", p, err)
|
||||
}
|
||||
t, err := decodeKey([]byte(s))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("keyring %s: %w", p, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func decodeKey(b []byte) (*oidc.TokenSet, error) {
|
||||
var e entity
|
||||
if err := d.Decode(&e); err != nil {
|
||||
return nil, fmt.Errorf("invalid json file %s: %w", p, err)
|
||||
err := json.Unmarshal(b, &e)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token cache json: %w", err)
|
||||
}
|
||||
return &oidc.TokenSet{
|
||||
IDToken: e.IDToken,
|
||||
@@ -59,41 +114,72 @@ func (r *Repository) FindByKey(dir string, key tokencache.Key) (*oidc.TokenSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Save(dir string, key tokencache.Key, tokenSet oidc.TokenSet) error {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return fmt.Errorf("could not create directory %s: %w", dir, err)
|
||||
}
|
||||
filename, err := computeFilename(key)
|
||||
func (r *Repository) Save(config tokencache.Config, key tokencache.Key, tokenSet oidc.TokenSet) error {
|
||||
checksum, err := computeChecksum(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not compute the key: %w", err)
|
||||
}
|
||||
p := filepath.Join(dir, filename)
|
||||
f, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
switch config.Storage {
|
||||
case tokencache.StorageAuto:
|
||||
if err := writeToKeyring(checksum, tokenSet); err != nil {
|
||||
if errors.Is(err, keyring.ErrUnsupportedPlatform) {
|
||||
return writeToFile(config, checksum, tokenSet)
|
||||
}
|
||||
if errors.Is(err, keyring.ErrSetDataTooBig) {
|
||||
return writeToFile(config, checksum, tokenSet)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case tokencache.StorageDisk:
|
||||
return writeToFile(config, checksum, tokenSet)
|
||||
case tokencache.StorageKeyring:
|
||||
return writeToKeyring(checksum, tokenSet)
|
||||
default:
|
||||
return fmt.Errorf("unknown storage mode: %v", config.Storage)
|
||||
}
|
||||
}
|
||||
|
||||
func writeToFile(config tokencache.Config, checksum string, tokenSet oidc.TokenSet) error {
|
||||
p := filepath.Join(config.Directory, checksum)
|
||||
b, err := encodeKey(tokenSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file %s: %w", p, err)
|
||||
}
|
||||
if err := os.MkdirAll(config.Directory, 0700); err != nil {
|
||||
return fmt.Errorf("could not create directory %s: %w", config.Directory, err)
|
||||
}
|
||||
if err := os.WriteFile(p, b, 0600); err != nil {
|
||||
return fmt.Errorf("could not create file %s: %w", p, err)
|
||||
}
|
||||
defer f.Close()
|
||||
e := entity{
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
}
|
||||
if err := json.NewEncoder(f).Encode(&e); err != nil {
|
||||
return fmt.Errorf("json encode error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) Lock(tokenCacheDir string, key tokencache.Key) (io.Closer, error) {
|
||||
if err := os.MkdirAll(tokenCacheDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("could not create directory %s: %w", tokenCacheDir, err)
|
||||
func writeToKeyring(checksum string, tokenSet oidc.TokenSet) error {
|
||||
p := keyringItemPrefix + checksum
|
||||
b, err := encodeKey(tokenSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("keyring %s: %w", p, err)
|
||||
}
|
||||
keyDigest, err := computeFilename(key)
|
||||
if err := keyring.Set(keyringService, p, string(b)); err != nil {
|
||||
return fmt.Errorf("keyring write %s: %w", p, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) Lock(config tokencache.Config, key tokencache.Key) (io.Closer, error) {
|
||||
checksum, err := computeChecksum(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not compute the key: %w", err)
|
||||
}
|
||||
// NOTE: Both keyring and disk storage types use files for locking
|
||||
// No sensitive data is stored in the lock file
|
||||
if err := os.MkdirAll(config.Directory, 0700); err != nil {
|
||||
return nil, fmt.Errorf("could not create directory %s: %w", config.Directory, err)
|
||||
}
|
||||
// Do not lock the token cache file.
|
||||
// https://github.com/int128/kubelogin/issues/1144
|
||||
lockFilepath := filepath.Join(tokenCacheDir, keyDigest+".lock")
|
||||
lockFilepath := filepath.Join(config.Directory, checksum+".lock")
|
||||
lockFile := flock.New(lockFilepath)
|
||||
if err := lockFile.Lock(); err != nil {
|
||||
return nil, fmt.Errorf("could not lock the cache file %s: %w", lockFilepath, err)
|
||||
@@ -101,7 +187,48 @@ func (r *Repository) Lock(tokenCacheDir string, key tokencache.Key) (io.Closer,
|
||||
return lockFile, nil
|
||||
}
|
||||
|
||||
func computeFilename(key tokencache.Key) (string, error) {
|
||||
func (r *Repository) DeleteAll(config tokencache.Config) error {
|
||||
return errors.Join(
|
||||
func() error {
|
||||
if err := os.RemoveAll(config.Directory); err != nil {
|
||||
return fmt.Errorf("remove the directory %s: %w", config.Directory, err)
|
||||
}
|
||||
r.Logger.Printf("Deleted the token cache at %s", config.Directory)
|
||||
return nil
|
||||
}(),
|
||||
func() error {
|
||||
switch config.Storage {
|
||||
case tokencache.StorageAuto:
|
||||
if err := keyring.DeleteAll(keyringService); err != nil {
|
||||
if errors.Is(err, keyring.ErrUnsupportedPlatform) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("keyring delete: %w", err)
|
||||
}
|
||||
r.Logger.Printf("Deleted the token cache in the keyring")
|
||||
return nil
|
||||
case tokencache.StorageKeyring:
|
||||
if err := keyring.DeleteAll(keyringService); err != nil {
|
||||
return fmt.Errorf("keyring delete: %w", err)
|
||||
}
|
||||
r.Logger.Printf("Deleted the token cache in the keyring")
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
)
|
||||
}
|
||||
|
||||
func encodeKey(tokenSet oidc.TokenSet) ([]byte, error) {
|
||||
e := entity{
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
}
|
||||
return json.Marshal(&e)
|
||||
}
|
||||
|
||||
func computeChecksum(key tokencache.Key) (string, error) {
|
||||
s := sha256.New()
|
||||
e := gob.NewEncoder(s)
|
||||
if err := e.Encode(&key); err != nil {
|
||||
|
||||
@@ -16,6 +16,10 @@ func TestRepository_FindByKey(t *testing.T) {
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
config := tokencache.Config{
|
||||
Directory: dir,
|
||||
Storage: tokencache.StorageDisk,
|
||||
}
|
||||
key := tokencache.Key{
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "YOUR_ISSUER",
|
||||
@@ -27,8 +31,9 @@ func TestRepository_FindByKey(t *testing.T) {
|
||||
CACertFilename: []string{"/path/to/cert"},
|
||||
},
|
||||
}
|
||||
|
||||
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
|
||||
filename, err := computeFilename(key)
|
||||
filename, err := computeChecksum(key)
|
||||
if err != nil {
|
||||
t.Errorf("could not compute the key: %s", err)
|
||||
}
|
||||
@@ -37,7 +42,7 @@ func TestRepository_FindByKey(t *testing.T) {
|
||||
t.Fatalf("could not write to the temp file: %s", err)
|
||||
}
|
||||
|
||||
got, err := r.FindByKey(dir, key)
|
||||
got, err := r.FindByKey(config, key)
|
||||
if err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
@@ -53,6 +58,10 @@ func TestRepository_Save(t *testing.T) {
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
config := tokencache.Config{
|
||||
Directory: dir,
|
||||
Storage: tokencache.StorageDisk,
|
||||
}
|
||||
key := tokencache.Key{
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "YOUR_ISSUER",
|
||||
@@ -65,11 +74,11 @@ func TestRepository_Save(t *testing.T) {
|
||||
},
|
||||
}
|
||||
tokenSet := oidc.TokenSet{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
|
||||
if err := r.Save(dir, key, tokenSet); err != nil {
|
||||
if err := r.Save(config, key, tokenSet); err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
|
||||
filename, err := computeFilename(key)
|
||||
filename, err := computeChecksum(key)
|
||||
if err != nil {
|
||||
t.Errorf("could not compute the key: %s", err)
|
||||
}
|
||||
@@ -78,8 +87,7 @@ func TestRepository_Save(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the token cache file: %s", err)
|
||||
}
|
||||
want := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}
|
||||
`
|
||||
want := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
|
||||
got := string(b)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
|
||||
@@ -11,3 +11,24 @@ type Key struct {
|
||||
TLSClientConfig tlsclientconfig.Config
|
||||
Username string
|
||||
}
|
||||
|
||||
// Config represents a configuration for the token cache.
|
||||
type Config struct {
|
||||
// Directory is a path to the directory to store a token cache.
|
||||
// Note that a lock file is created into this directory even if the keyring is used.
|
||||
Directory string
|
||||
|
||||
Storage Storage
|
||||
}
|
||||
|
||||
// Storage is an enum of different storage strategies.
|
||||
type Storage byte
|
||||
|
||||
const (
|
||||
// StorageAuto will prefer keyring when available, and fallback to disk when not.
|
||||
StorageAuto Storage = iota
|
||||
// StorageDisk will only store cached keys on disk.
|
||||
StorageDisk
|
||||
// StorageDisk will only store cached keys in the OS keyring.
|
||||
StorageKeyring
|
||||
)
|
||||
|
||||
@@ -41,9 +41,9 @@ func (u *Browser) Do(ctx context.Context, o *BrowserOption, oidcClient client.In
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate a nonce: %w", err)
|
||||
}
|
||||
p, err := pkce.New(oidcClient.SupportedPKCEMethods())
|
||||
pkceParams, err := pkce.New(oidcClient.NegotiatedPKCEMethod())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate PKCE parameters: %w", err)
|
||||
return nil, fmt.Errorf("could not generate the PKCE parameters: %w", err)
|
||||
}
|
||||
successHTML := BrowserSuccessHTML
|
||||
if o.OpenURLAfterAuthentication != "" {
|
||||
@@ -53,7 +53,7 @@ func (u *Browser) Do(ctx context.Context, o *BrowserOption, oidcClient client.In
|
||||
BindAddress: o.BindAddress,
|
||||
State: state,
|
||||
Nonce: nonce,
|
||||
PKCEParams: p,
|
||||
PKCEParams: pkceParams,
|
||||
RedirectURLHostname: o.RedirectURLHostname,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
LocalServerSuccessHTML: successHTML,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/oidc/client_mock"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/oidc/client"
|
||||
"github.com/int128/kubelogin/pkg/pkce"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -31,9 +32,7 @@ func TestBrowser_Do(t *testing.T) {
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
}
|
||||
mockClient := client_mock.NewMockInterface(t)
|
||||
mockClient.EXPECT().
|
||||
SupportedPKCEMethods().
|
||||
Return(nil)
|
||||
mockClient.EXPECT().NegotiatedPKCEMethod().Return(pkce.NoMethod)
|
||||
mockClient.EXPECT().
|
||||
GetTokenByAuthCode(mock.Anything, mock.Anything, mock.Anything).
|
||||
Run(func(_ context.Context, in client.GetTokenByAuthCodeInput, readyChan chan<- string) {
|
||||
@@ -85,9 +84,7 @@ func TestBrowser_Do(t *testing.T) {
|
||||
AuthenticationTimeout: 10 * time.Second,
|
||||
}
|
||||
mockClient := client_mock.NewMockInterface(t)
|
||||
mockClient.EXPECT().
|
||||
SupportedPKCEMethods().
|
||||
Return(nil)
|
||||
mockClient.EXPECT().NegotiatedPKCEMethod().Return(pkce.NoMethod)
|
||||
mockClient.EXPECT().
|
||||
GetTokenByAuthCode(mock.Anything, mock.Anything, mock.Anything).
|
||||
Run(func(_ context.Context, _ client.GetTokenByAuthCodeInput, readyChan chan<- string) {
|
||||
@@ -127,9 +124,7 @@ func TestBrowser_Do(t *testing.T) {
|
||||
AuthenticationTimeout: 10 * time.Second,
|
||||
}
|
||||
mockClient := client_mock.NewMockInterface(t)
|
||||
mockClient.EXPECT().
|
||||
SupportedPKCEMethods().
|
||||
Return(nil)
|
||||
mockClient.EXPECT().NegotiatedPKCEMethod().Return(pkce.NoMethod)
|
||||
mockClient.EXPECT().
|
||||
GetTokenByAuthCode(mock.Anything, mock.Anything, mock.Anything).
|
||||
Run(func(_ context.Context, _ client.GetTokenByAuthCodeInput, readyChan chan<- string) {
|
||||
|
||||
@@ -34,15 +34,14 @@ func (u *Keyboard) Do(ctx context.Context, o *KeyboardOption, oidcClient client.
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate a nonce: %w", err)
|
||||
}
|
||||
p, err := pkce.New(oidcClient.SupportedPKCEMethods())
|
||||
pkceParams, err := pkce.New(oidcClient.NegotiatedPKCEMethod())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate PKCE parameters: %w", err)
|
||||
return nil, fmt.Errorf("could not generate the PKCE parameters: %w", err)
|
||||
}
|
||||
|
||||
authCodeURL := oidcClient.GetAuthCodeURL(client.AuthCodeURLInput{
|
||||
State: state,
|
||||
Nonce: nonce,
|
||||
PKCEParams: p,
|
||||
PKCEParams: pkceParams,
|
||||
RedirectURI: o.RedirectURL,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
})
|
||||
@@ -55,7 +54,7 @@ func (u *Keyboard) Do(ctx context.Context, o *KeyboardOption, oidcClient client.
|
||||
u.Logger.V(1).Infof("exchanging the code and token")
|
||||
tokenSet, err := oidcClient.ExchangeAuthCode(ctx, client.ExchangeAuthCodeInput{
|
||||
Code: code,
|
||||
PKCEParams: p,
|
||||
PKCEParams: pkceParams,
|
||||
Nonce: nonce,
|
||||
RedirectURI: o.RedirectURL,
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/oidc/client_mock"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/oidc/client"
|
||||
"github.com/int128/kubelogin/pkg/pkce"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -24,9 +25,7 @@ func TestKeyboard_Do(t *testing.T) {
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
}
|
||||
mockClient := client_mock.NewMockInterface(t)
|
||||
mockClient.EXPECT().
|
||||
SupportedPKCEMethods().
|
||||
Return(nil)
|
||||
mockClient.EXPECT().NegotiatedPKCEMethod().Return(pkce.NoMethod)
|
||||
mockClient.EXPECT().
|
||||
GetAuthCodeURL(mock.Anything).
|
||||
Run(func(in client.AuthCodeURLInput) {
|
||||
|
||||
@@ -34,8 +34,6 @@ type Input struct {
|
||||
GrantOptionSet GrantOptionSet
|
||||
CachedTokenSet *oidc.TokenSet // optional
|
||||
TLSClientConfig tlsclientconfig.Config
|
||||
ForceRefresh bool
|
||||
UseAccessToken bool
|
||||
}
|
||||
|
||||
type GrantOptionSet struct {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/oidc/client_mock"
|
||||
"github.com/int128/kubelogin/pkg/oidc"
|
||||
"github.com/int128/kubelogin/pkg/oidc/client"
|
||||
"github.com/int128/kubelogin/pkg/pkce"
|
||||
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
testingLogger "github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/tlsclientconfig"
|
||||
@@ -96,9 +97,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mockClient := client_mock.NewMockInterface(t)
|
||||
mockClient.EXPECT().
|
||||
SupportedPKCEMethods().
|
||||
Return(nil)
|
||||
mockClient.EXPECT().NegotiatedPKCEMethod().Return(pkce.NoMethod)
|
||||
mockClient.EXPECT().
|
||||
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, errors.New("token has expired"))
|
||||
|
||||
38
pkg/usecases/clean/clean.go
Normal file
38
pkg/usecases/clean/clean.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package clean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/infrastructure/logger"
|
||||
"github.com/int128/kubelogin/pkg/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/tokencache/repository"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Clean), "*"),
|
||||
wire.Bind(new(Interface), new(*Clean)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Do(ctx context.Context, in Input) error
|
||||
}
|
||||
|
||||
// Input represents an input of the Clean use-case.
|
||||
type Input struct {
|
||||
TokenCacheConfig tokencache.Config
|
||||
}
|
||||
|
||||
type Clean struct {
|
||||
TokenCacheRepository repository.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (u *Clean) Do(ctx context.Context, in Input) error {
|
||||
u.Logger.V(1).Infof("Deleting the token cache")
|
||||
if err := u.TokenCacheRepository.DeleteAll(in.TokenCacheConfig); err != nil {
|
||||
return fmt.Errorf("delete the token cache: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -31,11 +31,11 @@ type Interface interface {
|
||||
|
||||
// Input represents an input DTO of the GetToken use-case.
|
||||
type Input struct {
|
||||
Provider oidc.Provider
|
||||
TokenCacheDir string
|
||||
GrantOptionSet authentication.GrantOptionSet
|
||||
TLSClientConfig tlsclientconfig.Config
|
||||
ForceRefresh bool
|
||||
Provider oidc.Provider
|
||||
ForceRefresh bool
|
||||
TokenCacheConfig tokencache.Config
|
||||
GrantOptionSet authentication.GrantOptionSet
|
||||
TLSClientConfig tlsclientconfig.Config
|
||||
}
|
||||
|
||||
type GetToken struct {
|
||||
@@ -56,7 +56,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
}
|
||||
u.Logger.V(1).Infof("credential plugin is called with apiVersion: %s", credentialPluginInput.ClientAuthenticationAPIVersion)
|
||||
|
||||
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
|
||||
u.Logger.V(1).Infof("finding a token cache")
|
||||
tokenCacheKey := tokencache.Key{
|
||||
Provider: in.Provider,
|
||||
TLSClientConfig: in.TLSClientConfig,
|
||||
@@ -66,7 +66,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
}
|
||||
|
||||
u.Logger.V(1).Infof("acquiring the lock of token cache")
|
||||
lock, err := u.TokenCacheRepository.Lock(in.TokenCacheDir, tokenCacheKey)
|
||||
lock, err := u.TokenCacheRepository.Lock(in.TokenCacheConfig, tokenCacheKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not lock the token cache: %w", err)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
}
|
||||
}()
|
||||
|
||||
cachedTokenSet, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, tokenCacheKey)
|
||||
cachedTokenSet, err := u.TokenCacheRepository.FindByKey(in.TokenCacheConfig, tokenCacheKey)
|
||||
if err != nil {
|
||||
u.Logger.V(1).Infof("could not find a token cache: %s", err)
|
||||
}
|
||||
@@ -114,7 +114,6 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
GrantOptionSet: in.GrantOptionSet,
|
||||
CachedTokenSet: cachedTokenSet,
|
||||
TLSClientConfig: in.TLSClientConfig,
|
||||
ForceRefresh: in.ForceRefresh,
|
||||
}
|
||||
authenticationOutput, err := u.Authentication.Do(ctx, authenticationInput)
|
||||
if err != nil {
|
||||
@@ -126,7 +125,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
}
|
||||
u.Logger.V(1).Infof("you got a token: %s", idTokenClaims.Pretty)
|
||||
u.Logger.V(1).Infof("you got a valid token until %s", idTokenClaims.Expiry)
|
||||
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, tokenCacheKey, authenticationOutput.TokenSet); err != nil {
|
||||
if err := u.TokenCacheRepository.Save(in.TokenCacheConfig, tokenCacheKey, authenticationOutput.TokenSet); err != nil {
|
||||
return fmt.Errorf("could not write the token cache: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("writing the token to client-go")
|
||||
|
||||
@@ -64,8 +64,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
}
|
||||
ctx := context.TODO()
|
||||
in := Input{
|
||||
Provider: dummyProvider,
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
Provider: dummyProvider,
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: "/path/to/token-cache",
|
||||
},
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
mockAuthentication := authentication_mock.NewMockInterface(t)
|
||||
@@ -81,13 +83,13 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Return(nil)
|
||||
mockRepository := repository_mock.NewMockInterface(t)
|
||||
mockRepository.EXPECT().
|
||||
Lock("/path/to/token-cache", tokenCacheKey).
|
||||
Lock(in.TokenCacheConfig, tokenCacheKey).
|
||||
Return(mockCloser, nil)
|
||||
mockRepository.EXPECT().
|
||||
FindByKey("/path/to/token-cache", tokenCacheKey).
|
||||
FindByKey(in.TokenCacheConfig, tokenCacheKey).
|
||||
Return(nil, errors.New("file not found"))
|
||||
mockRepository.EXPECT().
|
||||
Save("/path/to/token-cache", tokenCacheKey, issuedTokenSet).
|
||||
Save(in.TokenCacheConfig, tokenCacheKey, issuedTokenSet).
|
||||
Return(nil)
|
||||
mockReader := reader_mock.NewMockInterface(t)
|
||||
mockReader.EXPECT().
|
||||
@@ -125,8 +127,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
|
||||
ctx := context.TODO()
|
||||
in := Input{
|
||||
Provider: dummyProvider,
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
Provider: dummyProvider,
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: "/path/to/token-cache",
|
||||
},
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
mockAuthentication := authentication_mock.NewMockInterface(t)
|
||||
@@ -142,13 +146,13 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Return(nil)
|
||||
mockRepository := repository_mock.NewMockInterface(t)
|
||||
mockRepository.EXPECT().
|
||||
Lock("/path/to/token-cache", tokenCacheKey).
|
||||
Lock(in.TokenCacheConfig, tokenCacheKey).
|
||||
Return(mockCloser, nil)
|
||||
mockRepository.EXPECT().
|
||||
FindByKey("/path/to/token-cache", tokenCacheKey).
|
||||
FindByKey(in.TokenCacheConfig, tokenCacheKey).
|
||||
Return(nil, errors.New("file not found"))
|
||||
mockRepository.EXPECT().
|
||||
Save("/path/to/token-cache", tokenCacheKey, issuedTokenSet).
|
||||
Save(in.TokenCacheConfig, tokenCacheKey, issuedTokenSet).
|
||||
Return(nil)
|
||||
mockReader := reader_mock.NewMockInterface(t)
|
||||
mockReader.EXPECT().
|
||||
@@ -182,8 +186,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
|
||||
ctx := context.TODO()
|
||||
in := Input{
|
||||
Provider: dummyProvider,
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
Provider: dummyProvider,
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: "/path/to/token-cache",
|
||||
},
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
mockCloser := io_mock.NewMockCloser(t)
|
||||
@@ -192,10 +198,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Return(nil)
|
||||
mockRepository := repository_mock.NewMockInterface(t)
|
||||
mockRepository.EXPECT().
|
||||
Lock("/path/to/token-cache", tokenCacheKey).
|
||||
Lock(in.TokenCacheConfig, tokenCacheKey).
|
||||
Return(mockCloser, nil)
|
||||
mockRepository.EXPECT().
|
||||
FindByKey("/path/to/token-cache", tokencache.Key{
|
||||
FindByKey(in.TokenCacheConfig, tokencache.Key{
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
@@ -234,8 +240,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
}
|
||||
ctx := context.TODO()
|
||||
in := Input{
|
||||
Provider: dummyProvider,
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
Provider: dummyProvider,
|
||||
TokenCacheConfig: tokencache.Config{
|
||||
Directory: "/path/to/token-cache",
|
||||
},
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
mockAuthentication := authentication_mock.NewMockInterface(t)
|
||||
@@ -251,10 +259,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Return(nil)
|
||||
mockRepository := repository_mock.NewMockInterface(t)
|
||||
mockRepository.EXPECT().
|
||||
Lock("/path/to/token-cache", tokenCacheKey).
|
||||
Lock(in.TokenCacheConfig, tokenCacheKey).
|
||||
Return(mockCloser, nil)
|
||||
mockRepository.EXPECT().
|
||||
FindByKey("/path/to/token-cache", tokencache.Key{
|
||||
FindByKey(in.TokenCacheConfig, tokencache.Key{
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
|
||||
@@ -37,7 +37,7 @@ Add the following options to the kube-apiserver:
|
||||
Run the following command:
|
||||
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-api-version=client.authentication.k8s.io/v1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
@@ -73,9 +73,10 @@ type Stage2Input struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
UsePKCE bool // optional
|
||||
UseAccessToken bool // optional
|
||||
ListenAddressArgs []string // non-nil if set by the command arg
|
||||
PKCEMethod oidc.PKCEMethod
|
||||
PKCEMethodArg string
|
||||
GrantOptionSet authentication.GrantOptionSet
|
||||
TLSClientConfig tlsclientconfig.Config
|
||||
}
|
||||
@@ -84,15 +85,15 @@ func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
|
||||
u.Logger.Printf("authentication in progress...")
|
||||
out, err := u.Authentication.Do(ctx, authentication.Input{
|
||||
Provider: oidc.Provider{
|
||||
IssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
UsePKCE: in.UsePKCE,
|
||||
IssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
PKCEMethod: in.PKCEMethod,
|
||||
UseAccessToken: in.UseAccessToken,
|
||||
},
|
||||
GrantOptionSet: in.GrantOptionSet,
|
||||
TLSClientConfig: in.TLSClientConfig,
|
||||
UseAccessToken: in.UseAccessToken,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("authentication error: %w", err)
|
||||
@@ -127,8 +128,8 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
|
||||
for _, extraScope := range in.ExtraScopes {
|
||||
args = append(args, "--oidc-extra-scope="+extraScope)
|
||||
}
|
||||
if in.UsePKCE {
|
||||
args = append(args, "--oidc-use-pkce")
|
||||
if in.PKCEMethodArg != "" {
|
||||
args = append(args, "--oidc-pkce-method="+in.PKCEMethodArg)
|
||||
}
|
||||
if in.UseAccessToken {
|
||||
args = append(args, "--oidc-use-access-token")
|
||||
|
||||
@@ -70,7 +70,7 @@ func Test_makeCredentialPluginArgs(t *testing.T) {
|
||||
ClientID: "test_kid",
|
||||
ClientSecret: "test_ksecret",
|
||||
ExtraScopes: []string{"groups"},
|
||||
UsePKCE: true,
|
||||
PKCEMethodArg: "S256",
|
||||
ListenAddressArgs: []string{"127.0.0.1:8080", "127.0.0.1:8888"},
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeBrowserOption: &authcode.BrowserOption{
|
||||
@@ -94,7 +94,7 @@ func Test_makeCredentialPluginArgs(t *testing.T) {
|
||||
"--oidc-client-id=test_kid",
|
||||
"--oidc-client-secret=test_ksecret",
|
||||
"--oidc-extra-scope=groups",
|
||||
"--oidc-use-pkce",
|
||||
"--oidc-pkce-method=S256",
|
||||
"--certificate-authority=/path/to/ca.crt",
|
||||
"--certificate-authority-data=base64encoded1",
|
||||
"--insecure-skip-tls-verify",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
CERT_DIR := cert
|
||||
|
||||
.PHONY: login
|
||||
login: setup
|
||||
$(MAKE) -C login
|
||||
.PHONY: test-with-dbus-session
|
||||
test-with-dbus-session:
|
||||
dbus-run-session -- $(MAKE) test
|
||||
|
||||
.PHONY: setup
|
||||
setup: dex cluster setup-chrome
|
||||
.PHONY: test
|
||||
test: dex cluster setup-chrome setup-keyring
|
||||
$(MAKE) -C login
|
||||
|
||||
.PHONY: dex
|
||||
dex: cert
|
||||
@@ -15,11 +16,18 @@ dex: cert
|
||||
cluster: cert
|
||||
$(MAKE) -C cluster
|
||||
|
||||
# Add the server certificate of dex to the trust store for Chrome.
|
||||
.PHONY: setup-chrome
|
||||
setup-chrome: cert
|
||||
# add the dex server certificate to the trust store
|
||||
mkdir -p $(HOME)/.pki/nssdb
|
||||
certutil -A -d sql:$(HOME)/.pki/nssdb -n dex -i $(CERT_DIR)/ca.crt -t "TC,,"
|
||||
|
||||
# Start gnome-keyring-daemon.
|
||||
# https://github.com/zalando/go-keyring/issues/45
|
||||
.PHONY: setup-keyring
|
||||
setup-keyring:
|
||||
echo password | gnome-keyring-daemon --unlock
|
||||
|
||||
.PHONY: cert
|
||||
cert:
|
||||
$(MAKE) -C cert
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
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 set up Kubernetes OIDC authentication using the [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.
|
||||
|
||||
@@ -18,7 +17,6 @@ It depends on the following components:
|
||||
- Browser (Chrome)
|
||||
- kubectl command
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
Let's take a look at the diagram.
|
||||
@@ -45,7 +43,6 @@ It performs the test by the following steps:
|
||||
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:
|
||||
@@ -80,7 +77,6 @@ make terminate
|
||||
make clean
|
||||
```
|
||||
|
||||
|
||||
## Technical consideration
|
||||
|
||||
### Network and DNS
|
||||
|
||||
@@ -8,13 +8,16 @@ export KUBECONFIG
|
||||
cluster:
|
||||
cp $(CERT_DIR)/ca.crt /tmp/kubelogin-system-test-dex-ca.crt
|
||||
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
|
||||
# add the Dex container IP to /etc/hosts
|
||||
|
||||
# Add the Dex container IP to /etc/hosts.
|
||||
docker inspect -f '{{.NetworkSettings.Networks.kind.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
|
||||
docker exec -i $(CLUSTER_NAME)-control-plane tee -a /etc/hosts
|
||||
# wait for kube-apiserver oidc initialization
|
||||
# (oidc authenticator will retry oidc discovery every 10s)
|
||||
|
||||
# Wait for kube-apiserver oidc initialization.
|
||||
# oidc authenticator will retry oidc discovery every 10s.
|
||||
sleep 10
|
||||
# add the cluster role
|
||||
|
||||
# Add the cluster role.
|
||||
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
|
||||
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=admin@example.com
|
||||
|
||||
|
||||
@@ -2,15 +2,18 @@ CERT_DIR := ../cert
|
||||
|
||||
.PHONY: dex
|
||||
dex: dex.yaml
|
||||
# wait for kind network
|
||||
while true; do if docker network inspect kind; then break; fi; sleep 1; done
|
||||
# create a container
|
||||
# Wait for kind network.
|
||||
until docker network inspect kind; do sleep 1; done
|
||||
|
||||
# Create a container.
|
||||
docker create -q --name dex-server -p 10443:10443 --network kind ghcr.io/dexidp/dex:v2.39.0 dex serve /dex.yaml
|
||||
# deploy the config
|
||||
|
||||
# Deploy the config.
|
||||
docker cp $(CERT_DIR)/server.crt dex-server:/
|
||||
docker cp $(CERT_DIR)/server.key dex-server:/
|
||||
docker cp dex.yaml dex-server:/
|
||||
# start the container
|
||||
|
||||
# Start the container.
|
||||
docker start dex-server
|
||||
docker logs dex-server
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export KUBECONFIG
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
# see the setup instruction
|
||||
# See the setup instruction.
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
@@ -17,7 +17,8 @@ test: build
|
||||
--oidc-extra-scope=email \
|
||||
--certificate-authority=$(CERT_DIR)/ca.crt \
|
||||
--browser-command=$(BIN_DIR)/chromelogin
|
||||
# set up the kubeconfig
|
||||
|
||||
# Set up the kubeconfig.
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1 \
|
||||
--exec-interactive-mode=Never \
|
||||
@@ -30,7 +31,11 @@ test: build
|
||||
--exec-arg=--oidc-extra-scope=email \
|
||||
--exec-arg=--certificate-authority=$(CERT_DIR)/ca.crt \
|
||||
--exec-arg=--browser-command=$(BIN_DIR)/chromelogin
|
||||
# make sure we can access the cluster
|
||||
|
||||
# Show the kubeconfig.
|
||||
kubectl config view
|
||||
|
||||
# Test the authentication.
|
||||
kubectl --user=oidc cluster-info
|
||||
|
||||
.PHONY: build
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module github.com/int128/kubelogin/tools
|
||||
|
||||
go 1.23.4
|
||||
go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/golangci/golangci-lint v1.63.4
|
||||
github.com/google/wire v0.6.0
|
||||
github.com/vektra/mockery/v2 v2.50.4
|
||||
github.com/vektra/mockery/v2 v2.51.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -580,8 +580,8 @@ github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYR
|
||||
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
|
||||
github.com/uudashr/iface v1.3.0 h1:zwPch0fs9tdh9BmL5kcgSpvnObV+yHjO4JjVBl8IA10=
|
||||
github.com/uudashr/iface v1.3.0/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg=
|
||||
github.com/vektra/mockery/v2 v2.50.4 h1:tD2ndcn1bVD63r4bbTQ3d6ucOUdUYRQAcUq5LZHIzwQ=
|
||||
github.com/vektra/mockery/v2 v2.50.4/go.mod h1:xO2DeYemEPC2tCzIZ+a1tifZ/7Laf/Chxg3vlc+oDsI=
|
||||
github.com/vektra/mockery/v2 v2.51.1 h1:BiiUSotsS7B56xvTjlIY2VDZxiEY9rzQ+ev69jE/mtw=
|
||||
github.com/vektra/mockery/v2 v2.51.1/go.mod h1:xO2DeYemEPC2tCzIZ+a1tifZ/7Laf/Chxg3vlc+oDsI=
|
||||
github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=
|
||||
github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=
|
||||
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
|
||||
|
||||
Reference in New Issue
Block a user