Compare commits

...

31 Commits

Author SHA1 Message Date
Hidetake Iwata
c7ea97ff23 Refactor: remove test of make run (#199) 2019-12-23 18:02:15 +09:00
Hidetake Iwata
af18e734ea Set CGO_ENABLED=0 for static link (#198) 2019-12-23 17:55:43 +09:00
Hidetake Iwata
b5ae469b41 Create FUNDING.yml 2019-12-20 10:02:38 +09:00
Hidetake Iwata
94f480fdc9 Update README.md 2019-12-17 15:49:49 +09:00
Hidetake Iwata
7acb6e3a7b Refactor e2e tests (#196)
* Refactor: add e2e tests for credential plugin

* Refactor: extract assertCredentialPluginOutput()

* Refactor: add credential plugin test with TLS

* Refactor: extract helpers

* Refactor: rewrite TLS test cases

* Refactor: add test cases of token lifecycle
2019-12-17 11:07:43 +09:00
Hidetake Iwata
29e9c39a41 Update README.md 2019-12-12 10:16:47 +09:00
dependabot-preview[bot]
dd86168e4b Build(deps): bump github.com/google/wire from 0.3.0 to 0.4.0 (#195)
Bumps [github.com/google/wire](https://github.com/google/wire) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/google/wire/releases)
- [Commits](https://github.com/google/wire/compare/v0.3.0...v0.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-12 09:57:16 +09:00
dependabot-preview[bot]
1d48eab6b3 Build(deps): bump gopkg.in/yaml.v2 from 2.2.6 to 2.2.7 (#191)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.6 to 2.2.7.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.6...v2.2.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-27 23:34:24 +09:00
dependabot-preview[bot]
1e655a14b8 Build(deps): bump gopkg.in/yaml.v2 from 2.2.5 to 2.2.6 (#190)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.5 to 2.2.6.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.5...v2.2.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-19 13:45:08 -08:00
Hidetake Iwata
8a4d1f5169 Add LICENSE to distribution (#189) 2019-11-15 10:52:12 +09:00
Hidetake Iwata
6f417cd30c Add screencast (#186)
* Update README.md

* Update README.md

* Update README.md
2019-11-08 10:13:19 +09:00
dependabot-preview[bot]
7ba08f4254 Build(deps): bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#184)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.4...v2.2.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-05 21:37:20 +09:00
Hidetake Iwata
e778bbdadc Release v1.15.0 2019-11-01 11:45:25 +09:00
Hidetake Iwata
74108adf00 Update setup.md 2019-11-01 11:37:19 +09:00
Hidetake Iwata
0257b24156 Update README.md 2019-11-01 11:31:16 +09:00
Hidetake Iwata
b8c29985e7 Refactor (#183)
* Refactor: split authentication types

* Refactor: reduce responsibility of oidcclient package
2019-11-01 11:27:28 +09:00
Hidetake Iwata
4683a005c7 Add authorization code flow with keyboard interactive (#182) 2019-11-01 11:01:43 +09:00
Hidetake Iwata
cc48fb4cf7 Refactor: regenerate mocks with newer mockgen (#181) 2019-10-31 11:17:53 +09:00
Hidetake Iwata
ec7f7a062a Refactor: extract GrantOptionSet (#180) 2019-10-31 11:02:03 +09:00
Hidetake Iwata
e9ae98dfaf Fix nonce verification (#179)
fixup cf4e310b2e (#175)
2019-10-31 10:03:29 +09:00
Hidetake Iwata
0c582e97ad Add --grant-type option and username prompt for ROPC (#178) 2019-10-31 00:36:40 +09:00
Hidetake Iwata
5a71247214 Refactor: extract authentication options (#177)
* Refactor: extract authentication options

* Refactor: make subtests
2019-10-30 21:32:51 +09:00
Hidetake Iwata
4a084756c3 Add OAuth 2.0 PKCE support (#176) 2019-10-30 20:47:58 +09:00
Hidetake Iwata
cf4e310b2e Refactor: rename to oidcclient package and extract method (#175)
* Refactor: rename oidc package to oidcclient

* Refactor: extract parseToken method
2019-10-29 10:18:24 +09:00
Hidetake Iwata
4007e7f61a Refactor: extract jwtdecoder package (#174) 2019-10-29 09:55:29 +09:00
Hidetake Iwata
2700e439b9 Refactor: remove kubeconfig.OIDCConfig for single responsibility (#173)
* Refactor: remove kubeconfig.OIDCConfig for single responsibility

* fixup: add comments and rename methods

* fixup: fix methods name

* fixup: replace GetX509OrNil with SetRootCAs
2019-10-28 23:45:17 +09:00
Hidetake Iwata
dbf6238029 Refactor: rename auth package (#172) 2019-10-28 20:02:59 +09:00
Hidetake Iwata
93e893bc36 Refactor: replace ListenPort with BindAddress option (#171) 2019-10-28 19:59:45 +09:00
dependabot-preview[bot]
5dc06ae574 Bump github.com/int128/oauth2cli from 1.7.0 to 1.8.1 (#169)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.7.0 to 1.8.1.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.7.0...v1.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-26 14:04:23 +09:00
Hidetake Iwata
ab1023757b Add single page setup guide (#168) 2019-10-25 22:06:08 +09:00
Hidetake Iwata
e26dbd118e Add diagram of the credential plugin (#145)
* Update README.md

* Added credential-plugin-diagram.svg

* Update credential-plugin-diagram.svg
2019-10-24 20:26:08 +09:00
83 changed files with 3687 additions and 2561 deletions

View File

@@ -4,12 +4,11 @@ jobs:
docker:
- image: circleci/golang:1.13.3
steps:
- run: |
sudo apt install -y file
- run: |
mkdir -p ~/bin
echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
- run: |
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
chmod +x ~/bin/kubectl
- run: |
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.5.0/ghcp_linux_amd64
chmod +x ~/bin/ghcp
@@ -20,7 +19,11 @@ jobs:
- checkout
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make run
- run: make dist
- run: |
unzip dist/gh/kubelogin_linux_amd64.zip -d /tmp
file /tmp/kubelogin
/tmp/kubelogin --help
- run: |
if [ "$CIRCLE_TAG" ]; then
make release

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [int128] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,5 +1,4 @@
TARGET := kubelogin
TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
@@ -13,27 +12,26 @@ check:
$(TARGET): $(wildcard *.go)
go build -o $@ -ldflags "$(LDFLAGS)"
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
.PHONY: run
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
dist:
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
# make the zip files for GitHub Releases
VERSION=$(CIRCLE_TAG) CGO_ENABLED=0 goxzst -d dist/gh/ -i "LICENSE" -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
zipinfo dist/gh/kubelogin_linux_amd64.zip
# make the Homebrew formula
mv dist/gh/kubelogin.rb dist/
# make the yaml for krew-index
mkdir -p dist/plugins
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
.PHONY: release
release: dist
# publish to the GitHub Releases
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
# publish to the Homebrew tap repository
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
# fork krew-index and create a branch
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(CIRCLE_TAG)" -m "Bump oidc-login to $(CIRCLE_TAG)" -C dist/ plugins/oidc-login.yaml
.PHONY: clean
clean:
-rm $(TARGET)
-rm $(TARGET_PLUGIN)
-rm -r dist/

127
README.md
View File

@@ -2,14 +2,21 @@
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
This is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
Here is an example of Kubernetes authentication with the Google Identity Platform:
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/70971501-7bcebc80-20e4-11ea-8afc-539dcaea0aa8.gif" width="652" height="455">
Kubelogin is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
When you run kubectl, kubelogin opens the browser and you can log in to the provider.
Then kubelogin gets a token from the provider and kubectl access Kubernetes APIs with the token.
Take a look at the diagram:
![Diagram of the credential plugin](docs/credential-plugin-diagram.svg)
## Getting Started
### Install
### Setup
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
@@ -21,26 +28,32 @@ brew install int128/kubelogin/kubelogin
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.2/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.15.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
### Setup
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
The kubeconfig looks like:
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
See the following documents for more:
- [Getting Started with Google Identity Platform](docs/google.md)
- [Getting Started with dex and GitHub](docs/dex.md)
- [Getting Started with Keycloak](docs/keycloak.md)
Run the following command to show the setup instruction.
```sh
kubectl oidc-login setup
```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
```
See [the setup guide](docs/setup.md) for more.
### Run
Run kubectl.
@@ -59,7 +72,6 @@ After authentication, kubelogin returns the credentials to kubectl and finally k
```
% kubectl get pods
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
@@ -70,9 +82,25 @@ 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 reauthentication.
### Troubleshoot
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
Kubelogin will perform authentication if the token cache file does not exist.
You can dump the claims of token by passing `-v1` option.
```
I1212 10:14:17.754394 2517 get_token.go:91] the ID token has the claim: sub=********
I1212 10:14:17.754434 2517 get_token.go:91] the ID token has the claim: at_hash=********
I1212 10:14:17.754449 2517 get_token.go:91] the ID token has the claim: nonce=********
I1212 10:14:17.754459 2517 get_token.go:91] the ID token has the claim: iat=1576113256
I1212 10:14:17.754467 2517 get_token.go:91] the ID token has the claim: exp=1576116856
I1212 10:14:17.754484 2517 get_token.go:91] the ID token has the claim: iss=https://accounts.google.com
I1212 10:14:17.754497 2517 get_token.go:91] the ID token has the claim: azp=********.apps.googleusercontent.com
I1212 10:14:17.754506 2517 get_token.go:91] the ID token has the claim: aud=********.apps.googleusercontent.com
```
## Usage
@@ -93,13 +121,14 @@ 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
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
-h, --help help for get-token
Global Flags:
@@ -157,27 +186,65 @@ You need to register the following redirect URIs to the provider:
You can change the ports by the option:
```yaml
- --listen-port 12345
- --listen-port 23456
- --listen-port=12345
- --listen-port=23456
```
#### Authorization code flow with keyboard interactive
If you cannot access the browser, instead use the authorization code flow with keyboard interactive.
```yaml
- --grant-type=authcode-keyboard
```
Kubelogin will show the URL and prompt.
Open the URL in the browser and then copy the code shown.
```
% kubectl get pods
Open https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=...
Enter code: YOUR_CODE
```
Note that this flow uses the redirect URI `urn:ietf:wg:oauth:2.0:oob` and
some OIDC providers do not support it.
#### Resource owner password credentials grant flow
As well as you can use the resource owner password credentials grant flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
Most OIDC providers do not support this flow.
Kubelogin performs the resource owner password credentials grant flow
when `--grant-type=password` or `--username` is set.
You can pass the username and password:
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.
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.
If the password is not set, kubelogin will show the prompt for the password.
```yaml
- --username=USERNAME
```
```
% kubelogin --username USER
% kubectl get pods
Password:
```
If the username is not set, kubelogin will show the prompt for the username and password.
```yaml
- --grant-type=password
```
```
% kubectl get pods
Username: foo
Password:
```
@@ -196,7 +263,7 @@ Feel free to open issues and pull requests for improving code and documents.
### Development
Go 1.12 or later is required.
Go 1.13 or later is required.
```sh
# Run lint and tests

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,131 +0,0 @@
# Getting Started with dex and GitHub
Prerequisite:
- You have a GitHub account.
- You have an administrator role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed.
## 1. Set up the OpenID Connect Provider
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
- Application name: (any)
- Homepage URL: `https://dex.example.com`
- Authorization callback URL: `https://dex.example.com/callback`
Deploy the [dex](https://github.com/dexidp/dex) with the following config:
```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
staticClients:
- id: YOUR_CLIENT_ID
name: Kubernetes
redirectURIs:
- http://localhost:8000
- http://localhost:18000
secret: YOUR_DEX_CLIENT_SECRET
```
## 2. Verify authentication
Run the following command:
```sh
kubectl oidc-login setup \
--oidc-issuer-url=https://dex.example.com \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
It will open the browser and you can log in to the provider.
## 3. Bind a role
Bind the `cluster-admin` role to you.
Apply the following manifest:
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: https://dex.example.com#YOUR_SUBJECT
```
As well as you can create a custom role and bind it.
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
```
--oidc-issuer-url=https://dex.example.com
--oidc-client-id=YOUR_CLIENT_ID
```
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://dex.example.com
oidcClientID: YOUR_CLIENT_ID
```
## 5. Set up the kubeconfig
Add the following user to the kubeconfig:
```yaml
users:
- name: google
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://dex.example.com
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -1,108 +0,0 @@
# Getting Started with Google Identity Platform
Prerequisite:
- You have a Google account.
- You have an administrator role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Set up the OpenID Connect Provider
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
## 2. Verify authentication
Run the following command:
```sh
kubectl oidc-login setup \
--oidc-issuer-url=https://accounts.google.com \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
It will open the browser and you can log in to the provider.
## 3. Bind a role
Bind the `cluster-admin` role to you.
Apply the following manifest:
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: https://accounts.google.com#YOUR_SUBJECT
```
As well as you can create a custom role and bind it.
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
```
--oidc-issuer-url=https://accounts.google.com
--oidc-client-id=YOUR_CLIENT_ID
```
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://accounts.google.com
oidcClientID: YOUR_CLIENT_ID
```
## 5. Set up the kubeconfig
Add the following user to the kubeconfig:
```yaml
users:
- name: google
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://accounts.google.com
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -1,123 +0,0 @@
# Getting Started with Keycloak
Prerequisite:
- You have an administrator role of the Keycloak realm.
- You have an administrator role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed.
## 1. Set up the OpenID Connect Provider
Open the 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)
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
You can associate client roles by adding the following mapper:
- Name: `groups`
- Mapper Type: `User Client Role`
- Client ID: `YOUR_CLIENT_ID`
- Client Role prefix: `kubernetes:`
- Token Claim Name: `groups`
- Add to ID token: on
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
## 2. Verify authentication
Run the following command:
```sh
kubectl oidc-login setup \
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
It will open the browser and you can log in to the provider.
## 3. Bind a role
Bind the `cluster-admin` role to you.
Apply the following manifest:
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: https://keycloak.example.com/auth/realms/YOUR_REALM#YOUR_SUBJECT
```
As well as you can create a custom role and bind it.
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
```
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
--oidc-client-id=YOUR_CLIENT_ID
```
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
oidcClientID: YOUR_CLIENT_ID
```
## 5. Set up the kubeconfig
Add the following user to the kubeconfig:
```yaml
users:
- name: google
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

234
docs/setup.md Normal file
View File

@@ -0,0 +1,234 @@
# Kubernetes OpenID Connection authentication
This document guides how to set up the Kubernetes OpenID Connect (OIDC) authentication.
Let's see the following steps:
1. Set up the OIDC provider
1. Verify authentication
1. Bind a cluster role
1. Set up the Kubernetes API server
1. Set up the kubeconfig
1. Verify cluster access
## 1. Set up the OIDC provider
### Google Identity Platform
You can log in with a Google account.
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
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
### Keycloak
You can log in with a user of Keycloak.
Make sure you have an administrator role of the Keycloak realm.
Open the Keycloak and create an OIDC client as follows:
- Client ID: `YOUR_CLIENT_ID`
- Valid Redirect URLs:
- `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:
- Name: `groups`
- Mapper Type: `User Client Role`
- Client ID: `YOUR_CLIENT_ID`
- Client Role prefix: `kubernetes:`
- Token Claim Name: `groups`
- Add to ID token: on
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
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
### Dex with GitHub
You can log in with a GitHub account.
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
- Application name: (any)
- Homepage URL: `https://dex.example.com`
- Authorization callback URL: `https://dex.example.com/callback`
Deploy the [dex](https://github.com/dexidp/dex) with the following config:
```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
staticClients:
- 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`
### Okta
You can log in with an Okta user.
Okta supports [the authorization code flow with PKCE](https://developer.okta.com/docs/guides/implement-auth-code-pkce/overview/)
and this section explains how to set up it.
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)
- 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
You do not need to set `YOUR_CLIENT_SECRET`.
## 2. Verify authentication
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
```
It will open the browser and you can log in to the provider.
Then it will show the instruction.
## 3. Bind a cluster role
In this tutorial, bind the `cluster-admin` role to you.
Apply the following manifest:
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: ISSUER_URL#YOUR_SUBJECT
```
```sh
kubectl apply -f oidc-cluster-admin.yaml
```
As well as you can create a custom cluster role and bind it.
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
```
--oidc-issuer-url=ISSUER_URL
--oidc-client-id=YOUR_CLIENT_ID
```
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
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 the following user to the kubeconfig:
```yaml
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=ISSUER_URL
- --oidc-client-id=YOUR_CLIENT_ID
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -102,12 +102,13 @@ Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)

View File

@@ -8,14 +8,17 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
// Run the integration tests of the credential plugin use-case.
@@ -25,8 +28,7 @@ import (
// 3. Open a request for the local server.
// 4. Verify the output.
//
func TestCmd_Run_CredentialPlugin(t *testing.T) {
timeout := 1 * time.Second
func TestCredentialPlugin(t *testing.T) {
cacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
@@ -37,48 +39,261 @@ func TestCmd_Run_CredentialPlugin(t *testing.T) {
}
}()
t.Run("NoTLS", func(t *testing.T) {
testCredentialPlugin(t, cacheDir, keys.None, nil)
})
t.Run("TLS", func(t *testing.T) {
testCredentialPlugin(t, cacheDir, keys.Server, []string{"--certificate-authority", keys.Server.CACertPath})
})
}
func testCredentialPlugin(t *testing.T, cacheDir string, idpTLS keys.Keys, extraArgs []string) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(gomock.Any()).
Do(func(out credentialplugin.Output) {
if out.Token != idToken {
t.Errorf("Token wants %s but %s", idToken, out.Token)
}
if out.Expiry != tokenExpiryFuture {
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
}
})
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
runGetTokenCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
credentialPluginInteraction,
"--skip-open-browser",
"--listen-port", "0",
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
)
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
setupMockIDPForROPC(service, serverURL, "openid", "USER", "PASS", idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
"--username", "USER",
"--password", "PASS",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
setupTokenCache(t, cacheDir, tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
}, tokencache.TokenCache{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
assertTokenCache(t, cacheDir, tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
}, tokencache.TokenCache{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
setupMockIDPForDiscovery(service, serverURL)
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(validIDToken, "NEW_REFRESH_TOKEN"), nil)
setupTokenCache(t, cacheDir, tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
}, tokencache.TokenCache{
IDToken: expiredIDToken,
RefreshToken: "VALID_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &validIDToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
assertTokenCache(t, cacheDir, tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
}, tokencache.TokenCache{
IDToken: validIDToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &validIDToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
setupTokenCache(t, cacheDir, tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
}, tokencache.TokenCache{
IDToken: expiredIDToken,
RefreshToken: "EXPIRED_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &validIDToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
assertTokenCache(t, cacheDir, tokencache.Key{
IssuerURL: serverURL,
ClientID: "kubernetes",
}, tokencache.TokenCache{
IDToken: validIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "email profile openid", &idToken)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
assertCredentialPluginOutput(t, credentialPluginInteraction, &idToken)
args := []string{
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
}
args = append(args, extraArgs...)
runGetTokenCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), credentialPluginInteraction, args)
})
}
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc auth.LocalServerReadyFunc, interaction credentialplugin.Interface, args ...string) {
func assertCredentialPluginOutput(t *testing.T, credentialPluginInteraction *mock_credentialplugin.MockInterface, idToken *string) {
credentialPluginInteraction.EXPECT().
Write(gomock.Any()).
Do(func(out credentialplugin.Output) {
if out.Token != *idToken {
t.Errorf("Token wants %s but %s", *idToken, out.Token)
}
if out.Expiry != tokenExpiryFuture {
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
}
})
}
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, interaction credentialplugin.Interface, args []string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, interaction)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "get-token", "--v=1"}, args...), "HEAD")
exitCode := cmd.Run(ctx, append([]string{
"kubelogin", "get-token",
"--v=1",
"--skip-open-browser",
"--listen-port", "0",
}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func setupTokenCache(t *testing.T, cacheDir string, k tokencache.Key, v tokencache.TokenCache) {
var r tokencache.Repository
err := r.Save(cacheDir, k, v)
if err != nil {
t.Errorf("could not set up the token cache: %s", err)
}
}
func assertTokenCache(t *testing.T, cacheDir string, k tokencache.Key, want tokencache.TokenCache) {
var r tokencache.Repository
v, err := r.FindByKey(cacheDir, k)
if err != nil {
t.Errorf("could not set up the token cache: %s", err)
}
if diff := cmp.Diff(&want, v); diff != "" {
t.Errorf("token cache mismatch: %s", diff)
}
}

91
e2e_test/helpers_test.go Normal file
View File

@@ -0,0 +1,91 @@
package e2e_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
var (
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
)
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: expiry.Unix(),
}
claims.Nonce = nonce
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func setupMockIDPForDiscovery(service *mock_idp.MockService, serverURL string) {
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
}
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
var nonce string
setupMockIDPForDiscovery(service, serverURL)
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
service.EXPECT().Exchange("YOUR_AUTH_CODE").
DoAndReturn(func(string) (*idp.TokenResponse, error) {
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
})
}
func setupMockIDPForROPC(service *mock_idp.MockService, serverURL, scope, username, password, idToken string) {
setupMockIDPForDiscovery(service, serverURL)
service.EXPECT().AuthenticatePassword(username, password, scope).
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
}
func openBrowserOnReadyFunc(t *testing.T, ctx context.Context, k keys.Keys) authentication.LocalServerReadyFunc {
return func(url string) {
client := http.Client{Transport: &http.Transport{TLSClientConfig: k.TLSConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}
}

View File

@@ -35,6 +35,7 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder {
// AuthenticateCode mocks base method
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
@@ -43,11 +44,13 @@ func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
// AuthenticateCode indicates an expected call of AuthenticateCode
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
}
// AuthenticatePassword mocks base method
func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
@@ -56,11 +59,13 @@ func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenR
// AuthenticatePassword indicates an expected call of AuthenticatePassword
func (mr *MockServiceMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockService)(nil).AuthenticatePassword), arg0, arg1, arg2)
}
// Discovery mocks base method
func (m *MockService) Discovery() *idp.DiscoveryResponse {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Discovery")
ret0, _ := ret[0].(*idp.DiscoveryResponse)
return ret0
@@ -68,11 +73,13 @@ func (m *MockService) Discovery() *idp.DiscoveryResponse {
// Discovery indicates an expected call of Discovery
func (mr *MockServiceMockRecorder) Discovery() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockService)(nil).Discovery))
}
// Exchange mocks base method
func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exchange", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
@@ -81,11 +88,13 @@ func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
// Exchange indicates an expected call of Exchange
func (mr *MockServiceMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockService)(nil).Exchange), arg0)
}
// GetCertificates mocks base method
func (m *MockService) GetCertificates() *idp.CertificatesResponse {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCertificates")
ret0, _ := ret[0].(*idp.CertificatesResponse)
return ret0
@@ -93,11 +102,13 @@ func (m *MockService) GetCertificates() *idp.CertificatesResponse {
// GetCertificates indicates an expected call of GetCertificates
func (mr *MockServiceMockRecorder) GetCertificates() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockService)(nil).GetCertificates))
}
// Refresh mocks base method
func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
@@ -106,5 +117,6 @@ func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
// Refresh indicates an expected call of Refresh
func (mr *MockServiceMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockService)(nil).Refresh), arg0)
}

View File

@@ -4,68 +4,64 @@ import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"golang.org/x/xerrors"
)
// TLSCACert is path to the CA certificate.
// This should be generated by Makefile before test.
const TLSCACert = "keys/testdata/ca.crt"
// Keys represents a pair of certificate and key.
type Keys struct {
CertPath string
KeyPath string
CACertPath string
TLSConfig *tls.Config
}
// TLSCACertAsBase64 is a base64 encoded string of TLSCACert.
var TLSCACertAsBase64 string
// None represents non-TLS.
var None Keys
// TLSCACertAsConfig is a TLS config including TLSCACert.
var TLSCACertAsConfig = &tls.Config{RootCAs: x509.NewCertPool()}
// TLSServerCert is path to the server certificate.
// This should be generated by Makefile before test.
const TLSServerCert = "keys/testdata/server.crt"
// TLSServerKey is path to the server key.
// This should be generated by Makefile before test.
const TLSServerKey = "keys/testdata/server.key"
// Server is a Keys for TLS server.
// These files should be generated by Makefile before test.
var Server = Keys{
CertPath: "keys/testdata/server.crt",
KeyPath: "keys/testdata/server.key",
CACertPath: "keys/testdata/ca.crt",
TLSConfig: newTLSConfig("keys/testdata/ca.crt"),
}
// JWSKey is path to the key for signing ID tokens.
// This file should be generated by Makefile before test.
const JWSKey = "keys/testdata/jws.key"
// JWSKeyPair is the key pair loaded from JWSKey.
var JWSKeyPair *rsa.PrivateKey
var JWSKeyPair = readPrivateKey(JWSKey)
func init() {
var err error
JWSKeyPair, err = readPrivateKey(JWSKey)
if err != nil {
panic(err)
}
b, err := ioutil.ReadFile(TLSCACert)
if err != nil {
panic(err)
}
TLSCACertAsBase64 = base64.StdEncoding.EncodeToString(b)
if !TLSCACertAsConfig.RootCAs.AppendCertsFromPEM(b) {
panic("could not append the CA cert")
}
}
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
func newTLSConfig(name string) *tls.Config {
b, err := ioutil.ReadFile(name)
if err != nil {
return nil, xerrors.Errorf("could not read JWSKey: %w", err)
panic(err)
}
p := x509.NewCertPool()
if !p.AppendCertsFromPEM(b) {
panic("could not append the CA cert")
}
return &tls.Config{RootCAs: p}
}
func readPrivateKey(name string) *rsa.PrivateKey {
b, err := ioutil.ReadFile(name)
if err != nil {
panic(err)
}
block, rest := pem.Decode(b)
if block == nil {
return nil, xerrors.New("could not decode PEM")
panic("could not decode PEM")
}
if len(rest) > 0 {
return nil, xerrors.New("PEM should contain single key but multiple keys")
panic("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Errorf("could not parse the key: %w", err)
panic(err)
}
return k, nil
return k
}

View File

@@ -8,6 +8,8 @@ import (
"net"
"net/http"
"testing"
"github.com/int128/kubelogin/e2e_test/keys"
)
type Shutdowner interface {
@@ -28,7 +30,15 @@ func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
}
// Start starts an authentication server.
func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
// If k is non-nil, it starts a TLS server.
func Start(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
if k == keys.None {
return startNoTLS(t, h)
}
return startTLS(t, h, k)
}
func startNoTLS(t *testing.T, h http.Handler) (string, Shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
@@ -44,8 +54,7 @@ func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
return url, &shutdowner{l, s}
}
// Start starts an authentication server with TLS.
func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Shutdowner) {
func startTLS(t *testing.T, h http.Handler, k keys.Keys) (string, Shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
@@ -53,7 +62,7 @@ func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Sh
Handler: h,
}
go func() {
err := s.ServeTLS(l, cert, key)
err := s.ServeTLS(l, k.CertPath, k.KeyPath)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}

View File

@@ -2,13 +2,10 @@ package e2e_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
@@ -17,12 +14,7 @@ import (
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases/auth"
)
var (
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
// Run the integration tests of the Login use-case.
@@ -32,191 +24,19 @@ var (
// 3. Open a request for the local server.
// 4. Verify the kubeconfig.
//
func TestCmd_Run_Standalone(t *testing.T) {
func TestStandalone(t *testing.T) {
t.Run("NoTLS", func(t *testing.T) {
testStandalone(t, keys.None)
})
t.Run("TLS", func(t *testing.T) {
testStandalone(t, keys.Server)
})
}
func testStandalone(t *testing.T, idpTLS keys.Keys) {
timeout := 5 * time.Second
type testParameter struct {
startServer func(t *testing.T, h http.Handler) (string, localserver.Shutdowner)
kubeconfigIDPCertificateAuthority string
clientTLSConfig *tls.Config
}
testParameters := map[string]testParameter{
"NoTLS": {
startServer: localserver.Start,
},
"CACert": {
startServer: func(t *testing.T, h http.Handler) (string, localserver.Shutdowner) {
return localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, h)
},
kubeconfigIDPCertificateAuthority: keys.TLSCACert,
clientTLSConfig: keys.TLSCACertAsConfig,
},
}
runTest := func(t *testing.T, p testParameter) {
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
}
for name, p := range testParameters {
t.Run(name, func(t *testing.T) {
runTest(t, p)
})
}
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
@@ -224,19 +44,179 @@ func TestCmd_Run_Standalone(t *testing.T) {
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
setupMockIDPForROPC(service, serverURL, "openid", "USER", "PASS", idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
"--username", "USER",
"--password", "PASS",
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("env_KUBECONFIG", func(t *testing.T) {
// do not run this in parallel due to change of the env var
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
"--skip-open-browser", "--listen-port", "0")
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -251,20 +231,22 @@ func TestCmd_Run_Standalone(t *testing.T) {
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
serverURL, server := localserver.Start(t, idp.NewHandler(t, service), idpTLS)
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "profile groups openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
ExtraScopes: "profile,groups",
Issuer: serverURL,
ExtraScopes: "profile,groups",
IDPCertificateAuthority: idpTLS.CACertPath,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
args := []string{
"--kubeconfig", kubeConfigFilename,
}
runRootCmd(t, ctx, openBrowserOnReadyFunc(t, ctx, idpTLS), args)
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -272,76 +254,20 @@ func TestCmd_Run_Standalone(t *testing.T) {
})
}
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: expiry.Unix(),
}
claims.Nonce = nonce
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
var nonce string
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
service.EXPECT().Exchange("YOUR_AUTH_CODE").
DoAndReturn(func(string) (*idp.TokenResponse, error) {
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
})
}
func runCmd(t *testing.T, ctx context.Context, localServerReadyFunc auth.LocalServerReadyFunc, args ...string) {
func runRootCmd(t *testing.T, ctx context.Context, localServerReadyFunc authentication.LocalServerReadyFunc, args []string) {
t.Helper()
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, nil)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
exitCode := cmd.Run(ctx, append([]string{
"kubelogin",
"--v=1",
"--listen-port", "0",
"--skip-open-browser",
}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func openBrowserOnReadyFunc(t *testing.T, ctx context.Context, clientConfig *tls.Config) auth.LocalServerReadyFunc {
return func(url string) {
client := http.Client{Transport: &http.Transport{TLSClientConfig: clientConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {

7
go.mod
View File

@@ -7,8 +7,9 @@ require (
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda
github.com/go-test/deep v1.0.4
github.com/golang/mock v1.3.1
github.com/google/wire v0.3.0
github.com/int128/oauth2cli v1.7.0
github.com/google/go-cmp v0.3.1
github.com/google/wire v0.4.0
github.com/int128/oauth2cli v1.8.1
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v0.0.5
@@ -18,7 +19,7 @@ require (
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v2 v2.2.7
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
k8s.io/klog v0.4.0

14
go.sum
View File

@@ -31,12 +31,16 @@ github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Z
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60=
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@@ -47,8 +51,12 @@ github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/int128/listener v1.0.0 h1:a9H3m4jbXgXpxJUK3fxWrh37Iic/UU/kYOGE0WtjbbI=
github.com/int128/listener v1.0.0/go.mod h1:sho0rrH7mNRRZH4hYOYx+xwRDGmtRndaUiu2z9iumes=
github.com/int128/oauth2cli v1.7.0 h1:lguQEIJ4IcSFRTqQ6y7avnfvPqVe0U6dlkW8mC1Epts=
github.com/int128/oauth2cli v1.7.0/go.mod h1:bucNn0/es9IhOf0a2MWPvJ5xO5f6JYrCfitQTyjI5lA=
github.com/int128/oauth2cli v1.8.1 h1:Vkmfx0w225l4qUpJ1ZWGw1elw7hnXAybSiYoYyh1iBw=
github.com/int128/oauth2cli v1.8.1/go.mod h1:MkxKWhHUaPOaq/92Z5ifdCWySAKJKo04hUXaKA7OgDE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -144,6 +152,12 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.6 h1:97YCGUei5WVbkKfogoJQsLwUJ17cWvpLrgNvlcbxikE=
gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
k8s.io/api v0.0.0-20190620084959-7cf5895f2711 h1:BblVYz/wE5WtBsD/Gvu54KyBUTJMflolzc5I2DTvh50=
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9L4wrirlwYJ6w88jUQxTUw=

View File

@@ -30,8 +30,10 @@ spec:
sha256: "{{ sha256 .linux_amd64_archive }}"
bin: kubelogin
files:
- from: "kubelogin"
to: "."
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
@@ -40,8 +42,10 @@ spec:
sha256: "{{ sha256 .darwin_amd64_archive }}"
bin: kubelogin
files:
- from: "kubelogin"
to: "."
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: darwin
@@ -50,8 +54,10 @@ spec:
sha256: "{{ sha256 .windows_amd64_archive }}"
bin: kubelogin.exe
files:
- from: "kubelogin.exe"
to: "."
- from: kubelogin.exe
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: windows

View File

@@ -0,0 +1,76 @@
// Package certpool provides loading certificates from files or base64 encoded string.
package certpool
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool FactoryInterface,Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
wire.Struct(new(CertPool), "*"),
wire.Bind(new(Interface), new(*CertPool)),
)
type FactoryInterface interface {
New() Interface
}
type Factory struct{}
// New returns an instance which implements the Interface.
func (f *Factory) New() Interface {
return &CertPool{pool: x509.NewCertPool()}
}
type Interface interface {
AddFile(filename string) error
AddBase64Encoded(s string) error
SetRootCAs(cfg *tls.Config)
}
// CertPool represents a pool of certificates.
type CertPool struct {
pool *x509.CertPool
}
// SetRootCAs sets cfg.RootCAs if it has any certificate.
// Otherwise do nothing.
func (p *CertPool) SetRootCAs(cfg *tls.Config) {
if len(p.pool.Subjects()) > 0 {
cfg.RootCAs = p.pool
}
}
// AddFile loads the certificate from the file.
func (p *CertPool) AddFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
// AddBase64Encoded loads the certificate from the base64 encoded string.
func (p *CertPool) AddBase64Encoded(s string) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,62 @@
package certpool
import (
"crypto/tls"
"io/ioutil"
"testing"
)
func TestCertPool_AddFile(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
var f Factory
p := f.New()
if err := p.AddFile("testdata/ca1.crt"); err != nil {
t.Errorf("AddFile error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
})
t.Run("Invalid", func(t *testing.T) {
var f Factory
p := f.New()
err := p.AddFile("testdata/Makefile")
if err == nil {
t.Errorf("AddFile wants an error but was nil")
}
})
}
func TestCertPool_AddBase64Encoded(t *testing.T) {
var f Factory
p := f.New()
if err := p.AddBase64Encoded(readFile(t, "testdata/ca2.crt.base64")); err != nil {
t.Errorf("AddBase64Encoded error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
}
func TestCertPool_SetRootCAs(t *testing.T) {
var f Factory
p := f.New()
var cfg tls.Config
p.SetRootCAs(&cfg)
if cfg.RootCAs != nil {
t.Errorf("cfg.RootCAs wants nil but was %+v", cfg.RootCAs)
}
}
func readFile(t *testing.T, filename string) string {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return string(b)
}

View File

@@ -0,0 +1,112 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/certpool (interfaces: FactoryInterface,Interface)
// Package mock_certpool is a generated GoMock package.
package mock_certpool
import (
tls "crypto/tls"
gomock "github.com/golang/mock/gomock"
certpool "github.com/int128/kubelogin/pkg/adaptors/certpool"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New() certpool.Interface {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New")
ret0, _ := ret[0].(certpool.Interface)
return ret0
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New))
}
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AddBase64Encoded mocks base method
func (m *MockInterface) AddBase64Encoded(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddBase64Encoded", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddBase64Encoded indicates an expected call of AddBase64Encoded
func (mr *MockInterfaceMockRecorder) AddBase64Encoded(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBase64Encoded", reflect.TypeOf((*MockInterface)(nil).AddBase64Encoded), arg0)
}
// AddFile mocks base method
func (m *MockInterface) AddFile(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddFile", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddFile indicates an expected call of AddFile
func (mr *MockInterfaceMockRecorder) AddFile(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFile", reflect.TypeOf((*MockInterface)(nil).AddFile), arg0)
}
// SetRootCAs mocks base method
func (m *MockInterface) SetRootCAs(arg0 *tls.Config) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetRootCAs", arg0)
}
// SetRootCAs indicates an expected call of SetRootCAs
func (mr *MockInterfaceMockRecorder) SetRootCAs(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRootCAs", reflect.TypeOf((*MockInterface)(nil).SetRootCAs), arg0)
}

View File

@@ -2,6 +2,7 @@ package cmd
import (
"context"
"fmt"
"path/filepath"
"github.com/google/wire"
@@ -26,6 +27,13 @@ type Interface interface {
var defaultListenPort = []int{8000, 18000}
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
func translateListenPortToBindAddress(ports []int) (address []string) {
for _, p := range ports {
address = append(address, fmt.Sprintf("127.0.0.1:%d", p))
}
return
}
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Root *Root

View File

@@ -6,6 +6,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
@@ -16,213 +17,314 @@ func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("login/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
ListenPort: defaultListenPort,
t.Run("root", func(t *testing.T) {
tests := map[string]struct {
args []string
in standalone.Input
}{
"Defaults": {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
},
},
},
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"--grant-type", "authcode-keyboard",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"--grant-type", "password",
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
t.Run("login/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
t.Run("get-token", func(t *testing.T) {
tests := map[string]struct {
args []string
in credentialplugin.Input
}{
"Defaults": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
},
},
},
},
"FullOptions": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "authcode-keyboard",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "password",
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
ListenPort: defaultListenPort,
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
})
cmd := Cmd{
Root: &Root{
t.Run("MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
})
cmd := Cmd{
Root: &Root{
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("get-token/MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
}

View File

@@ -12,17 +12,14 @@ import (
// getTokenOptions represents the options for get-token command.
type getTokenOptions struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
CertificateAuthority string
SkipTLSVerify bool
TokenCacheDir string
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
TokenCacheDir string
authenticationOptions authenticationOptions
}
func (o *getTokenOptions) register(f *pflag.FlagSet) {
@@ -31,13 +28,10 @@ func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
o.authenticationOptions.register(f)
}
type GetToken struct {
@@ -63,18 +57,19 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
return nil
},
RunE: func(*cobra.Command, []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("error: %w", err)
}
in := credentialplugin.Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
TokenCacheDir: o.TokenCacheDir,
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
}
if err := cmd.GetToken.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)

View File

@@ -2,9 +2,12 @@ package cmd
import (
"context"
"fmt"
"strings"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -23,15 +26,12 @@ See https://github.com/int128/kubelogin for more.
// rootOptions represents the options for the root command.
type rootOptions struct {
Kubeconfig string
Context string
User string
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
CertificateAuthority string
SkipTLSVerify bool
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
authenticationOptions authenticationOptions
}
func (o *rootOptions) register(f *pflag.FlagSet) {
@@ -39,12 +39,52 @@ func (o *rootOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
}
type authenticationOptions struct {
GrantType string
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
var allGrantType = strings.Join([]string{
"auto",
"authcode",
"authcode-keyboard",
"password",
}, "|")
func (o *authenticationOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("The authorization grant type to use. One of (%s)", allGrantType))
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
}
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeOption = &authentication.AuthCodeOption{
BindAddress: translateListenPortToBindAddress(o.ListenPort),
SkipOpenBrowser: o.SkipOpenBrowser,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &authentication.ROPCOption{
Username: o.Username,
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}
type Root struct {
@@ -60,16 +100,17 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
Long: longDescription,
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("invalid option: %w", err)
}
in := standalone.Input{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
GrantOptionSet: grantOptionSet,
}
if err := cmd.Standalone.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)

View File

@@ -2,7 +2,6 @@ package cmd
import (
"context"
"reflect"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/spf13/cobra"
@@ -12,14 +11,13 @@ import (
// setupOptions represents the options for setup command.
type setupOptions struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
ListenPort []int
SkipOpenBrowser bool
CertificateAuthority string
SkipTLSVerify bool
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
authenticationOptions authenticationOptions
}
func (o *setupOptions) register(f *pflag.FlagSet) {
@@ -28,10 +26,9 @@ func (o *setupOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
}
type Setup struct {
@@ -44,17 +41,22 @@ func (cmd *Setup) New(ctx context.Context) *cobra.Command {
Use: "setup",
Short: "Show the setup instruction",
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("error: %w", err)
}
in := setup.Stage2Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
SkipOpenBrowser: o.SkipOpenBrowser,
ListenPort: o.ListenPort,
ListenPortIsSet: !reflect.DeepEqual(o.ListenPort, defaultListenPort),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
}
if c.Flags().Lookup("listen-port").Changed {
in.ListenPortArgs = o.authenticationOptions.ListenPort
}
if in.IssuerURL == "" || in.ClientID == "" {
cmd.Setup.DoStage1()

View File

@@ -35,6 +35,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// Write mocks base method
func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -42,5 +43,6 @@ func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
// Write indicates an expected call of Write
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)
}

View File

@@ -1,8 +1,11 @@
// Package env provides environment dependent facilities.
package env
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"github.com/google/wire"
@@ -27,6 +30,7 @@ var Set = wire.NewSet(
)
type Interface interface {
ReadString(prompt string) (string, error)
ReadPassword(prompt string) (string, error)
OpenBrowser(url string) error
}
@@ -34,6 +38,20 @@ type Interface interface {
// Env provides environment specific facilities.
type Env struct{}
// ReadString reads a string from the stdin.
func (*Env) ReadString(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("could not write the prompt: %w", err)
}
r := bufio.NewReader(os.Stdin)
s, err := r.ReadString('\n')
if err != nil {
return "", xerrors.Errorf("could not read from stdin: %w", err)
}
s = strings.TrimRight(s, "\r\n")
return s, nil
}
// ReadPassword reads a password from the stdin without echo back.
func (*Env) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
@@ -41,7 +59,7 @@ func (*Env) ReadPassword(prompt string) (string, error) {
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", xerrors.Errorf("could not read: %w", err)
return "", xerrors.Errorf("could not read from stdin: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", xerrors.Errorf("could not write a new line: %w", err)

View File

@@ -34,6 +34,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// OpenBrowser mocks base method
func (m *MockInterface) OpenBrowser(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -41,11 +42,13 @@ func (m *MockInterface) OpenBrowser(arg0 string) error {
// OpenBrowser indicates an expected call of OpenBrowser
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
}
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
@@ -54,5 +57,21 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}
// ReadString mocks base method
func (m *MockInterface) ReadString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadString", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadString indicates an expected call of ReadString
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)
}

View File

@@ -1,4 +1,5 @@
package oidc
// Package jwtdecoder provides decoding a JWT.
package jwtdecoder
import (
"bytes"
@@ -8,25 +9,35 @@ import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/google/wire"
"golang.org/x/xerrors"
)
type DecoderInterface interface {
DecodeIDToken(t string) (*DecodedIDToken, error)
//go:generate mockgen -destination mock_jwtdecoder/mock_jwtdecoder.go github.com/int128/kubelogin/pkg/adaptors/jwtdecoder Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Struct(new(Decoder), "*"),
wire.Bind(new(Interface), new(*Decoder)),
)
type Interface interface {
Decode(s string) (*Claims, error)
}
type DecodedIDToken struct {
// Claims represents claims of a token.
type Claims struct {
Subject string
Expiry time.Time
Claims map[string]string // string representation of claims for logging
Pretty map[string]string // string representation for debug and logging
}
type Decoder struct{}
// DecodeIDToken returns the claims of the ID token.
// Decode returns the claims of the JWT.
// Note that this method does not verify the signature and always trust it.
func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
parts := strings.Split(t, ".")
func (d *Decoder) Decode(s string) (*Claims, error) {
parts := strings.Split(s, ".")
if len(parts) != 3 {
return nil, xerrors.Errorf("token contains an invalid number of segments")
}
@@ -42,10 +53,10 @@ func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&rawClaims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
return &DecodedIDToken{
return &Claims{
Subject: claims.Subject,
Expiry: time.Unix(claims.ExpiresAt, 0),
Claims: dumpRawClaims(rawClaims),
Pretty: dumpRawClaims(rawClaims),
}, nil
}

View File

@@ -1,4 +1,4 @@
package oidc
package jwtdecoder
import (
"crypto/rsa"
@@ -11,23 +11,23 @@ import (
"github.com/dgrijalva/jwt-go"
)
func TestDecoder_DecodeIDToken(t *testing.T) {
func TestDecoder_Decode(t *testing.T) {
var decoder Decoder
t.Run("ValidToken", func(t *testing.T) {
expiry := time.Now().Round(time.Second)
idToken := newIDToken(t, "https://issuer.example.com", expiry)
decodedToken, err := decoder.DecodeIDToken(idToken)
decodedToken, err := decoder.Decode(idToken)
if err != nil {
t.Fatalf("DecodeIDToken error: %s", err)
t.Fatalf("Decode error: %s", err)
}
if decodedToken.Expiry != expiry {
t.Errorf("Expiry wants %s but %s", expiry, decodedToken.Expiry)
}
t.Logf("Claims=%+v", decodedToken.Claims)
t.Logf("Pretty=%+v", decodedToken.Pretty)
})
t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
decodedToken, err := decoder.Decode("HEADER.INVALID_TOKEN.SIGNATURE")
if err == nil {
t.Errorf("error wants non-nil but nil")
} else {

View File

@@ -0,0 +1,49 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/jwtdecoder (interfaces: Interface)
// Package mock_jwtdecoder is a generated GoMock package.
package mock_jwtdecoder
import (
gomock "github.com/golang/mock/gomock"
jwtdecoder "github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Decode mocks base method
func (m *MockInterface) Decode(arg0 string) (*jwtdecoder.Claims, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Decode", arg0)
ret0, _ := ret[0].(*jwtdecoder.Claims)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Decode indicates an expected call of Decode
func (mr *MockInterfaceMockRecorder) Decode(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decode", reflect.TypeOf((*MockInterface)(nil).Decode), arg0)
}

View File

@@ -2,6 +2,7 @@ package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
//go:generate mockgen -destination mock_kubeconfig/mock_kubeconfig.go github.com/int128/kubelogin/pkg/adaptors/kubeconfig Interface
@@ -26,22 +27,19 @@ type UserName string
// AuthProvider represents the authentication provider,
// i.e. context, user and auth-provider in a kubeconfig.
type AuthProvider struct {
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // Context name (optional)
OIDCConfig OIDCConfig
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // (optional) Context name
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id
ClientSecret string // (optional) client-secret
IDPCertificateAuthority string // (optional) idp-certificate-authority
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
ExtraScopes []string // (optional) extra-scopes
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
}
// OIDCConfig represents a configuration of an OIDC provider.
type OIDCConfig struct {
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id
ClientSecret string // client-secret
IDPCertificateAuthority string // (optional) idp-certificate-authority
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
ExtraScopes []string // (optional) extra-scopes
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
type Kubeconfig struct {
Logger logger.Interface
}
type Kubeconfig struct{}

View File

@@ -58,20 +58,16 @@ func findCurrentAuthProvider(config *api.Config, contextName ContextName, userNa
if userNode.AuthProvider.Config == nil {
return nil, xerrors.New("auth-provider.config is missing")
}
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
OIDCConfig: makeOIDCConfig(userNode.AuthProvider.Config),
}, nil
}
func makeOIDCConfig(m map[string]string) OIDCConfig {
m := userNode.AuthProvider.Config
var extraScopes []string
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return OIDCConfig{
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
IDPIssuerURL: m["idp-issuer-url"],
ClientID: m["client-id"],
ClientSecret: m["client-secret"],
@@ -80,5 +76,5 @@ func makeOIDCConfig(m map[string]string) OIDCConfig {
ExtraScopes: extraScopes,
IDToken: m["id-token"],
RefreshToken: m["refresh-token"],
}
}, nil
}

View File

@@ -106,19 +106,17 @@ func Test_findCurrentAuthProvider(t *testing.T) {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
@@ -151,9 +149,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
IDPIssuerURL: "https://accounts.google.com",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
@@ -180,9 +176,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
IDPIssuerURL: "https://accounts.google.com",
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)

View File

@@ -35,6 +35,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// GetCurrentAuthProvider mocks base method
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
ret1, _ := ret[1].(error)
@@ -43,11 +44,13 @@ func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.Cont
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuthProvider mocks base method
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -55,5 +58,6 @@ func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
}

View File

@@ -7,14 +7,14 @@ import (
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuthProvider(auth *AuthProvider) error {
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
func (*Kubeconfig) UpdateAuthProvider(p *AuthProvider) error {
config, err := clientcmd.LoadFromFile(p.LocationOfOrigin)
if err != nil {
return xerrors.Errorf("could not load %s: %w", auth.LocationOfOrigin, err)
return xerrors.Errorf("could not load %s: %w", p.LocationOfOrigin, err)
}
userNode, ok := config.AuthInfos[string(auth.UserName)]
userNode, ok := config.AuthInfos[string(p.UserName)]
if !ok {
return xerrors.Errorf("user %s does not exist", auth.UserName)
return xerrors.Errorf("user %s does not exist", p.UserName)
}
if userNode.AuthProvider == nil {
return xerrors.Errorf("auth-provider is missing")
@@ -22,23 +22,23 @@ func (*Kubeconfig) UpdateAuthProvider(auth *AuthProvider) error {
if userNode.AuthProvider.Name != "oidc" {
return xerrors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
}
copyOIDCConfig(auth.OIDCConfig, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, auth.LocationOfOrigin); err != nil {
return xerrors.Errorf("could not update %s: %w", auth.LocationOfOrigin, err)
copyAuthProviderConfig(p, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, p.LocationOfOrigin); err != nil {
return xerrors.Errorf("could not update %s: %w", p.LocationOfOrigin, err)
}
return nil
}
func copyOIDCConfig(config OIDCConfig, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", config.IDPIssuerURL)
setOrDeleteKey(m, "client-id", config.ClientID)
setOrDeleteKey(m, "client-secret", config.ClientSecret)
setOrDeleteKey(m, "idp-certificate-authority", config.IDPCertificateAuthority)
setOrDeleteKey(m, "idp-certificate-authority-data", config.IDPCertificateAuthorityData)
extraScopes := strings.Join(config.ExtraScopes, ",")
func copyAuthProviderConfig(p *AuthProvider, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", p.IDPIssuerURL)
setOrDeleteKey(m, "client-id", p.ClientID)
setOrDeleteKey(m, "client-secret", p.ClientSecret)
setOrDeleteKey(m, "idp-certificate-authority", p.IDPCertificateAuthority)
setOrDeleteKey(m, "idp-certificate-authority-data", p.IDPCertificateAuthorityData)
extraScopes := strings.Join(p.ExtraScopes, ",")
setOrDeleteKey(m, "extra-scopes", extraScopes)
setOrDeleteKey(m, "id-token", config.IDToken)
setOrDeleteKey(m, "refresh-token", config.RefreshToken)
setOrDeleteKey(m, "id-token", p.IDToken)
setOrDeleteKey(m, "refresh-token", p.RefreshToken)
}
func setOrDeleteKey(m map[string]string, key, value string) {

View File

@@ -19,13 +19,11 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
@@ -65,18 +63,16 @@ users:
}
}()
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
LocationOfOrigin: f.Name(),
UserName: "google",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}

View File

@@ -1,162 +0,0 @@
package oidc
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type Interface interface {
AuthenticateByCode(ctx context.Context, localServerPort []int, localServerReadyChan chan<- string) (*TokenSet, error)
AuthenticateByPassword(ctx context.Context, username, password string) (*TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
}
// TokenSet represents an output DTO of
// Interface.AuthenticateByCode, Interface.AuthenticateByPassword and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenSubject string
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
}
func (c *client) wrapContext(ctx context.Context) context.Context {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
return ctx
}
// AuthenticateByCode performs the authorization code flow.
func (c *client) AuthenticateByCode(ctx context.Context, localServerPort []int, localServerReadyChan chan<- string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
nonce, err := newNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce parameter")
}
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
LocalServerPort: localServerPort,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
LocalServerReadyChan: localServerReadyChan,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
if verifiedIDToken.Nonce != nonce {
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func newNonce() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", xerrors.Errorf("error while reading random: %w", err)
}
return fmt.Sprintf("%x", n), nil
}
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(ctx context.Context, username, password string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: refreshToken,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
return dumpRawClaims(rawClaims), err
}

View File

@@ -1,122 +0,0 @@
package oidc
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type FactoryInterface interface {
New(ctx context.Context, config ClientConfig) (Interface, error)
}
// ClientConfig represents a configuration of an Interface to create.
type ClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
SkipTLSVerify bool
}
type Factory struct {
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, config ClientConfig) (Interface, error) {
tlsConfig, err := f.tlsConfigFor(config)
if err != nil {
return nil, xerrors.Errorf("could not initialize TLS config: %w", err)
}
baseTransport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.Config.IDPIssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDCFactory issuer: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.Config.ClientID,
ClientSecret: config.Config.ClientSecret,
Scopes: append(config.Config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
}, nil
}
func (f *Factory) tlsConfigFor(config ClientConfig) (*tls.Config, error) {
pool := x509.NewCertPool()
if config.Config.IDPCertificateAuthority != "" {
f.Logger.V(1).Infof("loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if config.Config.IDPCertificateAuthorityData != "" {
f.Logger.V(1).Infof("loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if config.CACertFilename != "" {
f.Logger.V(1).Infof("loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
c := &tls.Config{
InsecureSkipVerify: config.SkipTLSVerify,
}
if len(pool.Subjects()) > 0 {
c.RootCAs = pool
}
return c, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -1,88 +0,0 @@
package oidc
import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
)
func TestFactory_tlsConfigFor(t *testing.T) {
testingLogger := mock_logger.New(t)
factory := &Factory{Logger: testingLogger}
t.Run("Defaults", func(t *testing.T) {
c, err := factory.tlsConfigFor(ClientConfig{})
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants false but true")
}
if c.RootCAs != nil {
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
config := ClientConfig{
SkipTLSVerify: true,
}
c, err := factory.tlsConfigFor(config)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if !c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants true but false")
}
if c.RootCAs != nil {
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
}
})
t.Run("AllCertificates", func(t *testing.T) {
config := ClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/tls/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/tls/ca2.crt.base64")),
},
CACertFilename: "testdata/tls/ca3.crt",
}
c, err := factory.tlsConfigFor(config)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants false but true")
}
if c.RootCAs == nil {
t.Fatalf("RootCAs wants non-nil but nil")
}
subjects := c.RootCAs.Subjects()
if len(subjects) != 3 {
t.Errorf("len(subjects) wants 3 but %d", len(subjects))
}
})
t.Run("InvalidCertificate", func(t *testing.T) {
config := ClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/tls/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/tls/ca2.crt.base64")),
},
CACertFilename: "testdata/Makefile", // invalid cert
}
_, err := factory.tlsConfigFor(config)
if err == nil {
t.Fatalf("NewConfig wants non-nil but nil")
}
t.Logf("expected error: %+v", err)
})
}
func readFile(t *testing.T, filename string) []byte {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return b
}

View File

@@ -1,146 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidc (interfaces: FactoryInterface,Interface,DecoderInterface)
// Package mock_oidc is a generated GoMock package.
package mock_oidc
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidc "github.com/int128/kubelogin/pkg/adaptors/oidc"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New(arg0 context.Context, arg1 oidc.ClientConfig) (oidc.Interface, error) {
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(oidc.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New), arg0, arg1)
}
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AuthenticateByCode mocks base method
func (m *MockInterface) AuthenticateByCode(arg0 context.Context, arg1 []int, arg2 chan<- string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByCode indicates an expected call of AuthenticateByCode
func (mr *MockInterfaceMockRecorder) AuthenticateByCode(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockInterface)(nil).AuthenticateByCode), arg0, arg1, arg2)
}
// AuthenticateByPassword mocks base method
func (m *MockInterface) AuthenticateByPassword(arg0 context.Context, arg1, arg2 string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
func (mr *MockInterfaceMockRecorder) AuthenticateByPassword(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockInterface)(nil).AuthenticateByPassword), arg0, arg1, arg2)
}
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}
// MockDecoderInterface is a mock of DecoderInterface interface
type MockDecoderInterface struct {
ctrl *gomock.Controller
recorder *MockDecoderInterfaceMockRecorder
}
// MockDecoderInterfaceMockRecorder is the mock recorder for MockDecoderInterface
type MockDecoderInterfaceMockRecorder struct {
mock *MockDecoderInterface
}
// NewMockDecoderInterface creates a new mock instance
func NewMockDecoderInterface(ctrl *gomock.Controller) *MockDecoderInterface {
mock := &MockDecoderInterface{ctrl: ctrl}
mock.recorder = &MockDecoderInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDecoderInterface) EXPECT() *MockDecoderInterfaceMockRecorder {
return m.recorder
}
// DecodeIDToken mocks base method
func (m *MockDecoderInterface) DecodeIDToken(arg0 string) (*oidc.DecodedIDToken, error) {
ret := m.ctrl.Call(m, "DecodeIDToken", arg0)
ret0, _ := ret[0].(*oidc.DecodedIDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DecodeIDToken indicates an expected call of DecodeIDToken
func (mr *MockDecoderInterfaceMockRecorder) DecodeIDToken(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeIDToken", reflect.TypeOf((*MockDecoderInterface)(nil).DecodeIDToken), arg0)
}

View File

@@ -1,15 +0,0 @@
package oidc
import (
"github.com/google/wire"
)
//go:generate mockgen -destination mock_oidc/mock_oidc.go github.com/int128/kubelogin/pkg/adaptors/oidc FactoryInterface,Interface,DecoderInterface
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
wire.Struct(new(Decoder)),
wire.Bind(new(DecoderInterface), new(*Decoder)),
)

View File

@@ -0,0 +1,68 @@
// Package oidcclient provides a client of OpenID Connect.
package oidcclient
import (
"context"
"crypto/tls"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type FactoryInterface interface {
New(ctx context.Context, config Config) (Interface, error)
}
// Config represents a configuration of OpenID Connect client.
type Config struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
}
type Factory struct {
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
var tlsConfig tls.Config
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
config.CertPool.SetRootCAs(&tlsConfig)
baseTransport := &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.IssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDCClientFactory issuer: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
}, nil
}

View File

@@ -0,0 +1,147 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidcclient (interfaces: FactoryInterface,Interface)
// Package mock_oidcclient is a generated GoMock package.
package mock_oidcclient
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidcclient "github.com/int128/kubelogin/pkg/adaptors/oidcclient"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New(arg0 context.Context, arg1 oidcclient.Config) (oidcclient.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(oidcclient.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New), arg0, arg1)
}
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// ExchangeAuthCode mocks base method
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExchangeAuthCode", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode
func (mr *MockInterfaceMockRecorder) ExchangeAuthCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthCode", reflect.TypeOf((*MockInterface)(nil).ExchangeAuthCode), arg0, arg1)
}
// GetAuthCodeURL mocks base method
func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthCodeURL", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL
func (mr *MockInterfaceMockRecorder) GetAuthCodeURL(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthCodeURL", reflect.TypeOf((*MockInterface)(nil).GetAuthCodeURL), arg0)
}
// GetTokenByAuthCode mocks base method
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode
func (mr *MockInterfaceMockRecorder) GetTokenByAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByAuthCode", reflect.TypeOf((*MockInterface)(nil).GetTokenByAuthCode), arg0, arg1, arg2)
}
// GetTokenByROPC mocks base method
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByROPC indicates an expected call of GetTokenByROPC
func (mr *MockInterfaceMockRecorder) GetTokenByROPC(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByROPC", reflect.TypeOf((*MockInterface)(nil).GetTokenByROPC), arg0, arg1, arg2)
}
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}

View File

@@ -0,0 +1,197 @@
package oidcclient
import (
"context"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient FactoryInterface,Interface
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
)
type Interface interface {
GetAuthCodeURL(in AuthCodeURLInput) string
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error)
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error)
GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
}
type AuthCodeURLInput struct {
State string
Nonce string
CodeChallenge string
CodeChallengeMethod string
RedirectURI string
}
type ExchangeAuthCodeInput struct {
Code string
CodeVerifier string
Nonce string
RedirectURI string
}
type GetTokenByAuthCodeInput struct {
BindAddress []string
Nonce string
CodeChallenge string
CodeChallengeMethod string
CodeVerifier string
}
// TokenSet represents an output DTO of
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenSubject string
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
}
func (c *client) wrapContext(ctx context.Context) context.Context {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
return ctx
}
// GetTokenByAuthCode performs the authorization code flow.
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
AuthCodeOptions: []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
oidc.Nonce(in.Nonce),
oauth2.SetAuthURLParam("code_challenge", in.CodeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", in.CodeChallengeMethod),
},
TokenRequestOptions: []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier),
},
LocalServerBindAddress: in.BindAddress,
LocalServerReadyChan: localServerReadyChan,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
// GetAuthCodeURL returns the URL of authentication request for the authorization code flow.
func (c *client) GetAuthCodeURL(in AuthCodeURLInput) string {
cfg := c.oauth2Config
cfg.RedirectURL = in.RedirectURI
return cfg.AuthCodeURL(in.State,
oauth2.AccessTypeOffline,
oidc.Nonce(in.Nonce),
oauth2.SetAuthURLParam("code_challenge", in.CodeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", in.CodeChallengeMethod),
)
}
// ExchangeAuthCode exchanges the authorization code and token.
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
cfg := c.oauth2Config
cfg.RedirectURL = in.RedirectURI
token, err := cfg.Exchange(ctx, in.Code, oauth2.SetAuthURLParam("code_verifier", in.CodeVerifier))
if err != nil {
return nil, xerrors.Errorf("could not exchange the authorization code: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
// GetTokenByROPC performs the resource owner password credentials flow.
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
return c.verifyToken(ctx, token, "")
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: refreshToken,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
return c.verifyToken(ctx, token, "")
}
// verifyToken verifies the token with the certificates of the provider and the nonce.
// If the nonce is an empty string, it does not verify the nonce.
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*TokenSet, error) {
idTokenString, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
idToken, err := verifier.Verify(ctx, idTokenString)
if err != nil {
return nil, xerrors.Errorf("could not verify the ID token: %w", err)
}
if nonce != "" && nonce != idToken.Nonce {
return nil, xerrors.Errorf("nonce did not match (wants %s but was %s)", nonce, idToken.Nonce)
}
claims, err := dumpClaims(idToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idTokenString,
RefreshToken: token.RefreshToken,
IDTokenExpiry: idToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
return dumpRawClaims(rawClaims), err
}
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
claims := make(map[string]string)
for k, v := range rawClaims {
switch v := v.(type) {
case float64:
claims[k] = fmt.Sprintf("%.f", v)
default:
claims[k] = fmt.Sprintf("%v", v)
}
}
return claims
}

View File

@@ -35,6 +35,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// FindByKey mocks base method
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.TokenCache, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*tokencache.TokenCache)
ret1, _ := ret[1].(error)
@@ -43,11 +44,13 @@ func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache
// FindByKey indicates an expected call of FindByKey
func (mr *MockInterfaceMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockInterface)(nil).FindByKey), arg0, arg1)
}
// Save mocks base method
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.TokenCache) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
@@ -55,5 +58,6 @@ func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.T
// Save indicates an expected call of Save
func (mr *MockInterfaceMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInterface)(nil).Save), arg0, arg1, arg2)
}

View File

@@ -5,14 +5,16 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
@@ -22,8 +24,8 @@ import (
func NewCmd() cmd.Interface {
wire.Build(
// use-cases
auth.Set,
wire.Value(auth.DefaultLocalServerReadyFunc),
authentication.Set,
wire.Value(authentication.DefaultLocalServerReadyFunc),
standalone.Set,
credentialPluginUseCase.Set,
setup.Set,
@@ -34,16 +36,18 @@ func NewCmd() cmd.Interface {
kubeconfig.Set,
tokencache.Set,
credentialPluginAdaptor.Set,
oidc.Set,
oidcclient.Set,
jwtdecoder.Set,
certpool.Set,
logger.Set,
)
return nil
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(logger.Interface, auth.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
func NewCmdForHeadless(logger.Interface, authentication.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
wire.Build(
auth.Set,
authentication.Set,
standalone.Set,
credentialPluginUseCase.Set,
setup.Set,
@@ -52,7 +56,9 @@ func NewCmdForHeadless(logger.Interface, auth.LocalServerReadyFunc, credentialPl
env.Set,
kubeconfig.Set,
tokencache.Set,
oidc.Set,
oidcclient.Set,
jwtdecoder.Set,
certpool.Set,
)
return nil
}

View File

@@ -6,14 +6,16 @@
package di
import (
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
@@ -23,24 +25,42 @@ import (
func NewCmd() cmd.Interface {
loggerInterface := logger.New()
factory := &oidc.Factory{
factory := &oidcclient.Factory{
Logger: loggerInterface,
}
decoder := &oidc.Decoder{}
decoder := &jwtdecoder.Decoder{}
envEnv := &env.Env{}
localServerReadyFunc := _wireLocalServerReadyFuncValue
authentication := &auth.Authentication{
OIDCFactory: factory,
OIDCDecoder: decoder,
authCode := &authentication.AuthCode{
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Env: envEnv,
Logger: loggerInterface,
}
ropc := &authentication.ROPC{
Env: envEnv,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
OIDCClientFactory: factory,
JWTDecoder: decoder,
Logger: loggerInterface,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
certpoolFactory := &certpool.Factory{}
standaloneStandalone := &standalone.Standalone{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
@@ -49,8 +69,9 @@ func NewCmd() cmd.Interface {
repository := &tokencache.Repository{}
interaction := &credentialplugin.Interaction{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
CertPoolFactory: certpoolFactory,
Interaction: interaction,
Logger: loggerInterface,
}
@@ -59,8 +80,9 @@ func NewCmd() cmd.Interface {
Logger: loggerInterface,
}
setupSetup := &setup.Setup{
Authentication: authentication,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
Setup: setupSetup,
@@ -75,27 +97,45 @@ func NewCmd() cmd.Interface {
}
var (
_wireLocalServerReadyFuncValue = auth.DefaultLocalServerReadyFunc
_wireLocalServerReadyFuncValue = authentication.DefaultLocalServerReadyFunc
)
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc auth.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
factory := &oidc.Factory{
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc authentication.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
factory := &oidcclient.Factory{
Logger: loggerInterface,
}
decoder := &oidc.Decoder{}
decoder := &jwtdecoder.Decoder{}
envEnv := &env.Env{}
authentication := &auth.Authentication{
OIDCFactory: factory,
OIDCDecoder: decoder,
authCode := &authentication.AuthCode{
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Env: envEnv,
Logger: loggerInterface,
}
ropc := &authentication.ROPC{
Env: envEnv,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
OIDCClientFactory: factory,
JWTDecoder: decoder,
Logger: loggerInterface,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
certpoolFactory := &certpool.Factory{}
standaloneStandalone := &standalone.Standalone{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
@@ -103,8 +143,9 @@ func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc au
}
repository := &tokencache.Repository{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
CertPoolFactory: certpoolFactory,
Interaction: credentialpluginInterface,
Logger: loggerInterface,
}
@@ -113,8 +154,9 @@ func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc au
Logger: loggerInterface,
}
setupSetup := &setup.Setup{
Authentication: authentication,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
CertPoolFactory: certpoolFactory,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
Setup: setupSetup,

65
pkg/domain/oidc/oidc.go Normal file
View File

@@ -0,0 +1,65 @@
package oidc
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"strings"
"golang.org/x/xerrors"
)
func NewState() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
func NewNonce() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
type PKCEParams struct {
CodeChallenge string
CodeChallengeMethod string
CodeVerifier string
}
func NewPKCEParams() (*PKCEParams, error) {
b, err := random32()
if err != nil {
return nil, xerrors.Errorf("could not generate a random: %w", err)
}
s := computeS256(b)
return &s, nil
}
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, xerrors.Errorf("could not read: %w", err)
}
return b, nil
}
func computeS256(b []byte) PKCEParams {
v := base64URLEncode(b)
s := sha256.New()
_, _ = s.Write([]byte(v))
return PKCEParams{
CodeChallenge: base64URLEncode(s.Sum(nil)),
CodeChallengeMethod: "S256",
CodeVerifier: v,
}
}
func base64URLEncode(b []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
}

View File

@@ -0,0 +1,25 @@
package oidc
import (
"testing"
)
func Test_computeS256(t *testing.T) {
// Testdata described at:
// https://tools.ietf.org/html/rfc7636#appendix-B
b := []byte{
116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
132, 141, 121,
}
p := computeS256(b)
if want := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; want != p.CodeVerifier {
t.Errorf("CodeVerifier wants %s but was %s", want, p.CodeVerifier)
}
if want := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; want != p.CodeChallenge {
t.Errorf("CodeChallenge wants %s but was %s", want, p.CodeChallenge)
}
if p.CodeChallengeMethod != "S256" {
t.Errorf("CodeChallengeMethod wants S256 but was %s", p.CodeChallengeMethod)
}
}

View File

@@ -1,200 +0,0 @@
package auth
import (
"context"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_auth/mock_auth.go github.com/int128/kubelogin/pkg/usecases/auth Interface
// Set provides the use-case of Authentication.
var Set = wire.NewSet(
wire.Struct(new(Authentication), "*"),
wire.Bind(new(Interface), new(*Authentication)),
)
// LocalServerReadyFunc provides an extension point for e2e tests.
type LocalServerReadyFunc func(url string)
// DefaultLocalServerReadyFunc is the default noop function.
var DefaultLocalServerReadyFunc = LocalServerReadyFunc(nil)
type Interface interface {
Do(ctx context.Context, in Input) (*Output, error)
}
// Input represents an input DTO of the Authentication use-case.
type Input struct {
OIDCConfig kubeconfig.OIDCConfig
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
}
// Output represents an output DTO of the Authentication use-case.
type Output struct {
AlreadyHasValidIDToken bool
IDTokenSubject string
IDTokenExpiry time.Time
IDTokenClaims map[string]string
IDToken string
RefreshToken string
}
const passwordPrompt = "Password: "
// Authentication provides the internal use-case of authentication.
//
// If the IDToken is not set, it performs the authentication flow.
// If the IDToken is valid, it does nothing.
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
// If the RefreshToken has expired, it performs the authentication flow.
//
// The authentication flow is determined as:
//
// If the Username is not set, it performs the authorization code flow.
// Otherwise, it performs the resource owner password credentials flow.
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
OIDCFactory oidc.FactoryInterface
OIDCDecoder oidc.DecoderInterface
Env env.Interface
Logger logger.Interface
LocalServerReadyFunc LocalServerReadyFunc // only for e2e tests
}
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if in.OIDCConfig.IDToken != "" {
u.Logger.V(1).Infof("checking expiration of the existing token")
// Skip verification of the token to reduce time of a discovery request.
// Here it trusts the signature and claims and checks only expiration,
// because the token has been verified before caching.
token, err := u.OIDCDecoder.DecodeIDToken(in.OIDCConfig.IDToken)
if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
}
if token.Expiry.After(time.Now()) { //TODO: inject time service
u.Logger.V(1).Infof("you already have a valid token until %s", token.Expiry)
return &Output{
AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken,
IDTokenSubject: token.Subject,
IDTokenExpiry: token.Expiry,
IDTokenClaims: token.Claims,
}, nil
}
u.Logger.V(1).Infof("you have an expired token at %s", token.Expiry)
}
u.Logger.V(1).Infof("initializing an OIDCFactory client")
client, err := u.OIDCFactory.New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return nil, xerrors.Errorf("could not create an OIDCFactory client: %w", err)
}
if in.OIDCConfig.RefreshToken != "" {
u.Logger.V(1).Infof("refreshing the token")
out, err := client.Refresh(ctx, in.OIDCConfig.RefreshToken)
if err == nil {
return &Output{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenSubject: out.IDTokenSubject,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.V(1).Infof("could not refresh the token: %s", err)
}
if in.Username == "" {
return u.doAuthCodeFlow(ctx, in, client)
}
return u.doPasswordCredentialsFlow(ctx, in, client)
}
func (u *Authentication) doAuthCodeFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authentication code flow")
readyChan := make(chan string, 1)
defer close(readyChan)
var out Output
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
select {
case url, ok := <-readyChan:
if !ok {
return nil
}
u.Logger.Printf("Open %s for authentication", url)
if u.LocalServerReadyFunc != nil {
u.LocalServerReadyFunc(url)
}
if in.SkipOpenBrowser {
return nil
}
if err := u.Env.OpenBrowser(url); err != nil {
u.Logger.V(1).Infof("could not open the browser: %s", err)
}
return nil
case <-ctx.Done():
return xerrors.Errorf("context cancelled while waiting for the local server: %w", ctx.Err())
}
})
eg.Go(func() error {
tokenSet, err := client.AuthenticateByCode(ctx, in.ListenPort, readyChan)
if err != nil {
return xerrors.Errorf("error while the authorization code flow: %w", err)
}
out = Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenSubject: tokenSet.IDTokenSubject,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &out, nil
}
func (u *Authentication) doPasswordCredentialsFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
if in.Password == "" {
var err error
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
}
}
tokenSet, err := client.AuthenticateByPassword(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
}
return &Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenSubject: tokenSet.IDTokenSubject,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}, nil
}

View File

@@ -1,435 +0,0 @@
package auth
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidc/mock_oidc"
"golang.org/x/xerrors"
)
func TestAuthentication_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
pastTime := time.Now().Add(-time.Hour) //TODO: inject time service
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
timeout := 5 * time.Second
t.Run("AuthorizationCodeFlow", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
ListenPort: []int{10000},
SkipOpenBrowser: true,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(gomock.Any(), []int{10000}, gomock.Any()).
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AuthorizationCodeFlow/OpenBrowser", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
ListenPort: []int{10000},
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(gomock.Any(), []int{10000}, gomock.Any()).
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{Config: in.OIDCConfig}).
Return(mockOIDCClient, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
OpenBrowser("LOCAL_SERVER_URL")
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
Env: mockEnv,
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/UsePassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(gomock.Any(), "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
Username: "USER",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(gomock.Any(), "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
Username: "USER",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mock_oidc.NewMockInterface(ctrl), nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := Authentication{
OIDCFactory: mockOIDCFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
}
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("VALID_ID_TOKEN").
Return(&oidc.DecodedIDToken{
Subject: "YOUR_SUBJECT",
Expiry: futureTime,
Claims: dummyTokenClaims,
}, nil)
u := Authentication{
OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl),
OIDCDecoder: mockOIDCDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "VALID_REFRESH_TOKEN",
},
}
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&oidc.DecodedIDToken{
Subject: "YOUR_SUBJECT",
Expiry: pastTime,
Claims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "VALID_REFRESH_TOKEN").
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
ListenPort: []int{10000},
SkipOpenBrowser: true,
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "EXPIRED_REFRESH_TOKEN",
},
}
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&oidc.DecodedIDToken{
Subject: "YOUR_SUBJECT",
Expiry: pastTime,
Claims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
AuthenticateByCode(gomock.Any(), []int{10000}, gomock.Any()).
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCFactory: mockOIDCFactory,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
}

View File

@@ -0,0 +1,81 @@
package authentication
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
// AuthCode provides the authentication code flow.
type AuthCode struct {
Env env.Interface
Logger logger.Interface
LocalServerReadyFunc LocalServerReadyFunc // only for e2e tests
}
func (u *AuthCode) Do(ctx context.Context, o *AuthCodeOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authentication code flow")
nonce, err := oidc.NewNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
}
p, err := oidc.NewPKCEParams()
if err != nil {
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
}
in := oidcclient.GetTokenByAuthCodeInput{
BindAddress: o.BindAddress,
Nonce: nonce,
CodeChallenge: p.CodeChallenge,
CodeChallengeMethod: p.CodeChallengeMethod,
CodeVerifier: p.CodeVerifier,
}
readyChan := make(chan string, 1)
defer close(readyChan)
var out Output
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
select {
case url, ok := <-readyChan:
if !ok {
return nil
}
u.Logger.Printf("Open %s for authentication", url)
if u.LocalServerReadyFunc != nil {
u.LocalServerReadyFunc(url)
}
if o.SkipOpenBrowser {
return nil
}
if err := u.Env.OpenBrowser(url); err != nil {
u.Logger.V(1).Infof("could not open the browser: %s", err)
}
return nil
case <-ctx.Done():
return xerrors.Errorf("context cancelled while waiting for the local server: %w", ctx.Err())
}
})
eg.Go(func() error {
tokenSet, err := client.GetTokenByAuthCode(ctx, in, readyChan)
if err != nil {
return xerrors.Errorf("error while the authorization code flow: %w", err)
}
out = Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenSubject: tokenSet.IDTokenSubject,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &out, nil
}

View File

@@ -0,0 +1,65 @@
package authentication
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/domain/oidc"
"golang.org/x/xerrors"
)
const authCodeKeyboardPrompt = "Enter code: "
const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
// AuthCodeKeyboard provides the authorization code flow with keyboard interactive.
type AuthCodeKeyboard struct {
Env env.Interface
Logger logger.Interface
}
func (u *AuthCodeKeyboard) Do(ctx context.Context, o *AuthCodeKeyboardOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authorization code flow with keyboard interactive")
state, err := oidc.NewState()
if err != nil {
return nil, xerrors.Errorf("could not generate a state: %w", err)
}
nonce, err := oidc.NewNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
}
p, err := oidc.NewPKCEParams()
if err != nil {
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
}
authCodeURL := client.GetAuthCodeURL(oidcclient.AuthCodeURLInput{
State: state,
Nonce: nonce,
CodeChallenge: p.CodeChallenge,
CodeChallengeMethod: p.CodeChallengeMethod,
RedirectURI: oobRedirectURI,
})
u.Logger.Printf("Open %s", authCodeURL)
code, err := u.Env.ReadString(authCodeKeyboardPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read the authorization code: %w", err)
}
tokenSet, err := client.ExchangeAuthCode(ctx, oidcclient.ExchangeAuthCodeInput{
Code: code,
CodeVerifier: p.CodeVerifier,
Nonce: nonce,
RedirectURI: oobRedirectURI,
})
if err != nil {
return nil, xerrors.Errorf("could not get the token: %w", err)
}
return &Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenSubject: tokenSet.IDTokenSubject,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}, nil
}

View File

@@ -0,0 +1,69 @@
package authentication
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
)
var nonNil = gomock.Not(gomock.Nil())
func TestAuthCodeKeyboard_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
timeout := 5 * time.Second
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetAuthCodeURL(nonNil).
Return("https://issuer.example.com/auth")
mockOIDCClient.EXPECT().
ExchangeAuthCode(nonNil, nonNil).
Do(func(_ context.Context, in oidcclient.ExchangeAuthCodeInput) {
if in.Code != "YOUR_AUTH_CODE" {
t.Errorf("Code wants YOUR_AUTH_CODE but was %s", in.Code)
}
}).
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
ReadString(authCodeKeyboardPrompt).
Return("YOUR_AUTH_CODE", nil)
u := AuthCodeKeyboard{
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, nil, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
}

View File

@@ -0,0 +1,105 @@
package authentication
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
)
func TestAuthCode_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
timeout := 5 * time.Second
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := AuthCode{
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("OpenBrowser", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
OpenBrowser("LOCAL_SERVER_URL")
u := AuthCode{
Logger: mock_logger.New(t),
Env: mockEnv,
}
out, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
}

View File

@@ -0,0 +1,164 @@
package authentication
import (
"context"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_authentication/mock_authentication.go github.com/int128/kubelogin/pkg/usecases/authentication Interface
// Set provides the use-case of Authentication.
var Set = wire.NewSet(
wire.Struct(new(Authentication), "*"),
wire.Bind(new(Interface), new(*Authentication)),
wire.Struct(new(AuthCode), "*"),
wire.Struct(new(AuthCodeKeyboard), "*"),
wire.Struct(new(ROPC), "*"),
)
// LocalServerReadyFunc provides an extension point for e2e tests.
type LocalServerReadyFunc func(url string)
// DefaultLocalServerReadyFunc is the default noop function.
var DefaultLocalServerReadyFunc = LocalServerReadyFunc(nil)
type Interface interface {
Do(ctx context.Context, in Input) (*Output, error)
}
// Input represents an input DTO of the Authentication use-case.
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
IDToken string // optional
RefreshToken string // optional
GrantOptionSet GrantOptionSet
}
type GrantOptionSet struct {
AuthCodeOption *AuthCodeOption
AuthCodeKeyboardOption *AuthCodeKeyboardOption
ROPCOption *ROPCOption
}
type AuthCodeOption struct {
SkipOpenBrowser bool
BindAddress []string
}
type AuthCodeKeyboardOption struct{}
type ROPCOption struct {
Username string
Password string // If empty, read a password using Env.ReadPassword()
}
// Output represents an output DTO of the Authentication use-case.
type Output struct {
AlreadyHasValidIDToken bool
IDTokenSubject string
IDTokenExpiry time.Time
IDTokenClaims map[string]string
IDToken string
RefreshToken string
}
const usernamePrompt = "Username: "
const passwordPrompt = "Password: "
// Authentication provides the internal use-case of authentication.
//
// If the IDToken is not set, it performs the authentication flow.
// If the IDToken is valid, it does nothing.
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
// If the RefreshToken has expired, it performs the authentication flow.
//
// The authentication flow is determined as:
//
// If the Username is not set, it performs the authorization code flow.
// Otherwise, it performs the resource owner password credentials flow.
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
OIDCClientFactory oidcclient.FactoryInterface
JWTDecoder jwtdecoder.Interface
Logger logger.Interface
AuthCode *AuthCode
AuthCodeKeyboard *AuthCodeKeyboard
ROPC *ROPC
}
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if in.IDToken != "" {
u.Logger.V(1).Infof("checking expiration of the existing token")
// Skip verification of the token to reduce time of a discovery request.
// Here it trusts the signature and claims and checks only expiration,
// because the token has been verified before caching.
claims, err := u.JWTDecoder.Decode(in.IDToken)
if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
}
if claims.Expiry.After(time.Now()) { //TODO: inject time service
u.Logger.V(1).Infof("you already have a valid token until %s", claims.Expiry)
return &Output{
AlreadyHasValidIDToken: true,
IDToken: in.IDToken,
RefreshToken: in.RefreshToken,
IDTokenSubject: claims.Subject,
IDTokenExpiry: claims.Expiry,
IDTokenClaims: claims.Pretty,
}, nil
}
u.Logger.V(1).Infof("you have an expired token at %s", claims.Expiry)
}
u.Logger.V(1).Infof("initializing an OpenID Connect client")
client, err := u.OIDCClientFactory.New(ctx, oidcclient.Config{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: in.CertPool,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return nil, xerrors.Errorf("could not create an OpenID Connect client: %w", err)
}
if in.RefreshToken != "" {
u.Logger.V(1).Infof("refreshing the token")
out, err := client.Refresh(ctx, in.RefreshToken)
if err == nil {
return &Output{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenSubject: out.IDTokenSubject,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.V(1).Infof("could not refresh the token: %s", err)
}
if in.GrantOptionSet.AuthCodeOption != nil {
return u.AuthCode.Do(ctx, in.GrantOptionSet.AuthCodeOption, client)
}
if in.GrantOptionSet.AuthCodeKeyboardOption != nil {
return u.AuthCodeKeyboard.Do(ctx, in.GrantOptionSet.AuthCodeKeyboardOption, client)
}
if in.GrantOptionSet.ROPCOption != nil {
return u.ROPC.Do(ctx, in.GrantOptionSet.ROPCOption, client)
}
return nil, xerrors.Errorf("any authorization grant must be set")
}

View File

@@ -0,0 +1,253 @@
package authentication
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/jwtdecoder/mock_jwtdecoder"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"golang.org/x/xerrors"
)
func TestAuthentication_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
pastTime := time.Now().Add(-time.Hour) //TODO: inject time service
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
timeout := 5 * time.Second
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
}
mockDecoder := mock_jwtdecoder.NewMockInterface(ctrl)
mockDecoder.EXPECT().
Decode("VALID_ID_TOKEN").
Return(&jwtdecoder.Claims{
Subject: "YOUR_SUBJECT",
Expiry: futureTime,
Pretty: dummyTokenClaims,
}, nil)
u := Authentication{
OIDCClientFactory: mock_oidcclient.NewMockFactoryInterface(ctrl),
JWTDecoder: mockDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "VALID_REFRESH_TOKEN",
}
mockDecoder := mock_jwtdecoder.NewMockInterface(ctrl)
mockDecoder.EXPECT().
Decode("EXPIRED_ID_TOKEN").
Return(&jwtdecoder.Claims{
Subject: "YOUR_SUBJECT",
Expiry: pastTime,
Pretty: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "VALID_REFRESH_TOKEN").
Return(&oidcclient.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClientFactory := mock_oidcclient.NewMockFactoryInterface(ctrl)
mockOIDCClientFactory.EXPECT().
New(ctx, oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCClientFactory: mockOIDCClientFactory,
JWTDecoder: mockDecoder,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasExpiredRefreshToken/AuthCode", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
GrantOptionSet: GrantOptionSet{
AuthCodeOption: &AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
},
},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "EXPIRED_REFRESH_TOKEN",
}
mockDecoder := mock_jwtdecoder.NewMockInterface(ctrl)
mockDecoder.EXPECT().
Decode("EXPIRED_ID_TOKEN").
Return(&jwtdecoder.Claims{
Subject: "YOUR_SUBJECT",
Expiry: pastTime,
Pretty: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidcclient.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClientFactory := mock_oidcclient.NewMockFactoryInterface(ctrl)
mockOIDCClientFactory.EXPECT().
New(ctx, oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCClientFactory: mockOIDCClientFactory,
JWTDecoder: mockDecoder,
Logger: mock_logger.New(t),
AuthCode: &AuthCode{
Logger: mock_logger.New(t),
},
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("NoToken/ROPC", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
GrantOptionSet: GrantOptionSet{
ROPCOption: &ROPCOption{
Username: "USER",
Password: "PASS",
},
},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClientFactory := mock_oidcclient.NewMockFactoryInterface(ctrl)
mockOIDCClientFactory.EXPECT().
New(ctx, oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDCClientFactory: mockOIDCClientFactory,
Logger: mock_logger.New(t),
ROPC: &ROPC{
Logger: mock_logger.New(t),
},
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
}

View File

@@ -1,13 +1,13 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/usecases/auth (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/usecases/authentication (interfaces: Interface)
// Package mock_auth is a generated GoMock package.
package mock_auth
// Package mock_authentication is a generated GoMock package.
package mock_authentication
import (
context "context"
gomock "github.com/golang/mock/gomock"
auth "github.com/int128/kubelogin/pkg/usecases/auth"
authentication "github.com/int128/kubelogin/pkg/usecases/authentication"
reflect "reflect"
)
@@ -35,14 +35,16 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
}
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 auth.Input) (*auth.Output, error) {
func (m *MockInterface) Do(arg0 context.Context, arg1 authentication.Input) (*authentication.Output, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*auth.Output)
ret0, _ := ret[0].(*authentication.Output)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -0,0 +1,45 @@
package authentication
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"golang.org/x/xerrors"
)
// ROPC provides the resource owner password credentials flow.
type ROPC struct {
Env env.Interface
Logger logger.Interface
}
func (u *ROPC) Do(ctx context.Context, in *ROPCOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
if in.Username == "" {
var err error
in.Username, err = u.Env.ReadString(usernamePrompt)
if err != nil {
return nil, xerrors.Errorf("could not get the username: %w", err)
}
}
if in.Password == "" {
var err error
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
}
}
tokenSet, err := client.GetTokenByROPC(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
}
return &Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenSubject: tokenSet.IDTokenSubject,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}, nil
}

View File

@@ -0,0 +1,161 @@
package authentication
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"golang.org/x/xerrors"
)
func TestROPC_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
timeout := 5 * time.Second
t.Run("AskUsernameAndPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadString(usernamePrompt).Return("USER", nil)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("UsePassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{
Username: "USER",
Password: "PASS",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := ROPC{
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{
Username: "USER",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, o, mockOIDCClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &ROPCOption{
Username: "USER",
}
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := ROPC{
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, o, mock_oidcclient.NewMockInterface(ctrl))
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
}

View File

@@ -7,11 +7,11 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
@@ -28,22 +28,20 @@ type Interface interface {
// Input represents an input DTO of the GetToken use-case.
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
TokenCacheDir string
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
TokenCacheDir string
GrantOptionSet authentication.GrantOptionSet
}
type GetToken struct {
Authentication auth.Interface
Authentication authentication.Interface
TokenCacheRepository tokencache.Interface
CertPoolFactory certpool.FactoryInterface
Interaction credentialplugin.Interface
Logger logger.Interface
}
@@ -61,7 +59,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
return nil
}
func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*auth.Output, error) {
func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*authentication.Output, error) {
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
cacheKey := tokencache.Key{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
@@ -69,22 +67,22 @@ func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*
u.Logger.V(1).Infof("could not find a token cache: %s", err)
cache = &tokencache.TokenCache{}
}
out, err := u.Authentication.Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
IDToken: cache.IDToken,
RefreshToken: cache.RefreshToken,
},
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
certPool := u.CertPoolFactory.New()
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
IDToken: cache.IDToken,
RefreshToken: cache.RefreshToken,
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return nil, xerrors.Errorf("error while authentication: %w", err)

View File

@@ -6,14 +6,14 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/adaptors/tokencache/mock_tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
"golang.org/x/xerrors"
)
@@ -22,37 +22,37 @@ func TestGetToken_Do(t *testing.T) {
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("FullOptions", func(t *testing.T) {
var grantOptionSet authentication.GrantOptionSet
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert")
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}).
Return(&auth.Output{
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
@@ -84,6 +84,7 @@ func TestGetToken_Do(t *testing.T) {
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
CertPoolFactory: mockCertPoolFactory,
Interaction: credentialPluginInteraction,
Logger: mock_logger.New(t),
}
@@ -102,17 +103,21 @@ func TestGetToken_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
CertPool: mockCertPool,
}).
Return(&auth.Output{
Return(&authentication.Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
@@ -136,6 +141,7 @@ func TestGetToken_Do(t *testing.T) {
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
CertPoolFactory: mockCertPoolFactory,
Interaction: credentialPluginInteraction,
Logger: mock_logger.New(t),
}
@@ -154,14 +160,18 @@ func TestGetToken_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
}).
Return(nil, xerrors.New("authentication error"))
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
@@ -174,6 +184,7 @@ func TestGetToken_Do(t *testing.T) {
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
CertPoolFactory: mockCertPoolFactory,
Interaction: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
}

View File

@@ -36,6 +36,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
@@ -43,5 +44,6 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) er
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -5,8 +5,9 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
var Set = wire.NewSet(
@@ -20,6 +21,7 @@ type Interface interface {
}
type Setup struct {
Authentication auth.Interface
Logger logger.Interface
Authentication authentication.Interface
CertPoolFactory certpool.FactoryInterface
Logger logger.Interface
}

View File

@@ -6,8 +6,7 @@ import (
"strings"
"text/template"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
@@ -66,30 +65,32 @@ type stage2Vars struct {
// Stage2Input represents an input DTO of the stage2.
type Stage2Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
SkipOpenBrowser bool
ListenPort []int
ListenPortIsSet bool // true if it is set by the command arg
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
ListenPortArgs []int // non-nil if set by the command arg
GrantOptionSet authentication.GrantOptionSet
}
func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
u.Logger.Printf(`## 2. Verify authentication`)
out, err := u.Authentication.Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
},
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
certPool := u.CertPoolFactory.New()
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return xerrors.Errorf("could not load the certificate: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
@@ -123,19 +124,25 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
for _, extraScope := range in.ExtraScopes {
args = append(args, "--oidc-extra-scope="+extraScope)
}
if in.SkipOpenBrowser {
args = append(args, "--skip-open-browser")
}
if in.ListenPortIsSet {
for _, port := range in.ListenPort {
args = append(args, fmt.Sprintf("--listen-port=%d", port))
}
}
if in.CACertFilename != "" {
args = append(args, "--certificate-authority="+in.CACertFilename)
}
if in.SkipTLSVerify {
args = append(args, "--insecure-skip-tls-verify")
}
if in.GrantOptionSet.AuthCodeOption != nil {
if in.GrantOptionSet.AuthCodeOption.SkipOpenBrowser {
args = append(args, "--skip-open-browser")
}
}
for _, port := range in.ListenPortArgs {
args = append(args, fmt.Sprintf("--listen-port=%d", port))
}
if in.GrantOptionSet.ROPCOption != nil {
if in.GrantOptionSet.ROPCOption.Username != "" {
args = append(args, "--username="+in.GrantOptionSet.ROPCOption.Username)
}
}
return args
}

View File

@@ -6,43 +6,47 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
)
func TestSetup_DoStage2(t *testing.T) {
var grantOptionSet authentication.GrantOptionSet
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
in := Stage2Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
SkipOpenBrowser: true,
ListenPort: []int{8000},
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert")
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
},
SkipOpenBrowser: true,
ListenPort: []int{8000},
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}).
Return(&auth.Output{
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
@@ -50,8 +54,9 @@ func TestSetup_DoStage2(t *testing.T) {
IDTokenClaims: map[string]string{"iss": "https://accounts.google.com"},
}, nil)
u := Setup{
Authentication: mockAuthentication,
Logger: mock_logger.New(t),
Authentication: mockAuthentication,
CertPoolFactory: mockCertPoolFactory,
Logger: mock_logger.New(t),
}
if err := u.DoStage2(ctx, in); err != nil {
t.Errorf("DoStage2 returned error: %+v", err)

View File

@@ -36,6 +36,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
@@ -43,5 +44,6 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -6,9 +6,10 @@ import (
"text/template"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
@@ -29,12 +30,9 @@ type Input struct {
KubeconfigFilename string // Default to the environment variable or global config as kubectl
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
KubeconfigUser kubeconfig.UserName // Default to the user of the context
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
GrantOptionSet authentication.GrantOptionSet
}
const oidcConfigErrorMessage = `You need to set up the kubeconfig for OpenID Connect authentication.
@@ -48,9 +46,10 @@ See https://github.com/int128/kubelogin for more.
// Otherwise, update the kubeconfig.
//
type Standalone struct {
Authentication auth.Interface
Kubeconfig kubeconfig.Interface
Logger logger.Interface
Authentication authentication.Interface
Kubeconfig kubeconfig.Interface
CertPoolFactory certpool.FactoryInterface
Logger logger.Interface
}
func (u *Standalone) Do(ctx context.Context, in Input) error {
@@ -66,15 +65,32 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
}
u.Logger.V(1).Infof("using the authentication provider of the user %s", authProvider.UserName)
u.Logger.V(1).Infof("a token will be written to %s", authProvider.LocationOfOrigin)
out, err := u.Authentication.Do(ctx, auth.Input{
OIDCConfig: authProvider.OIDCConfig,
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
certPool := u.CertPoolFactory.New()
if authProvider.IDPCertificateAuthority != "" {
if err := certPool.AddFile(authProvider.IDPCertificateAuthority); err != nil {
return xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if authProvider.IDPCertificateAuthorityData != "" {
if err := certPool.AddBase64Encoded(authProvider.IDPCertificateAuthorityData); err != nil {
return xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return xerrors.Errorf("could not load the certificate: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: authProvider.IDPIssuerURL,
ClientID: authProvider.ClientID,
ClientSecret: authProvider.ClientSecret,
ExtraScopes: authProvider.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
IDToken: authProvider.IDToken,
RefreshToken: authProvider.RefreshToken,
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
@@ -88,8 +104,8 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
}
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
authProvider.OIDCConfig.IDToken = out.IDToken
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
authProvider.IDToken = out.IDToken
authProvider.RefreshToken = out.RefreshToken
u.Logger.V(1).Infof("writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
@@ -129,22 +145,24 @@ type deprecationVars struct {
func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error {
var args []string
args = append(args, "--oidc-issuer-url="+p.OIDCConfig.IDPIssuerURL)
args = append(args, "--oidc-client-id="+p.OIDCConfig.ClientID)
if p.OIDCConfig.ClientSecret != "" {
args = append(args, "--oidc-client-secret="+p.OIDCConfig.ClientSecret)
args = append(args, "--oidc-issuer-url="+p.IDPIssuerURL)
args = append(args, "--oidc-client-id="+p.ClientID)
if p.ClientSecret != "" {
args = append(args, "--oidc-client-secret="+p.ClientSecret)
}
for _, extraScope := range p.OIDCConfig.ExtraScopes {
for _, extraScope := range p.ExtraScopes {
args = append(args, "--oidc-extra-scope="+extraScope)
}
if p.OIDCConfig.IDPCertificateAuthority != "" {
args = append(args, "--certificate-authority="+p.OIDCConfig.IDPCertificateAuthority)
if p.IDPCertificateAuthority != "" {
args = append(args, "--certificate-authority="+p.IDPCertificateAuthority)
}
if in.CACertFilename != "" {
args = append(args, "--certificate-authority="+in.CACertFilename)
}
if in.Username != "" {
args = append(args, "--username="+in.Username)
if in.GrantOptionSet.ROPCOption != nil {
if in.GrantOptionSet.ROPCOption.Username != "" {
args = append(args, "--username="+in.GrantOptionSet.ROPCOption.Username)
}
}
v := deprecationVars{

View File

@@ -6,11 +6,12 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig/mock_kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
"golang.org/x/xerrors"
)
@@ -19,6 +20,7 @@ func TestStandalone_Do(t *testing.T) {
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("FullOptions", func(t *testing.T) {
var grantOptionSet authentication.GrantOptionSet
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
@@ -26,59 +28,67 @@ func TestStandalone_Do(t *testing.T) {
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
CACertFilename: "/path/to/cert1",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert2",
IDPCertificateAuthorityData: "BASE64ENCODED",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert1")
mockCertPool.EXPECT().
AddFile("/path/to/cert2")
mockCertPool.EXPECT().
AddBase64Encoded("BASE64ENCODED")
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert2",
IDPCertificateAuthorityData: "BASE64ENCODED",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{
OIDCConfig: currentAuthProvider.OIDCConfig,
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}).
Return(&auth.Output{
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_logger.New(t),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
CertPoolFactory: mockCertPoolFactory,
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -93,29 +103,40 @@ func TestStandalone_Do(t *testing.T) {
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&auth.Output{
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
CertPool: mockCertPool,
}).
Return(&authentication.Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_logger.New(t),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
CertPoolFactory: mockCertPoolFactory,
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -127,15 +148,17 @@ func TestStandalone_Do(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
in := Input{}
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, xerrors.New("no oidc config"))
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_logger.New(t),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
CertPoolFactory: mockCertPoolFactory,
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
@@ -150,24 +173,33 @@ func TestStandalone_Do(t *testing.T) {
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
}).
Return(nil, xerrors.New("authentication error"))
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_logger.New(t),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
CertPoolFactory: mockCertPoolFactory,
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
@@ -182,12 +214,15 @@ func TestStandalone_Do(t *testing.T) {
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPoolFactory := mock_certpool.NewMockFactoryInterface(ctrl)
mockCertPoolFactory.EXPECT().
New().
Return(mockCertPool)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
@@ -196,28 +231,32 @@ func TestStandalone_Do(t *testing.T) {
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}).
Return(xerrors.New("I/O error"))
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&auth.Output{
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
}).
Return(&authentication.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_logger.New(t),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
CertPoolFactory: mockCertPoolFactory,
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")