Compare commits

...

11 Commits

Author SHA1 Message Date
Hidetake Iwata
c8967faf6b Fix krew yaml (#134) 2019-08-18 16:57:47 +09:00
Hidetake Iwata
315d6151d7 Refactor (#133)
* Refactor: change debug messages to lowercase

* Refactor: add debug messages

* Refactor Makefile

* Refactor: add keys and certificates of e2e tests
2019-08-18 15:14:07 +09:00
Hidetake Iwata
1ff03fdfb3 Skip verification of cached token to reduce time (#132) 2019-08-17 21:40:14 +09:00
Hidetake Iwata
5e0fc7f399 Save token cache for each issuer and client ID (#131) 2019-08-14 14:52:58 +09:00
Hidetake Iwata
9423a65f46 Add dex documentation (#130) 2019-08-11 16:09:53 +09:00
Hidetake Iwata
45417a18fd Refactor docs 2019-08-10 15:25:06 +09:00
Hidetake Iwata
760416fd04 Update README.md 2019-08-09 17:01:36 +09:00
Hidetake Iwata
0a4ebb26c2 Refactor packages structure (#129) 2019-08-09 10:15:17 +09:00
Hidetake Iwata
de9f7a2a01 Fix typo (#128)
* Fix typo

* Update google.md

* Update keycloak.md
2019-08-03 20:05:13 +09:00
Hidetake Iwata
0006cdda2d Update README.md 2019-08-02 14:10:20 +09:00
Hidetake Iwata
c89a8a1823 Update README.md 2019-08-01 11:00:55 +09:00
67 changed files with 937 additions and 421 deletions

View File

@@ -3,13 +3,11 @@ TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
.PHONY: check run diagram release clean
all: $(TARGET)
.PHONY: check
check:
golangci-lint run
$(MAKE) -C e2e_test/keys/testdata
go test -v -race -cover -coverprofile=coverage.out ./...
$(TARGET): $(wildcard *.go)
@@ -18,6 +16,7 @@ $(TARGET): $(wildcard *.go)
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
.PHONY: run
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
@@ -32,11 +31,13 @@ dist:
mkdir -p dist/plugins
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
.PHONY: release
release: dist
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
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)

View File

@@ -2,8 +2,9 @@
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`.
Kubelogin integrates browser based authentication with kubectl.
You do not need to manually set an ID token and refresh token to the kubeconfig.
This 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 calls the Kubernetes APIs with the token.
## Getting Started
@@ -19,7 +20,7 @@ brew install kubelogin
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.0/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.1/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
@@ -28,16 +29,16 @@ You need to configure the OIDC provider, Kubernetes API server, kubeconfig and r
See the following documents for more:
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with dex and GitHub](docs/dex.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
You can run kubelogin as the following methods:
- Run as a credential plugin
- Run as a standalone command
- Credential plugin mode
- Standalone mode
### Run as a credential plugin
### Credential plugin mode
You can run kubelogin as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
This provides transparent login without manually running `kubelogin` command.
@@ -86,11 +87,11 @@ 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.
You can log out by removing the token cache file (default `~/.kube/oidc-login.token-cache`).
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.
### Run as a standalone command
### Standalone mode
You can run kubelogin as a standalone command.
In this method, you need to manually run the command before running kubectl.
@@ -130,7 +131,7 @@ You got a valid token until 2019-05-18 10:28:51 +0900 JST
Updated ~/.kubeconfig
```
Now you can access to the cluster.
Now you can access the cluster.
```
% kubectl get pods
@@ -138,6 +139,22 @@ NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
Your kubeconfig looks like:
```yaml
users:
- name: keycloak
user:
auth-provider:
config:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: https://issuer.example.com
id-token: ey... # kubelogin will add or update the ID token here
refresh-token: ey... # kubelogin will add or update the refresh token here
name: oidc
```
If the ID token is valid, kubelogin does nothing.
```
@@ -155,7 +172,7 @@ This document is for the development version.
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
### Run as a credential plugin
### Credential plugin mode
Kubelogin supports the following options:
@@ -178,7 +195,7 @@ Flags:
--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
-v, --v int If set to 1 or greater, it shows debug log
--token-cache string Path to a file for caching the token (default "~/.kube/oidc-login.token-cache")
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
-h, --help help for get-token
```
@@ -200,7 +217,7 @@ You can use your self-signed certificates for the provider.
```
### Run as a standalone command
### Standalone mode
Kubelogin supports the following options:

View File

@@ -1,46 +0,0 @@
package tokencache
import (
"encoding/json"
"os"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/credentialplugin"
"golang.org/x/xerrors"
)
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Repository), "*"),
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
)
type Repository struct{}
func (*Repository) Read(filename string) (*credentialplugin.TokenCache, error) {
f, err := os.Open(filename)
if err != nil {
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
}
defer f.Close()
d := json.NewDecoder(f)
var c credentialplugin.TokenCache
if err := d.Decode(&c); err != nil {
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
}
return &c, nil
}
func (*Repository) Write(filename string, tc credentialplugin.TokenCache) error {
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return xerrors.Errorf("could not create file %s: %w", filename, err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(&tc); err != nil {
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
}
return nil
}

141
docs/dex.md Normal file
View File

@@ -0,0 +1,141 @@
# Getting Started with dex and GitHub
## Prerequisite
- You have a GitHub account.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed.
## 1. Setup GitHub OAuth
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`
## 2. Setup dex
Configure the 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: kubernetes
name: Kubernetes
redirectURIs:
- http://localhost:8000
- http://localhost:18000
secret: YOUR_DEX_CLIENT_SECRET
```
Now test authentication with the dex.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://dex.example.com \
--oidc-client-id=kubernetes \
--oidc-client-secret=YOUR_DEX_CLIENT_SECRET
```
You should get claims like:
```
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://dex.example.com
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
17:21:32.052683 get_token.go:57: ID token has the claim: aud=kubernetes
```
## 3. Setup Kubernetes API server
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
```
--oidc-issuer-url=https://dex.example.com
--oidc-client-id=kubernetes
```
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://dex.example.com
oidcClientID: kubernetes
```
## 4. Create a role binding
Here assign the `cluster-admin` role to your subject.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: YOUR_SUBJECT
```
You can create a custom role and assign it as well.
## 5. Setup kubeconfig
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: dex
name: dex@example.k8s.local
current-context: dex@example.k8s.local
kind: Config
preferences: {}
users:
- name: dex
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=kubernetes
- --oidc-client-secret=YOUR_DEX_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Run kubectl
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
Updated ~/.kubeconfig
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

@@ -13,6 +13,23 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
- Application Type: Other
Now test authentication with Google Identity Platform.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://accounts.google.com \
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
You should get claims like:
```
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://accounts.google.com
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
17:21:32.052683 get_token.go:57: ID token has the claim: aud=YOUR_CLIENT_ID.apps.googleusercontent.com
```
## 2. Setup Kubernetes API server
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
@@ -33,7 +50,7 @@ spec:
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to you.
Here assign the `cluster-admin` role to your subject.
```yaml
kind: ClusterRoleBinding
@@ -46,7 +63,7 @@ roleRef:
name: cluster-admin
subjects:
- kind: User
name: https://accounts.google.com#1234567890
name: YOUR_SUBJECT
```
You can create a custom role and assign it as well.
@@ -56,6 +73,19 @@ You can create a custom role and assign it as well.
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: google
name: google@example.k8s.local
current-context: google@example.k8s.local
kind: Config
preferences: {}
users:
- name: google
user:
@@ -69,9 +99,11 @@ users:
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 5. Run kubectl
Make sure you can access to the Kubernetes cluster.
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes

View File

@@ -28,6 +28,24 @@ You can associate client roles by adding the following mapper:
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
Now test authentication with the Keycloak.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--oidc-client-id=kubernetes \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
You should get claims like:
```
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://keycloak.example.com/auth/realms/YOUR_REALM
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
17:21:32.052683 get_token.go:57: ID token has the claim: aud=kubernetes
17:21:32.052694 get_token.go:57: ID token has the claim: groups=[kubernetes:admin]
```
## 2. Setup Kubernetes API server
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
@@ -73,6 +91,19 @@ You can create a custom role and assign it as well.
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: keycloak
name: keycloak@example.k8s.local
current-context: keycloak@example.k8s.local
kind: Config
preferences: {}
users:
- name: keycloak
user:
@@ -86,9 +117,11 @@ users:
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 5. Run kubectl
Make sure you can access to the Kubernetes cluster.
Make sure you can access the Kubernetes cluster.
```
% kubectl get nodes

View File

@@ -1,42 +0,0 @@
# Team on-boarding
## kops
Export the kubeconfig.
```sh
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
```
Remove the `admin` access from the kubeconfig.
It should look as like:
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubelogin
args:
- 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.

View File

@@ -2,19 +2,21 @@ package e2e_test
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/di"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/models/credentialplugin"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases"
)
// Run the integration tests of the credential plugin use-case.
@@ -26,6 +28,15 @@ import (
//
func TestCmd_Run_CredentialPlugin(t *testing.T) {
timeout := 1 * time.Second
cacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
}
defer func() {
if err := os.RemoveAll(cacheDir); err != nil {
t.Errorf("could not clean up the cache dir: %s", err)
}
}()
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
@@ -56,7 +67,7 @@ func TestCmd_Run_CredentialPlugin(t *testing.T) {
runGetTokenCmd(t, ctx, req, credentialPluginInteraction,
"--skip-open-browser",
"--listen-port", "0",
"--token-cache", "/dev/null",
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
)

View File

@@ -1,4 +1 @@
/CA
*.key
*.csr
*.crt

View File

@@ -1,13 +1,13 @@
all: ca.key ca.crt server.key server.crt jws.key
.PHONY: clean
all: server.crt ca.crt jws.key
clean:
rm -v ca.* server.*
-rm -v ca.* server.* jws.*
ca.key:
openssl genrsa -out $@ 1024
.INTERMEDIATE: ca.csr
ca.csr: openssl.cnf ca.key
openssl req -config openssl.cnf \
-new \
@@ -26,6 +26,7 @@ ca.crt: ca.csr ca.key
server.key:
openssl genrsa -out $@ 1024
.INTERMEDIATE: server.csr
server.csr: openssl.cnf server.key
openssl req -config openssl.cnf \
-new \

11
e2e_test/keys/testdata/ca.crt vendored Normal file
View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBnTCCAQYCCQCuPrhkr+BvGzANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
ZWxsbyBDQTAeFw0xOTA4MTgwNjAwMDZaFw0xOTA5MTcwNjAwMDZaMBMxETAPBgNV
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
ACfgNePlOLnLz1zJrWN6RZ6q0a+SSK8HdgSiKSF66SBIRILFoQmapBLXRY9YyATt
cdgg7pOd1WGCMqlOnhL56c8X5n+j/LGM5hc9PaEJA5vru7EBrnbxCkg0n8yp4Swc
8KFV5IiZ5D8t03AHjrXLQg8/HRzTuFRJJ1nJmc+FbnjT
-----END CERTIFICATE-----

15
e2e_test/keys/testdata/ca.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDnSTDsRx4UJmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ
4Pc51itOycWN1nclNVb489i9J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3L
u4mysxyOtYZNsdST8GBCP0m42tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQAB
AoGAaYmTYm29QvKW4et9oPxDjpYG0bqlz7P0xFRR9kKtKTATAMHjWeu2xFR/JI+b
rvJLIdZqHmWe5AmMb3NxZgfLonEB71ohaKQha1L8Vc7aoedRheJvqqaNr+ZxoCMO
8xcjsaMYxLEVt0tg6XyKyEhi1/hOufFZ4BSng4oQbrpaNIkCQQD6hPEzzPZtMEEe
eRdwTVUIStKFMQbRdwZ5Oc7pyDk2U+SFRJiqkBkqnmFekcf2UgbBQxem+GMhWNgE
LItKy/wVAkEA7FiHxbzn2msaE3hZCWudtnXqmJNuPO0zJ5icXe2svwmwPfLA/rm9
iazCuyzyK67J8IG9QgIjQFYXtQbMr2chGwJBAMm3dghBx0LQEf8Zfdf9TLSqmqyI
d3b+IgZGl+cCQ58NGfp863ibIsiAUuK0+4/JKItBHLBjXF6jjPx/aYFGkqkCQH4w
GnXCEYx1qJuCow87jR4xQQsrlC0lfC2E9t/TmWr6UkYRCWg3ZXJPcj0bl0Upcppd
ut22ZHniPZAizEBOcMcCQQDnMEOxufxhMsx2NC8yON/noewLINqKcbkMsl6DjvJl
+wLbQmzJ0j+uIBgdpsj4rWnEr7GxoL2eWG44QDUBco79
-----END RSA PRIVATE KEY-----

15
e2e_test/keys/testdata/jws.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQCZukkN1GxMlNXkpOZxCnvCF874/rn1sNKzO98fOwmBRPG2+m/c
yqBY7t2L2nihqz3+GZTiHmzSrBzMAGPVW1qGmk9KYg3m7akz9SiCxdoUkgM9MCCp
X/s8IhgtXkyoKFPcGdwHblDl/3aJG02b6TAQD8vTNQAKoKw7L0FST+pvRwIDAQAB
AoGBAJT1fXR5MbfDQL+dSe6fSex5RYTgzzDTdldW3I1Wl487Tz0OzvYTIe0LCIJL
4DhHxnpCL5IsCSbav8ytVA+ZxczHpEW6UxbalXt5UfgFu0joTrdoGxDcVWgUCW3J
Olbln0lOP55wViKh509gt45Za3VxJrNul3khVfVj7qGG9cKBAkEAxwtT8LxwqTYF
nqoeZvPp15JAqlgdk38ttJa4KEqvpBTSxNIXkL9T5gJ+irKZAzxlz/U7bhn5mw6E
3xFiljOXpQJBAMW3XRFOjgNBXNjbt81wREF5LdZl9EI8cRMSH6xljt2uwSqw4EG3
76gFvccUd+WnfspFQZVypSSD4pWzsAqh13sCQQCA9BLW5Y7r4ab0a2y08JNwaT1h
3yKSO5QF6pu25uQyHpeKkj5YNcyKONV40EqXsRqZB10QcN2omlh1GJNRkm1NAkEA
qV3lr4mnRUqcinfM/4MINT3k8h/sGUFFa5y+3SMyOtwURMm3kRRLi5c/dmYmPug4
SHUDNU48AQeo9awzRShWOQJBAJdw+cfRgi4fo3HY33uZdFa1T9G+qTA2ijhco6O3
8tOc0yOFEtPNXM87MsHGIQP3ZCLfIY1gs2O3WCTFbPxR4rc=
-----END RSA PRIVATE KEY-----

View File

@@ -10,7 +10,7 @@ new_certs_dir = $dir
default_md = sha256
policy = policy_match
serial = $dir/serial
default_days = 365
default_days = 3650
[ policy_match ]
countryName = optional

52
e2e_test/keys/testdata/server.crt vendored Normal file
View File

@@ -0,0 +1,52 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 0 (0x0)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Hello CA
Validity
Not Before: Aug 18 06:00:06 2019 GMT
Not After : Aug 15 06:00:06 2029 GMT
Subject: CN=localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (1024 bit)
Modulus:
00:d6:4e:eb:3a:cb:25:f9:7e:92:22:f2:63:99:da:
08:05:8b:a3:e7:d3:fd:71:3e:bd:da:c5:d5:63:b7:
d3:7b:f8:cd:1a:2e:5c:a2:4f:48:98:c2:b4:da:e8:
1e:d3:d7:8f:d8:ee:a9:70:d0:9d:4f:f4:8d:95:e5:
8e:9a:71:b6:80:aa:0b:cb:28:1d:f6:0d:7e:aa:78:
bf:30:e6:58:d7:6b:92:8f:19:1c:7d:95:f8:d5:2f:
8c:58:49:98:88:05:50:88:80:a9:77:c4:16:b4:c1:
00:45:1e:d3:d0:ed:98:4d:f7:a3:5d:f1:82:cb:a5:
4d:19:64:4d:43:db:13:d4:17
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
DNS:localhost
Signature Algorithm: sha256WithRSAEncryption
5a:5c:5e:8b:de:82:86:f4:98:40:0e:cf:c5:51:fe:89:46:49:
f0:26:d2:a5:06:e3:91:43:c1:f8:b2:ad:b7:a1:23:13:1a:80:
45:00:51:70:b6:06:63:c6:a8:c8:22:5d:1b:00:e0:4a:8c:2e:
ce:b4:da:b1:89:8a:d2:d0:e3:eb:0f:16:34:45:a1:bd:64:5c:
48:41:8c:0a:bf:66:be:1c:a8:35:47:ce:b0:dc:c8:4f:5e:c1:
ec:ef:21:fb:45:55:95:e3:99:40:46:0b:6c:8a:b3:d5:f0:bf:
39:a4:ba:c4:d7:58:88:58:08:07:98:59:6e:ca:9c:08:e4:c4:
4f:db
-----BEGIN CERTIFICATE-----
MIIBzTCCATagAwIBAgIBADANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhIZWxs
byBDQTAeFw0xOTA4MTgwNjAwMDZaFw0yOTA4MTUwNjAwMDZaMBQxEjAQBgNVBAMM
CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1k7rOssl+X6S
IvJjmdoIBYuj59P9cT692sXVY7fTe/jNGi5cok9ImMK02uge09eP2O6pcNCdT/SN
leWOmnG2gKoLyygd9g1+qni/MOZY12uSjxkcfZX41S+MWEmYiAVQiICpd8QWtMEA
RR7T0O2YTfejXfGCy6VNGWRNQ9sT1BcCAwEAAaMwMC4wCQYDVR0TBAIwADALBgNV
HQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4GB
AFpcXovegob0mEAOz8VR/olGSfAm0qUG45FDwfiyrbehIxMagEUAUXC2BmPGqMgi
XRsA4EqMLs602rGJitLQ4+sPFjRFob1kXEhBjAq/Zr4cqDVHzrDcyE9ewezvIftF
VZXjmUBGC2yKs9XwvzmkusTXWIhYCAeYWW7KnAjkxE/b
-----END CERTIFICATE-----

15
e2e_test/keys/testdata/server.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDWTus6yyX5fpIi8mOZ2ggFi6Pn0/1xPr3axdVjt9N7+M0aLlyi
T0iYwrTa6B7T14/Y7qlw0J1P9I2V5Y6acbaAqgvLKB32DX6qeL8w5ljXa5KPGRx9
lfjVL4xYSZiIBVCIgKl3xBa0wQBFHtPQ7ZhN96Nd8YLLpU0ZZE1D2xPUFwIDAQAB
AoGBAJhNR7Dl1JwFzndViWE6aP7/6UEFEBWeADDs7aTLbFmrTJ+xmRWkgLRHk14L
HnVwuYLywaoyJ8o9wy1nEbxC2e4zWZ94d351MQf3/komCXDBzEsktfAcNsAFnMmS
HZuGXfhi0FYWoftpIGxUmEBmQRcq0ctycbLves6TY3y+oajpAkEA8UHmSr/zsM3E
XQXPp2BCAvRrTH/njk4R0jwB29Bi89gt/XDD4uvfWbHw7TZxnZuCpWisnxpMPIwa
1rjqIQmhEwJBAONncQUOxwYCIuvraIhV0QtkIUa+YpTvAxP8ZNXx+agtHmHG2TTf
kGv2YddvjxXZItN/FZOzUGm9OptaeLRTpW0CQHO8CEzNnoqve0agtgf2LlSaiiqt
pRhoLTZsYPvhEMcnapCNGvtt6bxul0REfOZ9poPRHhZJGE9naqydEnv80Y8CQQC3
pxLfws95SsBpR/VkJepuCK/XMmrrXRxfR7coEgROjiG7VZyV1vgMOS9Ljg1A19wI
cto6LtcCjpCGZsqU1/kBAkEAv2tXBts3vuIjguZNMz7KLWmu3zG2SQRaqdEZwL+R
DQmD5tbI6gEtd5OmgmSiW8A4mpfgFYvG7Um2fwi7TTXtSA==
-----END RSA PRIVATE KEY-----

View File

@@ -1,7 +1,7 @@
package logger
import (
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
func New(t testingLogger) *logger.Logger {

View File

@@ -11,14 +11,14 @@ import (
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/di"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/e2e_test/kubeconfig"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases"
)
var (
@@ -124,8 +124,6 @@ func TestCmd_Run_Login(t *testing.T) {
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))
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/golang/mock v1.3.1
github.com/google/wire v0.3.0
github.com/int128/oauth2cli v1.4.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
github.com/spf13/pflag v1.0.3

View File

@@ -4,7 +4,7 @@ import (
"context"
"os"
"github.com/int128/kubelogin/di"
"github.com/int128/kubelogin/pkg/di"
)
var version = "HEAD"

View File

@@ -4,10 +4,21 @@ metadata:
name: oidc-login
spec:
homepage: https://github.com/int128/kubelogin
shortDescription: kubectl integration for OpenID Connect authentication
shortDescription: Log in to the OpenID Connect provider
description: |
Kubelogin integrates browser based authentication with kubectl.
You do not need to manually set an ID token and refresh token to the kubeconfig.
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
## Credential plugin mode
kubectl executes oidc-login before calling the Kubernetes APIs.
oidc-login automatically opens the browser and you can log in to the provider.
After authentication, kubectl gets the token from oidc-login and you can access the cluster.
See https://github.com/int128/kubelogin#credential-plugin-mode for more.
## Standalone mode
Run `kubectl oidc-login`.
It automatically opens the browser and you can log in to the provider.
After authentication, it writes the token to the kubeconfig and you can access the cluster.
See https://github.com/int128/kubelogin#standalone-mode for more.
caveats: |
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.

View File

@@ -6,9 +6,9 @@ import (
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
@@ -31,7 +31,7 @@ const examples = ` # Login to the provider using the authorization code flow.
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
var defaultListenPort = []int{8000, 18000}
var defaultTokenCache = homedir.HomeDir() + "/.kube/oidc-login.token-cache"
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
@@ -152,7 +152,7 @@ type getTokenOptions struct {
CertificateAuthority string
SkipTLSVerify bool
Verbose int
TokenCacheFilename string
TokenCacheDir string
}
func (o *getTokenOptions) register(f *pflag.FlagSet) {
@@ -165,7 +165,7 @@ func (o *getTokenOptions) register(f *pflag.FlagSet) {
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.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
f.StringVar(&o.TokenCacheFilename, "token-cache", defaultTokenCache, "Path to a file for caching the token")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
}
func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
@@ -188,17 +188,17 @@ func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
RunE: func(*cobra.Command, []string) error {
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.GetTokenIn{
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,
TokenCacheFilename: o.TokenCacheFilename,
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,
}
if err := cmd.GetToken.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)

View File

@@ -5,10 +5,10 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
)
func TestCmd_Run(t *testing.T) {
@@ -104,10 +104,10 @@ func TestCmd_Run(t *testing.T) {
getToken := mock_usecases.NewMockGetToken(ctrl)
getToken.EXPECT().
Do(ctx, usecases.GetTokenIn{
ListenPort: defaultListenPort,
TokenCacheFilename: defaultTokenCache,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ListenPort: defaultListenPort,
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
})
logger := mock_adaptors.NewLogger(t, ctrl)
@@ -135,17 +135,17 @@ func TestCmd_Run(t *testing.T) {
getToken := mock_usecases.NewMockGetToken(ctrl)
getToken.EXPECT().
Do(ctx, usecases.GetTokenIn{
TokenCacheFilename: defaultTokenCache,
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",
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",
})
logger := mock_adaptors.NewLogger(t, ctrl)

View File

@@ -6,8 +6,8 @@ import (
"os"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"golang.org/x/xerrors"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"

View File

@@ -6,7 +6,7 @@ import (
"syscall"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
)

View File

@@ -4,11 +4,11 @@ import (
"context"
"time"
"github.com/int128/kubelogin/models/credentialplugin"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,Env,Logger
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/pkg/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,Env,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
@@ -20,8 +20,8 @@ type Kubeconfig interface {
}
type TokenCacheRepository interface {
Read(filename string) (*credentialplugin.TokenCache, error)
Write(filename string, tc credentialplugin.TokenCache) error
FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error)
Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error
}
type CredentialPluginInteraction interface {
@@ -42,7 +42,6 @@ type OIDCClientConfig struct {
type OIDCClient interface {
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*OIDCVerifyOut, error)
Refresh(ctx context.Context, in OIDCRefreshIn) (*OIDCAuthenticateOut, error)
}
@@ -68,23 +67,20 @@ type OIDCAuthenticateOut struct {
IDTokenClaims map[string]string // string representation of claims for logging
}
// OIDCVerifyIn represents an input DTO of OIDCClient.Verify.
type OIDCVerifyIn struct {
IDToken string
RefreshToken string
}
// OIDCVerifyIn represents an output DTO of OIDCClient.Verify.
type OIDCVerifyOut struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
// OIDCRefreshIn represents an input DTO of OIDCClient.Refresh.
type OIDCRefreshIn struct {
RefreshToken string
}
type OIDCDecoder interface {
DecodeIDToken(t string) (*DecodedIDToken, error)
}
type DecodedIDToken struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
type Env interface {
ReadPassword(prompt string) (string, error)
}

View File

@@ -2,7 +2,7 @@ package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
// Set provides an implementation and interface for Kubeconfig.

View File

@@ -3,7 +3,7 @@ package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"

View File

@@ -5,7 +5,7 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)

View File

@@ -3,7 +3,7 @@ package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
)

View File

@@ -5,7 +5,7 @@ import (
"os"
"testing"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"os"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
// Set provides an implementation and interface for Logger.

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
type mockDebugLogger struct {

View File

@@ -2,7 +2,7 @@ package mock_adaptors
import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,Env,Logger)
// Source: github.com/int128/kubelogin/pkg/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,Env,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
@@ -7,9 +7,9 @@ package mock_adaptors
import (
context "context"
gomock "github.com/golang/mock/gomock"
adaptors "github.com/int128/kubelogin/adaptors"
credentialplugin "github.com/int128/kubelogin/models/credentialplugin"
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
adaptors "github.com/int128/kubelogin/pkg/adaptors"
credentialplugin "github.com/int128/kubelogin/pkg/models/credentialplugin"
kubeconfig "github.com/int128/kubelogin/pkg/models/kubeconfig"
reflect "reflect"
)
@@ -84,29 +84,29 @@ func (m *MockTokenCacheRepository) EXPECT() *MockTokenCacheRepositoryMockRecorde
return m.recorder
}
// Read mocks base method
func (m *MockTokenCacheRepository) Read(arg0 string) (*credentialplugin.TokenCache, error) {
ret := m.ctrl.Call(m, "Read", arg0)
// FindByKey mocks base method
func (m *MockTokenCacheRepository) FindByKey(arg0 string, arg1 credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error) {
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*credentialplugin.TokenCache)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read
func (mr *MockTokenCacheRepositoryMockRecorder) Read(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTokenCacheRepository)(nil).Read), arg0)
// FindByKey indicates an expected call of FindByKey
func (mr *MockTokenCacheRepositoryMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockTokenCacheRepository)(nil).FindByKey), arg0, arg1)
}
// Write mocks base method
func (m *MockTokenCacheRepository) Write(arg0 string, arg1 credentialplugin.TokenCache) error {
ret := m.ctrl.Call(m, "Write", arg0, arg1)
// Save mocks base method
func (m *MockTokenCacheRepository) Save(arg0 string, arg1 credentialplugin.TokenCacheKey, arg2 credentialplugin.TokenCache) error {
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write
func (mr *MockTokenCacheRepositoryMockRecorder) Write(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockTokenCacheRepository)(nil).Write), arg0, arg1)
// Save indicates an expected call of Save
func (mr *MockTokenCacheRepositoryMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockTokenCacheRepository)(nil).Save), arg0, arg1, arg2)
}
// MockCredentialPluginInteraction is a mock of CredentialPluginInteraction interface
@@ -242,17 +242,40 @@ func (mr *MockOIDCClientMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockOIDCClient)(nil).Refresh), arg0, arg1)
}
// Verify mocks base method
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCVerifyOut)
// MockOIDCDecoder is a mock of OIDCDecoder interface
type MockOIDCDecoder struct {
ctrl *gomock.Controller
recorder *MockOIDCDecoderMockRecorder
}
// MockOIDCDecoderMockRecorder is the mock recorder for MockOIDCDecoder
type MockOIDCDecoderMockRecorder struct {
mock *MockOIDCDecoder
}
// NewMockOIDCDecoder creates a new mock instance
func NewMockOIDCDecoder(ctrl *gomock.Controller) *MockOIDCDecoder {
mock := &MockOIDCDecoder{ctrl: ctrl}
mock.recorder = &MockOIDCDecoderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDCDecoder) EXPECT() *MockOIDCDecoderMockRecorder {
return m.recorder
}
// DecodeIDToken mocks base method
func (m *MockOIDCDecoder) DecodeIDToken(arg0 string) (*adaptors.DecodedIDToken, error) {
ret := m.ctrl.Call(m, "DecodeIDToken", arg0)
ret0, _ := ret[0].(*adaptors.DecodedIDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify
func (mr *MockOIDCClientMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDCClient)(nil).Verify), arg0, arg1)
// DecodeIDToken indicates an expected call of DecodeIDToken
func (mr *MockOIDCDecoderMockRecorder) DecodeIDToken(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeIDToken", reflect.TypeOf((*MockOIDCDecoder)(nil).DecodeIDToken), arg0)
}
// MockEnv is a mock of Env interface

View File

@@ -0,0 +1,53 @@
package oidc
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/xerrors"
)
type Decoder struct{}
// DecodeIDToken returns the claims of the ID token.
// Note that this method does not verify the signature and always trust it.
func (d *Decoder) DecodeIDToken(t string) (*adaptors.DecodedIDToken, error) {
parts := strings.Split(t, ".")
if len(parts) != 3 {
return nil, xerrors.Errorf("token contains an invalid number of segments")
}
b, err := jwt.DecodeSegment(parts[1])
if err != nil {
return nil, xerrors.Errorf("could not decode the token: %w", err)
}
var claims jwt.StandardClaims
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&claims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
var rawClaims map[string]interface{}
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 &adaptors.DecodedIDToken{
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
IDTokenClaims: dumpRawClaims(rawClaims),
}, nil
}
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
claims := make(map[string]string)
for k, v := range rawClaims {
switch v.(type) {
case float64:
claims[k] = fmt.Sprintf("%.f", v.(float64))
default:
claims[k] = fmt.Sprintf("%v", v)
}
}
return claims
}

View File

@@ -0,0 +1,87 @@
package oidc
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
)
func TestDecoder_DecodeIDToken(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)
if err != nil {
t.Fatalf("DecodeIDToken error: %s", err)
}
if decodedToken.IDTokenExpiry != expiry {
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry)
}
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims)
})
t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
if err == nil {
t.Errorf("error wants non-nil but nil")
} else {
t.Logf("expected error: %+v", err)
}
if decodedToken != nil {
t.Errorf("decodedToken wants nil but %+v", decodedToken)
}
})
}
func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
t.Helper()
claims := struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
EmailVerified bool `json:"email_verified"`
}{
StandardClaims: jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: expiry.Unix(),
},
Nonce: "NONCE",
Groups: []string{"admin", "users"},
EmailVerified: false,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(readPrivateKey(t, "testdata/jws.key"))
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func readPrivateKey(t *testing.T, name string) *rsa.PrivateKey {
t.Helper()
b, err := ioutil.ReadFile(name)
if err != nil {
t.Fatalf("could not read the file: %s", err)
}
block, rest := pem.Decode(b)
if block == nil {
t.Fatalf("could not decode PEM")
}
if len(rest) > 0 {
t.Fatalf("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("could not parse the key: %s", err)
}
return k
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
const (
@@ -24,7 +24,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the request: %s", err)
return t.Base.RoundTrip(req)
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
@@ -34,7 +34,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
}
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the response: %s", err)
return resp, err
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))

View File

@@ -8,8 +8,8 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
)
type mockTransport struct {

View File

@@ -11,9 +11,9 @@ import (
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/int128/kubelogin/adaptors/oidc/tls"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/oidc/logging"
"github.com/int128/kubelogin/pkg/adaptors/oidc/tls"
"github.com/int128/oauth2cli"
"github.com/pkg/browser"
"golang.org/x/oauth2"
@@ -31,6 +31,8 @@ func init() {
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(adaptors.OIDC), new(*Factory)),
wire.Struct(new(Decoder)),
wire.Bind(new(adaptors.OIDCDecoder), new(*Decoder)),
)
type Factory struct {
@@ -165,28 +167,6 @@ func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAut
}, nil
}
// Verify checks client ID and signature of the ID token.
// This does not check the expiration and caller should check it.
func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
ctx = c.wrapContext(ctx)
verifier := c.provider.Verifier(&oidc.Config{
ClientID: c.oauth2Config.ClientID,
SkipExpiryCheck: true,
})
verifiedIDToken, err := verifier.Verify(ctx, in.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.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return &adaptors.OIDCVerifyOut{
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
ctx = c.wrapContext(ctx)
@@ -223,17 +203,5 @@ func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adapt
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
claims := make(map[string]string)
for k, v := range rawClaims {
switch v.(type) {
case float64:
claims[k] = fmt.Sprintf("%f", v.(float64))
default:
claims[k] = fmt.Sprintf("%s", v)
}
}
if err != nil {
return claims, xerrors.Errorf("error while decoding the ID token: %w", err)
}
return claims, nil
return dumpRawClaims(rawClaims), err
}

8
pkg/adaptors/oidc/testdata/Makefile vendored Normal file
View File

@@ -0,0 +1,8 @@
all: jws.key
jws.key:
openssl genrsa -out $@ 1024
.PHONY: clean
clean:
-rm -v jws.key

15
pkg/adaptors/oidc/testdata/jws.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrH34yA/f/sBOUlkYnRtd2jgDZ3WivhidqvoQaa73xqTazbkn6
GZ9r7jx0CGLRV2bmErj2WoyT54yrhezrKh0YXAHlrwLdsmV4dwiV0lOfUJd9P/vF
e2hiAWv4CcO9ZuNkTsrxM5W8Wdj2tjqOvsIn4We+HWPkpknT7VtT5RrumwIDAQAB
AoGAFqy5oA7+kZbXQV0YNqQgcMkoO7Ym5Ps1xeMwxf94z8jIQsZebxFuGnMa95UU
4wBd1ias85fUANUxwpigaBjQee5Hk+dnfUe1snUWYNm9H6tKrXEF8ajer3a2knEv
GfK0CSEumFougfW2xG88ChGTS60wc+MIRfXERCvWpGm/5EECQQDdv5IBSi89g/R1
5AGZKFCoqr6Zw5bWEKPzCCYJZzncR1ER9vP2AnMExM8Io/87WYvmpZIUrXJvQYm8
hkfVOcBZAkEAxY4VcqmRWru3zmnbj21MwcwtgESaONkWsHeYs1C/Y/3zt7TuelYz
ZJ9aUuUsaiJLEs9Y26nMt0L0snWGr2noEwJBANaDp1PWFyMUTt3pB17JcFXqb15C
pt1I1cGapWk9Uez1lMijNNhNAEWhuoKqW5Nnif5DN7EHJYfZR8x3vm/YYWkCQHAA
0iAkCwjKDLe2RIjYiwAE5ncmbdl1GuwJokVnrlrei+LHbb1mSdTuk6MT006JCs8r
R1GivzHXgCv9fdLN1IkCQHxRvv9RPND80eEkdMv4qu0s22OLRhLQ/pb+YeT5Cjjv
pJYWKrvXdRZcuNde9JiiTgK2UW1wM8KeD/EGvK2yF6M=
-----END RSA PRIVATE KEY-----

View File

@@ -1,9 +1,8 @@
.PHONY: clean
all: ca1.crt ca1.crt.base64 ca2.crt ca2.crt.base64 ca3.crt ca3.crt.base64
.PHONY: clean
clean:
rm -v *.key *.csr *.crt *.base64
-rm -v *.key *.csr *.crt *.base64
%.key:
openssl genrsa -out $@ 1024

View File

@@ -6,7 +6,7 @@ import (
"encoding/base64"
"io/ioutil"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/xerrors"
)

View File

@@ -4,9 +4,9 @@ import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
func TestNewConfig(t *testing.T) {

View File

@@ -0,0 +1,64 @@
package tokencache
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"golang.org/x/xerrors"
)
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Repository), "*"),
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
)
// Repository provides access to the token cache on the local filesystem.
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
type Repository struct{}
func (r *Repository) FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error) {
filename := filepath.Join(dir, computeFilename(key))
f, err := os.Open(filename)
if err != nil {
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
}
defer f.Close()
d := json.NewDecoder(f)
var c credentialplugin.TokenCache
if err := d.Decode(&c); err != nil {
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
}
return &c, nil
}
func (r *Repository) Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return xerrors.Errorf("could not create directory %s: %w", dir, err)
}
filename := filepath.Join(dir, computeFilename(key))
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return xerrors.Errorf("could not create file %s: %w", filename, err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(&cache); err != nil {
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
}
return nil
}
func computeFilename(key credentialplugin.TokenCacheKey) string {
s := sha256.New()
_, _ = s.Write([]byte(key.IssuerURL))
_, _ = s.Write([]byte{0x00})
_, _ = s.Write([]byte(key.ClientID))
return hex.EncodeToString(s.Sum(nil))
}

View File

@@ -7,10 +7,10 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/models/credentialplugin"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
)
func TestRepository_Read(t *testing.T) {
func TestRepository_FindByKey(t *testing.T) {
var r Repository
t.Run("Success", func(t *testing.T) {
@@ -23,13 +23,17 @@ func TestRepository_Read(t *testing.T) {
t.Errorf("could not clean up the temp dir: %s", err)
}
}()
key := credentialplugin.TokenCacheKey{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
}
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
filename := filepath.Join(dir, "token-cache")
filename := filepath.Join(dir, computeFilename(key))
if err := ioutil.WriteFile(filename, []byte(json), 0600); err != nil {
t.Fatalf("could not write to the temp file: %s", err)
}
tokenCache, err := r.Read(filename)
tokenCache, err := r.FindByKey(dir, key)
if err != nil {
t.Errorf("err wants nil but %+v", err)
}
@@ -40,7 +44,7 @@ func TestRepository_Read(t *testing.T) {
})
}
func TestRepository_Write(t *testing.T) {
func TestRepository_Save(t *testing.T) {
var r Repository
t.Run("Success", func(t *testing.T) {
@@ -54,12 +58,16 @@ func TestRepository_Write(t *testing.T) {
}
}()
filename := filepath.Join(dir, "token-cache")
key := credentialplugin.TokenCacheKey{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
}
tokenCache := credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Write(filename, tokenCache); err != nil {
if err := r.Save(dir, key, tokenCache); err != nil {
t.Errorf("err wants nil but %+v", err)
}
filename := filepath.Join(dir, computeFilename(key))
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("could not read the token cache file: %s", err)

View File

@@ -5,18 +5,18 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
credentialPluginAdaptor "github.com/int128/kubelogin/adaptors/credentialplugin"
"github.com/int128/kubelogin/adaptors/env"
"github.com/int128/kubelogin/adaptors/kubeconfig"
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/adaptors/tokencache"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/auth"
credentialPluginUseCase "github.com/int128/kubelogin/usecases/credentialplugin"
"github.com/int128/kubelogin/usecases/login"
"github.com/int128/kubelogin/pkg/adaptors"
"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/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/auth"
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/login"
)
// NewCmd returns an instance of adaptors.Cmd.

View File

@@ -6,18 +6,18 @@
package di
import (
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"github.com/int128/kubelogin/adaptors/credentialplugin"
"github.com/int128/kubelogin/adaptors/env"
"github.com/int128/kubelogin/adaptors/kubeconfig"
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/adaptors/tokencache"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/auth"
credentialplugin2 "github.com/int128/kubelogin/usecases/credentialplugin"
"github.com/int128/kubelogin/usecases/login"
"github.com/int128/kubelogin/pkg/adaptors"
"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/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/auth"
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/login"
)
// Injectors from di.go:
@@ -27,12 +27,14 @@ func NewCmd() adaptors.Cmd {
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
showLocalServerURL := &auth.ShowLocalServerURL{
Logger: adaptorsLogger,
}
authentication := &auth.Authentication{
OIDC: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
@@ -63,9 +65,11 @@ func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL u
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
authentication := &auth.Authentication{
OIDC: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,

View File

@@ -3,7 +3,13 @@ package credentialplugin
import "time"
// TokenCache represents a token object cached.
// TokenCacheKey represents a key of a token cache.
type TokenCacheKey struct {
IssuerURL string
ClientID string
}
// TokenCache represents a token cache.
type TokenCache struct {
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`

View File

@@ -5,8 +5,8 @@ import (
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
@@ -39,12 +39,36 @@ const passwordPrompt = "Password: "
//
type Authentication struct {
OIDC adaptors.OIDC
OIDCDecoder adaptors.OIDCDecoder
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
if in.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "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.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
u.Logger.Debugf(1, "you already have a valid token until %s", token.IDTokenExpiry)
return &usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken,
IDTokenExpiry: token.IDTokenExpiry,
IDTokenClaims: token.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "you have an expired token at %s", token.IDTokenExpiry)
}
u.Logger.Debugf(1, "initializing an OIDC client")
client, err := u.OIDC.New(ctx, adaptors.OIDCClientConfig{
Config: in.OIDCConfig,
CACertFilename: in.CACertFilename,
@@ -54,27 +78,8 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
return nil, xerrors.Errorf("could not create an OIDC client: %w", err)
}
if in.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Verifying the existing token")
out, err := client.Verify(ctx, adaptors.OIDCVerifyIn{IDToken: in.OIDCConfig.IDToken})
if err != nil {
return nil, xerrors.Errorf("you need to remove the existing token manually: %w", err)
}
if out.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
u.Logger.Debugf(1, "You already have a valid token")
return &usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "You have an expired token at %s", out.IDTokenExpiry)
}
if in.OIDCConfig.RefreshToken != "" {
u.Logger.Debugf(1, "Refreshing the token")
u.Logger.Debugf(1, "refreshing the token")
out, err := client.Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: in.OIDCConfig.RefreshToken,
})
@@ -86,11 +91,11 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "Could not refresh the token: %s", err)
u.Logger.Debugf(1, "could not refresh the token: %s", err)
}
if in.Username == "" {
u.Logger.Debugf(1, "Performing the authentication code flow")
u.Logger.Debugf(1, "performing the authentication code flow")
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
@@ -107,7 +112,7 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
}, nil
}
u.Logger.Debugf(1, "Performing the resource owner password credentials flow")
u.Logger.Debugf(1, "performing the resource owner password credentials flow")
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {

View File

@@ -7,10 +7,10 @@ import (
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
@@ -220,22 +220,17 @@ func TestAuthentication_Do(t *testing.T) {
IDToken: "VALID_ID_TOKEN",
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "VALID_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("VALID_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDC: mock_adaptors.NewMockOIDC(ctrl),
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
@@ -264,13 +259,14 @@ func TestAuthentication_Do(t *testing.T) {
RefreshToken: "VALID_REFRESH_TOKEN",
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "VALID_REFRESH_TOKEN",
@@ -288,8 +284,9 @@ func TestAuthentication_Do(t *testing.T) {
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDC: mockOIDC,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
@@ -319,13 +316,14 @@ func TestAuthentication_Do(t *testing.T) {
RefreshToken: "EXPIRED_REFRESH_TOKEN",
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "EXPIRED_REFRESH_TOKEN",
@@ -348,8 +346,9 @@ func TestAuthentication_Do(t *testing.T) {
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDC: mockOIDC,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {

View File

@@ -7,10 +7,10 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/credentialplugin"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
@@ -29,10 +29,12 @@ type GetToken struct {
func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
tokenCache, err := u.TokenCacheRepository.Read(in.TokenCacheFilename)
u.Logger.Debugf(1, "finding a token from cache directory %s", in.TokenCacheDir)
cacheKey := credentialplugin.TokenCacheKey{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
if err != nil {
u.Logger.Debugf(1, "could not read the token cache file: %s", err)
tokenCache = &credentialplugin.TokenCache{}
u.Logger.Debugf(1, "could not find a token cache: %s", err)
cache = &credentialplugin.TokenCache{}
}
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
OIDCConfig: kubeconfig.OIDCConfig{
@@ -40,8 +42,8 @@ func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
IDToken: tokenCache.IDToken,
RefreshToken: tokenCache.RefreshToken,
IDToken: cache.IDToken,
RefreshToken: cache.RefreshToken,
},
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
@@ -54,20 +56,22 @@ func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
u.Logger.Debugf(1, "the ID token has the claim: %s=%v", k, v)
}
if !out.AlreadyHasValidIDToken {
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
if err := u.TokenCacheRepository.Write(in.TokenCacheFilename, credentialplugin.TokenCache{
cache := credentialplugin.TokenCache{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
}); err != nil {
}
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, cacheKey, cache); err != nil {
return xerrors.Errorf("could not write the token cache: %w", err)
}
}
u.Logger.Debugf(1, "writing the token to client-go")
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenExpiry}); err != nil {
return xerrors.Errorf("could not write a credential object: %w", err)
return xerrors.Errorf("could not write the token to client-go: %w", err)
}
return nil
}

View File

@@ -6,11 +6,11 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/credentialplugin"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
"golang.org/x/xerrors"
)
@@ -23,16 +23,16 @@ func TestGetToken_Do(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.GetTokenIn{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheFilename: "/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",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
@@ -57,13 +57,21 @@ func TestGetToken_Do(t *testing.T) {
}, nil)
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
tokenCacheRepository.EXPECT().
Read("/path/to/token-cache").
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
Return(nil, xerrors.New("file not found"))
tokenCacheRepository.EXPECT().
Write("/path/to/token-cache", credentialplugin.TokenCache{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
Save("/path/to/token-cache",
credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
},
credentialplugin.TokenCache{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
credentialPluginInteraction.EXPECT().
Write(credentialplugin.Output{
@@ -86,10 +94,10 @@ func TestGetToken_Do(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.GetTokenIn{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheFilename: "/path/to/token-cache",
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
@@ -109,7 +117,10 @@ func TestGetToken_Do(t *testing.T) {
}, nil)
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
tokenCacheRepository.EXPECT().
Read("/path/to/token-cache").
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
Return(&credentialplugin.TokenCache{
IDToken: "VALID_ID_TOKEN",
}, nil)
@@ -135,10 +146,10 @@ func TestGetToken_Do(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.GetTokenIn{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheFilename: "/path/to/token-cache",
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
@@ -152,7 +163,10 @@ func TestGetToken_Do(t *testing.T) {
Return(nil, xerrors.New("authentication error"))
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
tokenCacheRepository.EXPECT().
Read("/path/to/token-cache").
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
Return(nil, xerrors.New("file not found"))
u := GetToken{
Authentication: mockAuthentication,

View File

@@ -4,10 +4,10 @@ import (
"context"
"time"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,GetToken,Authentication
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/pkg/usecases Login,GetToken,Authentication
type Login interface {
Do(ctx context.Context, in LoginIn) error
@@ -38,17 +38,17 @@ type GetToken interface {
// GetTokenIn represents an input DTO of the GetToken use-case.
type GetTokenIn 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
TokenCacheFilename string
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
}
type Authentication interface {

View File

@@ -4,8 +4,8 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
@@ -42,8 +42,8 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Printf(oidcConfigErrorMessage)
return xerrors.Errorf("could not find the current authentication provider: %w", err)
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", authProvider.UserName)
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
u.Logger.Debugf(1, "using the authentication provider of the user %s", authProvider.UserName)
u.Logger.Debugf(1, "a token will be written to %s", authProvider.LocationOfOrigin)
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
OIDCConfig: authProvider.OIDCConfig,
@@ -58,7 +58,7 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
u.Logger.Debugf(1, "the ID token has the claim: %s=%v", k, v)
}
if out.AlreadyHasValidIDToken {
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
@@ -68,7 +68,7 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
authProvider.OIDCConfig.IDToken = out.IDToken
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
u.Logger.Debugf(1, "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)
}

View File

@@ -6,10 +6,10 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
"golang.org/x/xerrors"
)

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,GetToken,Authentication)
// Source: github.com/int128/kubelogin/pkg/usecases (interfaces: Login,GetToken,Authentication)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
@@ -7,7 +7,7 @@ package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
usecases "github.com/int128/kubelogin/usecases"
usecases "github.com/int128/kubelogin/pkg/usecases"
reflect "reflect"
)