Compare commits

..

43 Commits
1.3 ... 1.8

Author SHA1 Message Date
Hidetake Iwata
4b08a49a51 Add workaround for Go Modules issue 2018-10-31 14:21:33 +09:00
Hidetake Iwata
9c74f3748b Merge pull request #23 from int128/oauth2cli
Import github.com/int128/oauth2cli
2018-10-31 14:16:25 +09:00
Hidetake Iwata
17e03f2abc Import github.com/int128/oauth2cli 2018-10-31 14:13:09 +09:00
Hidetake Iwata
ebef81f9d7 Merge pull request #22 from int128/ignore-cert-errors
Skip invalid CA certs instead of raising error
2018-10-30 09:56:22 +09:00
Hidetake Iwata
8d0d82fb71 Skip invalid CA certs instead of raising error 2018-10-30 09:55:12 +09:00
Hidetake Iwata
b600e54a12 Refactor e2e test 2018-10-29 14:04:10 +09:00
Hidetake Iwata
75317f88a1 Merge pull request #21 from int128/go111
Move to Go Modules
2018-10-29 13:50:01 +09:00
Hidetake Iwata
5a794e8ceb Move to Go Modules 2018-10-29 13:45:32 +09:00
Hidetake Iwata
1fe1ec4c20 Update README.md 2018-10-16 09:47:50 +09:00
Hidetake Iwata
7676ffbfab Fix golint 2018-10-16 09:25:38 +09:00
Hidetake Iwata
7e1e6a096b Refactor docs 2018-10-15 15:52:52 +09:00
Hidetake Iwata
4d3d1c3b78 Add explanation about extra scopes 2018-10-04 16:05:36 +09:00
Hidetake Iwata
1ebdfc0e4f Use "Application Type: Other" on Google IdP 2018-10-03 15:33:56 +09:00
Hidetake Iwata
9c67c52b34 Fix test for #17 2018-09-14 15:06:05 +09:00
Hidetake Iwata
550396e1dd Merge pull request #17 from stang/allow-to-change-listening-port
Allow to change listening port
2018-09-14 11:20:08 +09:00
Stephane Tang
34f0578b59 Allow to change listening port
It's using port 8000 by default, which is identical as the original behavior.

Signed-off-by: Stephane Tang <hi@stang.sh>
2018-09-14 00:03:18 +01:00
Hidetake Iwata
604d118b68 Update README.md 2018-09-07 10:56:26 +09:00
Hidetake Iwata
91959e8a56 Update README.md 2018-09-07 10:56:21 +09:00
Hidetake Iwata
9b325a66a9 Show version on help 2018-09-05 12:54:00 +09:00
Hidetake Iwata
8b6257d60b Introduce goreleaser 2018-09-05 12:54:00 +09:00
Hidetake Iwata
d469df4978 Fix cli.Parse does not respect argument 2018-09-05 11:34:40 +09:00
Hidetake Iwata
3ae68df848 Merge pull request #14 from int128/ux
Improve error messages
2018-09-04 06:52:24 +09:00
Hidetake Iwata
e8805f7a94 Improve error messages 2018-09-04 06:49:49 +09:00
Hidetake Iwata
717da9d442 Merge pull request #15 from int128/refresh-token
Fix refresh token is not set with Google IdP
2018-09-04 06:41:58 +09:00
Hidetake Iwata
de176cfbaa Fix refresh token is not set with Google IdP 2018-09-03 14:42:00 +09:00
Hidetake Iwata
9bf8a89577 Merge pull request #13 from int128/extra-scopes
Add extra-scopes support
2018-09-02 14:20:35 +09:00
Hidetake Iwata
a91c020f46 Update README.md 2018-09-02 14:19:23 +09:00
Hidetake Iwata
d4fb49613d Add extra-scopes support 2018-08-31 21:02:34 +09:00
Hidetake Iwata
64b1d52208 Fix test says message if CLI returns error 2018-08-31 15:19:58 +09:00
Hidetake Iwata
a298058e3f Refactor test 2018-08-31 14:59:50 +09:00
Hidetake Iwata
309e73d8c0 Merge pull request #12 from int128/browser-delay
Add delay before opening browser
2018-08-31 09:30:05 +09:00
Hidetake Iwata
857d5dad88 Add delay before opening browser 2018-08-30 21:28:17 +09:00
Hidetake Iwata
455c920b65 Refactor e2e test 2018-08-30 14:47:03 +09:00
Hidetake Iwata
afad46817a Update README.md 2018-08-28 12:28:59 +09:00
Hidetake Iwata
4f506b9f62 Update README.md 2018-08-28 09:53:16 +09:00
Hidetake Iwata
72bc19bc10 Rename 2018-08-28 09:30:11 +09:00
Hidetake Iwata
69bcb16e26 Update README.md 2018-08-27 22:27:11 +09:00
Hidetake Iwata
978a45bcf1 Refactor 2018-08-27 14:49:25 +09:00
Hidetake Iwata
62b9a2158d Refactor 2018-08-26 12:33:35 +09:00
Hidetake Iwata
974fc5c526 Merge pull request #10 from int128/oidc-browser
Open browser automatically on authentication
2018-08-26 11:07:07 +09:00
Hidetake Iwata
2c7d958efd Close browser automatically 2018-08-25 21:53:41 +09:00
Hidetake Iwata
16b15cd21b Open browser automatically on authentication 2018-08-25 21:53:41 +09:00
Hidetake Iwata
3213572180 Polish 2018-08-24 15:22:09 +09:00
29 changed files with 940 additions and 641 deletions

View File

@@ -2,37 +2,41 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/int128/kubelogin
- image: circleci/golang:1.11.1
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/golang/lint/golint
- run: go get -u golang.org/x/lint/golint
- run: golint
# Workaround for https://github.com/golang/go/issues/27925
- run: go get -v k8s.io/client-go
- run: go vet
- run: go build -v
- run: make -C integration-test/testdata
- run: go test -v ./...
- run: make -C cli_test/authserver/testdata
- run: go test -v -race ./...
release:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/int128/kubelogin
- image: circleci/golang:1.11.1
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/mitchellh/gox
- run: go get github.com/tcnksm/ghr
- run: gox --osarch 'darwin/amd64 linux/amd64 windows/amd64 windows/386' -output 'dist/{{.Dir}}_{{.OS}}_{{.Arch}}'
- run: ghr -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" "$CIRCLE_TAG" dist
# Workaround for https://github.com/golang/go/issues/27925
- run: go get -v k8s.io/client-go
- run: go vet
- run: curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
build:
all:
jobs:
- build
- build:
filters:
tags:
only: /.*/
- release:
filters:
branches:
ignore: /.*/
tags:
only: /.*/
requires:
- build

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
/kubelogin
/dist
/.kubeconfig

19
.goreleaser.yml Normal file
View File

@@ -0,0 +1,19 @@
builds:
- binary: kubelogin
goos:
- windows
- darwin
- linux
goarch:
- amd64
archive:
files:
- none*
brew:
github:
owner: int128
name: homebrew-kubelogin
homepage: https://github.com/int128/kubelogin
description: "kubectl with OpenID Connect (OIDC) authentication"
test: system "#{bin}/kubelogin --help"
install: bin.install "kubelogin"

267
README.md
View File

@@ -1,181 +1,82 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin)
`kubelogin` is a command to get an OpenID Connect (OIDC) token for `kubectl` authentication.
This is a command for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
It gets a token from the OIDC provider and writes it to the kubeconfig.
## TL;DR
1. Setup your OpenID Connect provider, e.g. Google Identity Platform or Keycloak.
1. Setup your Kubernetes cluster.
1. Setup your `kubectl`.
You need to setup the following components:
- OIDC provider
- Kubernetes API server
- Role for your group or user
- kubectl authentication
You can install this by brew tap or from the [releases](https://github.com/int128/kubelogin/releases).
```sh
brew tap int128/kubelogin
brew install kubelogin
```
After initial setup or when the token has been expired, just run `kubelogin`.
```
% kubelogin
2018/08/27 15:03:06 Reading /home/user/.kube/config
2018/08/27 15:03:06 Using current context: hello.k8s.local
2018/08/27 15:03:07 Open http://localhost:8000 for authorization
2018/08/27 15:03:07 GET /
2018/08/27 15:03:08 GET /?state=a51081925f20c043&session_state=5637cbdf-ffdc-4fab-9fc7-68a3e6f2e73f&code=ey...
2018/08/27 15:03:09 Got token for subject=cf228a73-47fe-4986-a2a8-b2ced80a884b
2018/08/27 15:03:09 Updated /home/user/.kube/config
```
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
For more, see the following documents:
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
## Configuration
This supports the following options.
```
% kubelogin --help
2018/08/15 19:08:58 Usage:
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
Help Options:
-h, --help Show this help message
```
This also supports the following keys of `auth-provider` in kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
## Getting Started with Google Account
### 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client as follows:
- Application Type: Web application
- Redirect URL: `http://localhost:8000/`
### 2. Setup Kubernetes cluster
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
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.apps.googleusercontent.com
```
Here assign the `cluster-admin` role to your user.
```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#1234567890
```
### 3. Setup kubectl and kubelogin
Setup `kubectl` to authenticate with your identity provider.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
Run `kubelogin` and open http://localhost:8000 in your browser.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
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
```
Key | Direction | Value
----|-----------|------
`idp-issuer-url` | IN (Required) | Issuer URL of the provider.
`client-id` | IN (Required) | Client ID of the provider.
`client-secret` | IN (Required) | Client Secret of the provider.
`idp-certificate-authority` | IN (Optional) | CA certificate path of the provider.
`idp-certificate-authority-data` | IN (Optional) | Base64 encoded CA certificate of the provider.
`extra-scopes` | IN (Optional) | Scopes to request to the provider (comma separated).
`id-token` | OUT | ID token got from the provider.
`refresh-token` | OUT | Refresh token got from the provider.
## Getting Started with Keycloak
### 1. Setup Keycloak
Create an OIDC client as follows:
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
- Client ID: `kubernetes`
- Groups claim: `groups`
Then create a group `kubernetes:admin` and join to it.
### 2. Setup Kubernetes cluster
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
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: kubernetes
oidcGroupsClaim: groups
```
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
```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: Group
name: /kubernetes:admin
```
### 3. Setup kubectl and kubelogin
Setup `kubectl` to authenticate with your identity provider.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
Run `kubelogin` and make sure you can access to the cluster.
See the previous section for details.
## Configuration
### Kubeconfig
### Kubeconfig path
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
@@ -184,51 +85,37 @@ Default to `~/.kube/config`.
export KUBECONFIG="$PWD/.kubeconfig"
```
### OpenID Connect Provider CA Certificate
You can specify the CA certificate of your OpenID Connect provider by [`idp-certificate-authority` or `idp-certificate-authority-data` in the kubeconfig](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
### Extra scopes
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
### Setup script
In actual team operation, you can share the following script to your team members for easy setup.
Note that kubectl does not accept multiple scopes and you need to edit the kubeconfig as like:
```sh
#!/bin/sh -xe
CLUSTER_NAME="hello.k8s.local"
export KUBECONFIG="$PWD/.kubeconfig"
kubectl config set-cluster "$CLUSTER_NAME" \
--server https://api-xxx.xxx.elb.amazonaws.com \
--certificate-authority "$PWD/cluster.crt"
kubectl config set-credentials "$CLUSTER_NAME" \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
kubectl config use-context "$CLUSTER_NAME"
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
## CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by `idp-certificate-authority` and `idp-certificate-authority-data` in the kubeconfig.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```
If kubelogin could not parse the certificate, it shows a warning and skips it.
## Contributions
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests.
### Build
```sh
go get github.com/int128/kubelogin
```
### Release
CircleCI publishes the build to GitHub. See [.circleci/config.yml](.circleci/config.yml).
Feel free to open issues and pull requests for improving code and documents.

View File

@@ -1,107 +0,0 @@
package auth
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"net/http"
"golang.org/x/oauth2"
)
// BrowserAuthCodeFlow is a flow to get a token by browser interaction.
type BrowserAuthCodeFlow struct {
oauth2.Config
Port int // HTTP server port
}
// GetToken returns a token.
func (f *BrowserAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
f.Config.RedirectURL = fmt.Sprintf("http://localhost:%d/", f.Port)
state, err := generateState()
if err != nil {
return nil, fmt.Errorf("Could not generate state parameter: %s", err)
}
log.Printf("Open http://localhost:%d for authorization", f.Port)
code, err := f.getCode(ctx, &f.Config, state)
if err != nil {
return nil, err
}
token, err := f.Config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
}
return token, nil
}
func generateState() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", err
}
return fmt.Sprintf("%x", n), nil
}
func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config, state string) (string, error) {
codeCh := make(chan string)
errCh := make(chan error)
server := http.Server{
Addr: fmt.Sprintf(":%d", f.Port),
Handler: &handler{
AuthCodeURL: config.AuthCodeURL(state),
Callback: func(code string, actualState string, err error) {
switch {
case err != nil:
errCh <- err
case actualState != state:
errCh <- fmt.Errorf("OAuth state did not match, should be %s but %s", state, actualState)
default:
codeCh <- code
}
},
},
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
server.Shutdown(ctx)
return "", err
case code := <-codeCh:
server.Shutdown(ctx)
return code, nil
}
}
type handler struct {
AuthCodeURL string
Callback func(code string, state string, err error)
}
func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.RequestURI)
switch r.URL.Path {
case "/":
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
errorCode := r.URL.Query().Get("error")
errorDescription := r.URL.Query().Get("error_description")
switch {
case code != "":
s.Callback(code, state, nil)
fmt.Fprintf(w, "Back to command line.")
case errorCode != "":
s.Callback("", "", fmt.Errorf("OAuth Error: %s %s", errorCode, errorDescription))
fmt.Fprintf(w, "Back to command line.")
default:
http.Redirect(w, r, s.AuthCodeURL, 302)
}
default:
http.Error(w, "Not Found", 404)
}
}

View File

@@ -3,8 +3,11 @@ package auth
import (
"context"
"fmt"
"log"
"net/http"
oidc "github.com/coreos/go-oidc"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
)
@@ -12,49 +15,55 @@ import (
type TokenSet struct {
IDToken string
RefreshToken string
Claims *Claims
}
// Claims represents properties in the ID token.
type Claims struct {
Email string `json:"email"`
// Config represents OIDC configuration.
type Config struct {
Issuer string
ClientID string
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
// GetTokenSet retrieves a token from the OIDC provider.
func GetTokenSet(ctx context.Context, issuer string, clientID string, clientSecret string) (*TokenSet, error) {
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return nil, fmt.Errorf("Could not access OIDC issuer: %s", err)
// GetTokenSet retrives a token from the OIDC provider and returns a TokenSet.
func (c *Config) GetTokenSet(ctx context.Context) (*TokenSet, error) {
if c.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.Client)
}
flow := BrowserAuthCodeFlow{
Port: 8000,
provider, err := oidc.NewProvider(ctx, c.Issuer)
if err != nil {
return nil, fmt.Errorf("Could not discovery the OIDC issuer: %s", err)
}
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{oidc.ScopeOpenID, "email"},
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: c.LocalServerPort,
SkipOpenBrowser: c.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
}
token, err := flow.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("Could not get a token: %s", err)
}
rawIDToken, ok := token.Extra("id_token").(string)
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
idToken, err := verifier.Verify(ctx, rawIDToken)
verifier := provider.Verifier(&oidc.Config{ClientID: c.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, fmt.Errorf("Could not verify the id_token: %s", err)
}
var claims Claims
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("Could not extract claims from the token response: %s", err)
}
log.Printf("Got token for subject=%s", verifiedIDToken.Subject)
return &TokenSet{
IDToken: rawIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
Claims: &claims,
}, nil
}

View File

@@ -2,11 +2,7 @@ package cli
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"
@@ -14,14 +10,16 @@ import (
"github.com/int128/kubelogin/kubeconfig"
flags "github.com/jessevdk/go-flags"
homedir "github.com/mitchellh/go-homedir"
"golang.org/x/oauth2"
)
// Parse parses command line arguments and returns a CLI instance.
func Parse(args []string) (*CLI, error) {
func Parse(osArgs []string, version string) (*CLI, error) {
var cli CLI
parser := flags.NewParser(&cli, flags.HelpFlag)
args, err := parser.Parse()
parser.LongDescription = fmt.Sprintf(`Version %s
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
version)
args, err := parser.ParseArgs(osArgs[1:])
if err != nil {
return nil, err
}
@@ -33,83 +31,62 @@ func Parse(args []string) (*CLI, error) {
// CLI represents an interface of this command.
type CLI struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
// CertificateAuthority string `long:"certificate-authority" env:"KUBELOGIN_CERTIFICATE_AUTHORITY" description:"Path to a cert file for the certificate authority"`
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *CLI) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s", c.KubeConfig)
return "", fmt.Errorf("Could not expand %s: %s", c.KubeConfig, err)
}
return d, nil
}
// Run performs this command.
func (c *CLI) Run(ctx context.Context) error {
log.Printf("Reading %s", c.KubeConfig)
path, err := c.ExpandKubeConfig()
if err != nil {
return err
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Load(path)
cfg, err := kubeconfig.Read(path)
if err != nil {
return fmt.Errorf("Could not load kubeconfig: %s", err)
return fmt.Errorf("Could not read kubeconfig: %s", err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
log.Printf("Using current-context: %s", cfg.CurrentContext)
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
if err != nil {
return fmt.Errorf("Could not find auth-provider: %s", err)
return fmt.Errorf(`Could not find OIDC configuration in kubeconfig: %s
Did you setup kubectl for OIDC authentication?
kubectl config set-credentials %s \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`,
err, cfg.CurrentContext)
}
tlsConfig, err := c.tlsConfig(authProvider)
if err != nil {
return fmt.Errorf("Could not configure TLS: %s", err)
tlsConfig := c.tlsConfig(authProvider)
authConfig := &auth.Config{
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}},
LocalServerPort: c.ListenPort,
SkipOpenBrowser: c.SkipOpenBrowser,
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
token, err := auth.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
token, err := authConfig.GetTokenSet(ctx)
if err != nil {
return fmt.Errorf("Authentication error: %s", err)
return fmt.Errorf("Could not get token from OIDC provider: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
log.Printf("Updated %s", c.KubeConfig)
return nil
}
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProviderConfig) (*tls.Config, error) {
p := x509.NewCertPool()
if authProvider.IDPCertificateAuthority() != "" {
b, err := ioutil.ReadFile(authProvider.IDPCertificateAuthority())
if err != nil {
return nil, fmt.Errorf("Could not read idp-certificate-authority: %s", err)
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority: %s", err)
}
log.Printf("Using CA certificate: %s", authProvider.IDPCertificateAuthority())
}
if authProvider.IDPCertificateAuthorityData() != "" {
b, err := base64.StdEncoding.DecodeString(authProvider.IDPCertificateAuthorityData())
if err != nil {
return nil, fmt.Errorf("Could not decode idp-certificate-authority-data: %s", err)
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority-data: %s", err)
}
log.Printf("Using CA certificate of idp-certificate-authority-data")
}
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}
if len(p.Subjects()) > 0 {
cfg.RootCAs = p
}
return cfg, nil
}

35
cli/cli_test.go Normal file
View File

@@ -0,0 +1,35 @@
package cli
import (
"testing"
)
func TestParse(t *testing.T) {
c, err := Parse([]string{"kubelogin"}, "version")
if err != nil {
t.Errorf("Parse returned error: %s", err)
}
if c == nil {
t.Errorf("Parse should return CLI but nil")
}
}
func TestParse_TooManyArgs(t *testing.T) {
c, err := Parse([]string{"kubelogin", "some"}, "version")
if err == nil {
t.Errorf("Parse should return error but nil")
}
if c != nil {
t.Errorf("Parse should return nil but %+v", c)
}
}
func TestParse_Help(t *testing.T) {
c, err := Parse([]string{"kubelogin", "--help"}, "version")
if err == nil {
t.Errorf("Parse should return error but nil")
}
if c != nil {
t.Errorf("Parse should return nil but %+v", c)
}
}

57
cli/tls.go Normal file
View File

@@ -0,0 +1,57 @@
package cli
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"github.com/int128/kubelogin/kubeconfig"
)
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) *tls.Config {
p := x509.NewCertPool()
if ca := authProvider.IDPCertificateAuthority(); ca != "" {
if err := appendCertFile(p, ca); err != nil {
log.Printf("Skip CA certificate of idp-certificate-authority: %s", err)
} else {
log.Printf("Using CA certificate: %s", ca)
}
}
if ca := authProvider.IDPCertificateAuthorityData(); ca != "" {
if err := appendCertData(p, ca); err != nil {
log.Printf("Skip CA certificate of idp-certificate-authority-data: %s", err)
} else {
log.Printf("Using CA certificate of idp-certificate-authority-data")
}
}
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}
if len(p.Subjects()) > 0 {
cfg.RootCAs = p
}
return cfg
}
func appendCertFile(p *x509.CertPool, name string) error {
b, err := ioutil.ReadFile(name)
if err != nil {
return fmt.Errorf("Could not read %s: %s", name, err)
}
if p.AppendCertsFromPEM(b) != true {
return fmt.Errorf("Could not append certificate from %s", name)
}
return nil
}
func appendCertData(p *x509.CertPool, data string) error {
b, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return fmt.Errorf("Could not decode base64: %s", err)
}
if p.AppendCertsFromPEM(b) != true {
return fmt.Errorf("Could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,49 @@
package authserver
import (
"net/http"
"testing"
)
// Addr is address to listen.
const Addr = "localhost:9000"
// CACert is path to the CA certificate.
// This should be generated by Makefile before test.
const CACert = "authserver/testdata/ca.crt"
// ServerCert is path to the server certificate.
// This should be generated by Makefile before test.
const ServerCert = "authserver/testdata/server.crt"
// ServerKey is path to the server key.
// This should be generated by Makefile before test.
const ServerKey = "authserver/testdata/server.key"
// Config represents server configuration.
type Config struct {
Issuer string
Scope string
Cert string
Key string
}
// Start starts a HTTP server.
func (c *Config) Start(t *testing.T) *http.Server {
s := &http.Server{
Addr: Addr,
Handler: newHandler(t, c),
}
go func() {
var err error
if c.Cert != "" && c.Key != "" {
err = s.ListenAndServeTLS(c.Cert, c.Key)
} else {
err = s.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return s
}

View File

@@ -1,45 +1,47 @@
package integration
package authserver
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"html/template"
"log"
"math/big"
"net/http"
"testing"
"text/template"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
// AuthHandler provides the stub handler for OIDC authentication.
type AuthHandler struct {
// Values in templates
type handler struct {
discovery *template.Template
token *template.Template
jwks *template.Template
authCode string
Issuer string
AuthCode string
Scope string // Default to openid
IDToken string
PrivateKey struct{ N, E string }
// Response templates
discoveryJSON *template.Template
tokenJSON *template.Template
jwksJSON *template.Template
}
// NewAuthHandler returns a new AuthHandler.
func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
h := &AuthHandler{
Issuer: issuer,
AuthCode: "0b70006b-f62a-4438-aba5-c0b96775d8e5",
discoveryJSON: template.Must(template.ParseFiles("testdata/oidc-discovery.json")),
tokenJSON: template.Must(template.ParseFiles("testdata/oidc-token.json")),
jwksJSON: template.Must(template.ParseFiles("testdata/oidc-jwks.json")),
func newHandler(t *testing.T, c *Config) *handler {
h := handler{
discovery: readTemplate(t, "oidc-discovery.json"),
token: readTemplate(t, "oidc-token.json"),
jwks: readTemplate(t, "oidc-jwks.json"),
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
Issuer: c.Issuer,
Scope: c.Scope,
}
if h.Scope == "" {
h.Scope = "openid"
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
Issuer: h.Issuer,
Issuer: c.Issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
})
@@ -53,24 +55,43 @@ func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
}
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
return h
return &h
}
func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
func readTemplate(t *testing.T, name string) *template.Template {
t.Helper()
tpl, err := template.ParseFiles("authserver/testdata/" + name)
if err != nil {
t.Fatalf("Could not read template %s: %s", name, err)
}
return tpl
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
log.Printf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
log.Printf("[auth-server] %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := s.discoveryJSON.Execute(w, s); err != nil {
if err := h.discovery.Execute(w, h); err != nil {
return err
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), s.AuthCode)
if h.Scope != q.Get("scope") {
return fmt.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.authCode)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
@@ -78,16 +99,16 @@ func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
if s.AuthCode != r.Form.Get("code") {
return fmt.Errorf("code wants %s but %s", s.AuthCode, r.Form.Get("code"))
if h.authCode != r.Form.Get("code") {
return fmt.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
}
w.Header().Add("Content-Type", "application/json")
if err := s.tokenJSON.Execute(w, s); err != nil {
if err := h.token.Execute(w, h); err != nil {
return err
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := s.jwksJSON.Execute(w, s); err != nil {
if err := h.jwks.Execute(w, h); err != nil {
return err
}
default:
@@ -95,10 +116,3 @@ func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
}
return nil
}
func (s *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := s.serveHTTP(w, r); err != nil {
log.Printf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}

View File

@@ -1,40 +1,40 @@
.PHONY: clean
all: authserver.crt authserver-ca.crt
all: server.crt ca.crt
clean:
rm -v authserver*
rm -v ca.* server.*
authserver-ca.key:
ca.key:
openssl genrsa -out $@ 1024
authserver-ca.csr: openssl.cnf authserver-ca.key
ca.csr: openssl.cnf ca.key
openssl req -config openssl.cnf \
-new \
-key authserver-ca.key \
-key ca.key \
-subj "/CN=Hello CA" \
-out $@
openssl req -noout -text -in $@
authserver-ca.crt: authserver-ca.csr authserver-ca.key
ca.crt: ca.csr ca.key
openssl x509 -req \
-signkey authserver-ca.key \
-in authserver-ca.csr \
-signkey ca.key \
-in ca.csr \
-out $@
openssl x509 -text -in $@
authserver.key:
server.key:
openssl genrsa -out $@ 1024
authserver.csr: openssl.cnf authserver.key
server.csr: openssl.cnf server.key
openssl req -config openssl.cnf \
-new \
-key authserver.key \
-key server.key \
-subj "/CN=localhost" \
-out $@
openssl req -noout -text -in $@
authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
server.crt: openssl.cnf server.csr ca.key ca.crt
rm -fr ./CA
mkdir -p ./CA
touch CA/index.txt
@@ -43,8 +43,8 @@ authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
openssl ca -config openssl.cnf \
-extensions v3_req \
-batch \
-cert authserver-ca.crt \
-keyfile authserver-ca.key \
-in authserver.csr \
-cert ca.crt \
-keyfile ca.key \
-in server.csr \
-out $@
openssl x509 -text -in $@

167
cli_test/e2e_test.go Normal file
View File

@@ -0,0 +1,167 @@
package cli_test
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
"github.com/int128/kubelogin/cli"
"github.com/int128/kubelogin/cli_test/authserver"
"github.com/int128/kubelogin/cli_test/kubeconfig"
"golang.org/x/sync/errgroup"
)
// End-to-end test.
//
// 1. Start the auth server at port 9000.
// 2. Run the CLI.
// 3. Open a request for port 8000.
// 4. Wait for the CLI.
// 5. Shutdown the auth server.
func TestE2E(t *testing.T) {
data := map[string]struct {
kubeconfigValues kubeconfig.Values
cli cli.CLI
serverConfig authserver.Config
clientTLS *tls.Config
}{
"NoTLS": {
kubeconfig.Values{Issuer: "http://localhost:9000"},
cli.CLI{},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
"ExtraScope": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
ExtraScopes: "profile groups",
},
cli.CLI{},
authserver.Config{
Issuer: "http://localhost:9000",
Scope: "profile groups openid",
},
&tls.Config{},
},
"SkipTLSVerify": {
kubeconfig.Values{Issuer: "https://localhost:9000"},
cli.CLI{SkipTLSVerify: true},
authserver.Config{
Issuer: "https://localhost:9000",
Cert: authserver.ServerCert,
Key: authserver.ServerKey,
},
&tls.Config{InsecureSkipVerify: true},
},
"CACert": {
kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: authserver.CACert,
},
cli.CLI{},
authserver.Config{
Issuer: "https://localhost:9000",
Cert: authserver.ServerCert,
Key: authserver.ServerKey,
},
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
},
"CACertData": {
kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)),
},
cli.CLI{},
authserver.Config{
Issuer: "https://localhost:9000",
Cert: authserver.ServerCert,
Key: authserver.ServerKey,
},
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
},
"InvalidCACertShouldBeSkipped": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
IDPCertificateAuthority: "e2e_test.go",
},
cli.CLI{},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
"InvalidCACertDataShouldBeSkipped": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")),
},
cli.CLI{},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
}
for name, c := range data {
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := c.serverConfig.Start(t)
defer server.Shutdown(ctx)
kcfg := kubeconfig.Create(t, &c.kubeconfigValues)
defer os.Remove(kcfg)
c.cli.KubeConfig = kcfg
c.cli.SkipOpenBrowser = true
c.cli.ListenPort = 8000
var eg errgroup.Group
eg.Go(func() error {
return c.cli.Run(ctx)
})
if err := openBrowserRequest(c.clientTLS); err != nil {
cancel()
t.Error(err)
}
if err := eg.Wait(); err != nil {
t.Fatalf("CLI returned error: %s", err)
}
kubeconfig.Verify(t, kcfg)
})
}
}
func openBrowserRequest(tlsConfig *tls.Config) error {
time.Sleep(50 * time.Millisecond)
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
res, err := client.Get("http://localhost:8000/")
if err != nil {
return fmt.Errorf("Could not send a request: %s", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("StatusCode wants 200 but %d", res.StatusCode)
}
return nil
}
func read(t *testing.T, name string) []byte {
t.Helper()
b, err := ioutil.ReadFile(name)
if err != nil {
t.Fatalf("Could not read %s: %s", name, err)
}
return b
}
func readCert(t *testing.T, name string) *x509.CertPool {
t.Helper()
p := x509.NewCertPool()
b := read(t, name)
if !p.AppendCertsFromPEM(b) {
t.Fatalf("Could not append cert from %s", name)
}
return p
}

View File

@@ -1,4 +1,4 @@
package integration
package kubeconfig
import (
"html/template"
@@ -7,20 +7,23 @@ import (
"testing"
)
type kubeconfigValues struct {
// Values represents values in .kubeconfig template.
type Values struct {
Issuer string
ExtraScopes string
IDPCertificateAuthority string
IDPCertificateAuthorityData string
}
func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
// Create creates a kubeconfig file and returns path to it.
func Create(t *testing.T, v *Values) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tpl, err := template.ParseFiles("testdata/kubeconfig.yaml")
tpl, err := template.ParseFiles("kubeconfig/testdata/kubeconfig.yaml")
if err != nil {
t.Fatal(err)
}
@@ -30,7 +33,8 @@ func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
return f.Name()
}
func verifyKubeconfig(t *testing.T, kubeconfig string) {
// Verify returns true if the kubeconfig has valid values.
func Verify(t *testing.T, kubeconfig string) {
b, err := ioutil.ReadFile(kubeconfig)
if err != nil {
t.Fatal(err)

View File

@@ -19,6 +19,9 @@ users:
client-id: kubernetes
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
idp-issuer-url: {{ .Issuer }}
#{{ if .ExtraScopes }}
extra-scopes: {{ .ExtraScopes }}
#{{ end }}
#{{ if .IDPCertificateAuthority }}
idp-certificate-authority: {{ .IDPCertificateAuthority }}
#{{ end }}

100
docs/google.md Normal file
View File

@@ -0,0 +1,100 @@
# Getting Started with Google Identity Platform
## Prerequisite
- You have a Google account.
- You have the Cluster Admin role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
## 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).
### kops
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.apps.googleusercontent.com
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to you.
```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#1234567890
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
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
```

107
docs/keycloak.md Normal file
View File

@@ -0,0 +1,107 @@
# Getting Started with Keycloak
## Prerequisite
- You have administrator access to the Keycloak.
- You have the Cluster Admin role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Keycloak
Open the Keycloak and create an OIDC client as follows:
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
- Client ID: `kubernetes`
- Groups claim: `groups`
Create a group `kubernetes:admin` and join to it.
This is used for group based access control.
## 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).
### kops
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: kubernetes
oidcGroupsClaim: groups
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
```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: Group
name: /kubernetes:admin
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://keycloak.example.com/auth/realms/YOUR_REALM
client-id: kubernetes
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
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
```

40
docs/team_ops.md Normal file
View File

@@ -0,0 +1,40 @@
# Team Operation
## 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:
auth-provider:
name: oidc
config:
client-id: YOUR_CLIEND_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: YOUR_ISSUER
```
You can share the kubeconfig to your team members for easy onboarding.

30
go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/int128/kubelogin
require (
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/int128/oauth2cli v1.0.0
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.5 // indirect
github.com/mitchellh/go-homedir v1.0.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/pflag v1.0.3 // indirect
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 // indirect
golang.org/x/text v0.3.0 // indirect
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.1.9 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd // indirect
k8s.io/client-go v9.0.0+incompatible
)

58
go.sum Normal file
View File

@@ -0,0 +1,58 @@
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
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/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/int128/oauth2cli v1.0.0 h1:bQcKSgS7lBVIhEJ1IE689oGW6AmTPrh/mQkxqGxX2Z0=
github.com/int128/oauth2cli v1.0.0/go.mod h1:ClkmeKFkDlkVtqncv98+V4Gny/luvARCIoK/+3KlKb8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107 h1:63fpDttzclb8owmRoxSaFNbnT1CG25L0Yvnhh9lU1SE=
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 h1:biUuj9O+0+XckRUCDzjoOGm6yFV5c0IHbm1ODP3e4Zw=
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/square/go-jose.v2 v2.1.9 h1:YCFbL5T2gbmC2sMG12s1x2PAlTK5TZNte3hjZEIcCAg=
gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd h1:rzRDbWmTXaWr6SDV9caWzwAUPSfvgwX6j89LBnq/ycw=
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v9.0.0+incompatible h1:NXWpDuPFeVB5lYP1fTqJUtwigjtmRXJNtndnN53ldGI=
k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=

View File

@@ -1,160 +0,0 @@
package integration
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
"github.com/int128/kubelogin/cli"
)
const caCert = "testdata/authserver-ca.crt"
const tlsCert = "testdata/authserver.crt"
const tlsKey = "testdata/authserver.key"
func Test(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "http://localhost:9000"),
}
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "http://localhost:9000",
})
defer os.Remove(kubeconfig)
go listenAndServe(t, authServer)
go authenticate(t, &tls.Config{})
c := cli.CLI{
KubeConfig: kubeconfig,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func TestWithSkipTLSVerify(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
})
defer os.Remove(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{InsecureSkipVerify: true})
c := cli.CLI{
KubeConfig: kubeconfig,
SkipTLSVerify: true,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func TestWithCACert(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: caCert,
})
defer os.Remove(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
c := cli.CLI{
KubeConfig: kubeconfig,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func TestWithCACertData(t *testing.T) {
ctx := context.Background()
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(t, "https://localhost:9000"),
}
defer authServer.Shutdown(ctx)
b, err := ioutil.ReadFile(caCert)
if err != nil {
t.Fatal(err)
}
kubeconfig := createKubeconfig(t, &kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(b),
})
defer os.Remove(kubeconfig)
go listenAndServeTLS(t, authServer)
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
c := cli.CLI{
KubeConfig: kubeconfig,
}
if err := c.Run(ctx); err != nil {
t.Fatal(err)
}
verifyKubeconfig(t, kubeconfig)
}
func authenticate(t *testing.T, tlsConfig *tls.Config) {
t.Helper()
time.Sleep(100 * time.Millisecond)
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
res, err := client.Get("http://localhost:8000/")
if err != nil {
t.Error(err)
return
}
if res.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d: res=%+v", res.StatusCode, res)
}
}
func loadCACert(t *testing.T) *x509.CertPool {
p := x509.NewCertPool()
b, err := ioutil.ReadFile(caCert)
if err != nil {
t.Fatal(err)
}
if !p.AppendCertsFromPEM(b) {
t.Fatalf("Could not AppendCertsFromPEM")
}
return p
}
func listenAndServe(t *testing.T, s *http.Server) {
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Fatal(err)
}
}
func listenAndServeTLS(t *testing.T, s *http.Server) {
if err := s.ListenAndServeTLS(tlsCert, tlsKey); err != nil && err != http.ErrServerClosed {
t.Fatal(err)
}
}

View File

@@ -2,65 +2,74 @@ package kubeconfig
import (
"fmt"
"strings"
"k8s.io/client-go/tools/clientcmd/api"
)
// FindCurrentAuthInfo returns the authInfo of current context.
// If the current context does not exist, this returns nil.
func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
// FindOIDCAuthProvider returns the current OIDC authProvider.
// If the context, auth-info or auth-provider does not exist, this returns an error.
// If auth-provider is not "oidc", this returns an error.
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil
return nil, fmt.Errorf("context %s does not exist", config.CurrentContext)
}
authInfo := config.AuthInfos[context.AuthInfo]
if authInfo == nil {
return nil, fmt.Errorf("auth-info %s does not exist", context.AuthInfo)
}
return config.AuthInfos[context.AuthInfo]
}
// ToOIDCAuthProviderConfig converts from api.AuthInfo to OIDCAuthProviderConfig.
func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig, error) {
if authInfo.AuthProvider == nil {
return nil, fmt.Errorf("auth-provider is not set, did you setup kubectl as listed here: https://github.com/int128/kubelogin#3-setup-kubectl")
return nil, fmt.Errorf("auth-provider is not set")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
return nil, fmt.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
}
// OIDCAuthProviderConfig represents OIDC configuration in the kubeconfig.
type OIDCAuthProviderConfig api.AuthProviderConfig
// OIDCAuthProvider represents OIDC configuration in the kubeconfig.
type OIDCAuthProvider api.AuthProviderConfig
// IDPIssuerURL returns the idp-issuer-url.
func (c *OIDCAuthProviderConfig) IDPIssuerURL() string {
func (c *OIDCAuthProvider) IDPIssuerURL() string {
return c.Config["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c *OIDCAuthProviderConfig) ClientID() string {
func (c *OIDCAuthProvider) ClientID() string {
return c.Config["client-id"]
}
// ClientSecret returns the client-secret.
func (c *OIDCAuthProviderConfig) ClientSecret() string {
func (c *OIDCAuthProvider) ClientSecret() string {
return c.Config["client-secret"]
}
// IDPCertificateAuthority returns the idp-certificate-authority.
func (c *OIDCAuthProviderConfig) IDPCertificateAuthority() string {
func (c *OIDCAuthProvider) IDPCertificateAuthority() string {
return c.Config["idp-certificate-authority"]
}
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
func (c *OIDCAuthProviderConfig) IDPCertificateAuthorityData() string {
func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
return c.Config["idp-certificate-authority-data"]
}
// ExtraScopes returns the extra-scopes.
func (c *OIDCAuthProvider) ExtraScopes() []string {
if c.Config["extra-scopes"] == "" {
return []string{}
}
return strings.Split(c.Config["extra-scopes"], ",")
}
// SetIDToken replaces the id-token.
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
c.Config["id-token"] = idToken
}
// SetRefreshToken replaces the refresh-token.
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
func (c *OIDCAuthProvider) SetRefreshToken(refreshToken string) {
c.Config["refresh-token"] = refreshToken
}

View File

@@ -1,19 +1,13 @@
package kubeconfig
import (
"fmt"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
// Load loads the file and returns the Config.
func Load(path string) (*api.Config, error) {
config, err := clientcmd.LoadFromFile(path)
if err != nil {
return nil, fmt.Errorf("Could not load kubeconfig from %s: %s", path, err)
}
return config, nil
// Read parses the file and returns the Config.
func Read(path string) (*api.Config, error) {
return clientcmd.LoadFromFile(path)
}
// Write writes the config to the file.

View File

@@ -8,13 +8,16 @@ import (
"github.com/int128/kubelogin/cli"
)
// Set by goreleaser, see https://goreleaser.com/environment/
var version = "1.x"
func main() {
c, err := cli.Parse(os.Args)
c, err := cli.Parse(os.Args, version)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := c.Run(ctx); err != nil {
log.Fatal(err)
log.Fatalf("Error: %s", err)
}
}