Compare commits

..

33 Commits

Author SHA1 Message Date
James White
6726d851cb Fallback to disk storage if too big for keyring (#1257) 2025-01-25 09:54:28 +09:00
renovate[bot]
21e03dc294 chore(deps): update docker/build-push-action action to v6.13.0 (#1261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 14:07:45 +00:00
renovate[bot]
5f1ed82a85 fix(deps): update module github.com/chromedp/chromedp to v0.12.1 (#1258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 08:11:34 +00:00
renovate[bot]
abb1a564f4 chore(deps): update actions/setup-go action to v5.3.0 (#1256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 11:06:49 +00:00
renovate[bot]
6d4eee5d1d fix(deps): update module github.com/vektra/mockery/v2 to v2.51.1 (#1254)
* fix(deps): update module github.com/vektra/mockery/v2 to v2.51.1

* Generated by GitHub Actions (go / generate)

https://github.com/int128/kubelogin/actions/runs/12861944941

* Empty commit to trigger GitHub Actions

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: update-generated-files-action <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: int128-renovate-merge-bot[bot] <132176788+int128-renovate-merge-bot[bot]@users.noreply.github.com>
2025-01-20 08:12:01 +00:00
Hidetake Iwata
4c10146639 Refactor integration-test and acceptance-test (#1252)
* Refactor tests

* Fix

* Run plugin

* Fix

* Update acceptance-test.yaml

* Fix
2025-01-20 09:37:10 +09:00
Hidetake Iwata
3121e55498 Update apiVersion to client.authentication.k8s.io/v1 (integration-test) (#1251) 2025-01-19 17:58:55 +09:00
Hidetake Iwata
a2a6ea229d Improve docs (#1250)
* Refactor docs

* Update --exec-api-version

* Add device authorization grant

* Fix
2025-01-19 15:02:02 +09:00
Rahul Somasundaram
e7819f15eb Added windows arm64 release (#1244)
Co-authored-by: Hidetake Iwata <int128@gmail.com>
2025-01-19 11:12:57 +09:00
renovate[bot]
6099a60aad chore(deps): update dependency go to v1.23.5 (#1249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-18 15:06:31 +00:00
Hidetake Iwata
e31ad59e63 Add clean command (#1248)
* Add clean command

* Refactor

* Refactor
2025-01-18 22:24:23 +09:00
renovate[bot]
355d9cf224 fix(deps): update kubernetes packages to v0.32.1 (#1246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 15:08:01 +00:00
renovate[bot]
fb5cfcf18f chore(deps): update docker/build-push-action action to v6.12.0 (#1247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 13:14:40 +00:00
renovate[bot]
31fadd2569 chore(deps): update int128/update-generated-files-action action to v2.57.0 (#1245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 12:14:48 +00:00
renovate[bot]
9f55437307 fix(deps): update module github.com/vektra/mockery/v2 to v2.51.0 (#1243)
* fix(deps): update module github.com/vektra/mockery/v2 to v2.51.0

* Generated by GitHub Actions (go / generate)

https://github.com/int128/kubelogin/actions/runs/12764870470

* Empty commit to trigger GitHub Actions

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: update-generated-files-action <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: int128-renovate-merge-bot[bot] <132176788+int128-renovate-merge-bot[bot]@users.noreply.github.com>
2025-01-14 12:14:38 +00:00
Hidetake Iwata
aa1f445672 Rename flag to --oidc-pkce-method and improve docs (#1240)
* Add --oidc-pkce-method and improve docs

* Fix lint

* Refactor

* Refactor
2025-01-14 09:57:19 +09:00
Hidetake Iwata
0c160f9db2 Refactor integration-test (#1242)
* Refactor integration-test

* Refactor
2025-01-13 16:29:27 +09:00
Hidetake Iwata
8c7903b2db Test PKCE by default (integration-test) (#1241) 2025-01-13 15:50:48 +09:00
Hidetake Iwata
898e8a12de Refactor PKCE implementation (#1239) 2025-01-12 21:41:20 +09:00
Hidetake Iwata
606f1cd0b6 Remove unused struct field (#1238) 2025-01-12 15:55:26 +09:00
Hidetake Iwata
562b998ca7 Add [SECURITY RISK] to insecure flag description (#1237) 2025-01-12 15:17:47 +09:00
Hidetake Iwata
6c9d198ef5 Add --token-cache-storage flag (#1236) 2025-01-12 14:55:46 +09:00
Hidetake Iwata
5ebecc534e Format markdown (#1235) 2025-01-12 14:00:03 +09:00
Hidetake Iwata
ca273c358d Refactor getDefaultTokenCacheDir() (#1234) 2025-01-12 13:36:28 +09:00
Hidetake Iwata
ccc6b772db Extract tokenCacheOptions (#1232)
* Extract tokenCacheOptions

* Refactor
2025-01-12 13:21:03 +09:00
Hidetake Iwata
1681d84fae Push container image on push event only (#1233) 2025-01-12 13:15:09 +09:00
Hidetake Iwata
6f62b25c40 Extract struct tokencache.Config (#1226) 2025-01-11 16:44:56 +09:00
renovate[bot]
71a7467e64 chore(deps): update docker/build-push-action action to v6.11.0 (#1228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 18:11:18 +00:00
renovate[bot]
5c78b7823b chore(deps): update docker/setup-qemu-action action to v3.3.0 (#1229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 16:09:57 +00:00
Hidetake Iwata
361c376c95 Enable keyring in system-test (#1225)
* Enable keyring in system-test

* Add dbus-run-session and gnome-keyring-daemon

* Fix

* Fix

* Refactor
2025-01-08 16:53:59 +09:00
Hidetake Iwata
c66570c030 Remove unused struct member (#1224) 2025-01-08 12:50:15 +09:00
kalle (jag)
afb25f511c Added key cache via OS keyring (#973)
* Added key cache via OS keyring

* Fix lint issue

* Disable keyring in integration tests

* Disable keyring in system test

---------

Co-authored-by: Hidetake Iwata <int128@gmail.com>
2025-01-08 12:32:26 +09:00
Hidetake Iwata
a836ef0e92 Do not push container image on fork (#1223) 2025-01-08 12:17:50 +09:00
87 changed files with 1468 additions and 781 deletions

34
.github/workflows/acceptance-test.yaml vendored Normal file
View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ Take a look at the diagram:
![Diagram of the credential plugin](docs/credential-plugin-diagram.svg)
## 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.

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ type Service interface {
Provider
IssuerURL() string
SetConfig(config testconfig.TestConfig)
SetConfig(config testconfig.Config)
LastTokenResponse() *TokenResponse
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
})

View File

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

View File

@@ -34,8 +34,6 @@ type Input struct {
GrantOptionSet GrantOptionSet
CachedTokenSet *oidc.TokenSet // optional
TLSClientConfig tlsclientconfig.Config
ForceRefresh bool
UseAccessToken bool
}
type GrantOptionSet struct {

View File

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

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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