Compare commits

..

13 Commits
1.3 ... 1.4.1

Author SHA1 Message Date
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
20 changed files with 415 additions and 343 deletions

View File

@@ -10,7 +10,7 @@ jobs:
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -v
- run: make -C integration-test/testdata
- run: make -C e2e/testdata
- run: go test -v ./...
release:

131
README.md
View File

@@ -1,28 +1,38 @@
# 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 helper 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.
This may work with various OIDC providers such as Keycloak, Google Identity Platform and Azure AD.
## 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 OIDC provider and [Kubernetes OIDC authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
After setup or when the token has been expired, just run `kubelogin`:
```
% kubelogin --help
2018/08/15 19:08:58 Usage:
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--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]
Help Options:
-h, --help Show this help message
% 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
```
It opens the browser and you can log in to the provider.
After you logged in to the provider, it closes the browser automatically.
Then it writes the ID token and refresh token to the kubeconfig.
```
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
```
Please see the later section for details.
## Getting Started with Google Account
@@ -175,7 +185,34 @@ See the previous section for details.
## Configuration
### Kubeconfig
```
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--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 supports the following `auth-provider` keys in kubeconfig.
See also [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
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.
`id-token` | OUT | ID token got from the provider.
`refresh-token` | OUT | Refresh token got from the provider.
### Kubeconfig path
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
@@ -184,37 +221,41 @@ Default to `~/.kube/config`.
export KUBECONFIG="$PWD/.kubeconfig"
```
### OpenID Connect Provider CA Certificate
### Team onboarding
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).
You can share the kubeconfig to your team members for easy setup.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
```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
```
### Setup script
In actual team operation, you can share the following script to your team members for easy setup.
If you are using kops, export the kubeconfig and edit it.
```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"
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
vim .kubeconfig
```
@@ -223,12 +264,18 @@ kubectl config use-context "$CLUSTER_NAME"
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests.
### Build
### Build and Test
```sh
go get github.com/int128/kubelogin
```
```sh
cd $GOPATH/src/github.com/int128/kubelogin
make -C e2e/testdata
go test -v ./...
```
### Release
CircleCI publishes the build to GitHub. See [.circleci/config.yml](.circleci/config.yml).

View File

@@ -2,65 +2,56 @@ package auth
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"net/http"
"time"
"github.com/pkg/browser"
"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
type authCodeFlow struct {
Config *oauth2.Config
ServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
// 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()
func (f *authCodeFlow) getToken(ctx context.Context) (*oauth2.Token, error) {
code, err := f.getAuthCode(ctx)
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
return nil, fmt.Errorf("Could not get an auth code: %s", err)
}
token, err := f.Config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
return nil, fmt.Errorf("Could not exchange token: %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
func (f *authCodeFlow) getAuthCode(ctx context.Context) (string, error) {
state, err := generateState()
if err != nil {
return "", fmt.Errorf("Could not generate state parameter: %s", 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)
defer close(codeCh)
errCh := make(chan error)
defer close(errCh)
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:
Addr: fmt.Sprintf("localhost:%d", f.ServerPort),
Handler: &authCodeHandler{
authCodeURL: f.Config.AuthCodeURL(state),
gotCode: func(code string, gotState string) {
if gotState == state {
codeCh <- code
} else {
errCh <- fmt.Errorf("State does not match, wants %s but %s", state, gotState)
}
},
gotError: func(err error) {
errCh <- err
},
},
}
go func() {
@@ -68,6 +59,13 @@ func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config
errCh <- err
}
}()
go func() {
log.Printf("Open http://localhost:%d for authorization", f.ServerPort)
if !f.SkipOpenBrowser {
time.Sleep(500 * time.Millisecond)
browser.OpenURL(fmt.Sprintf("http://localhost:%d/", f.ServerPort))
}
}()
select {
case err := <-errCh:
server.Shutdown(ctx)
@@ -75,32 +73,36 @@ func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config
case code := <-codeCh:
server.Shutdown(ctx)
return code, nil
case <-ctx.Done():
server.Shutdown(ctx)
return "", ctx.Err()
}
}
type handler struct {
AuthCodeURL string
Callback func(code string, state string, err error)
type authCodeHandler struct {
authCodeURL string
gotCode func(code string, state string)
gotError func(err error)
}
func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *authCodeHandler) 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)
}
m := r.Method
p := r.URL.Path
q := r.URL.Query()
switch {
case m == "GET" && p == "/" && q.Get("error") != "":
h.gotError(fmt.Errorf("OAuth Error: %s %s", q.Get("error"), q.Get("error_description")))
http.Error(w, "OAuth Error", 500)
case m == "GET" && p == "/" && q.Get("code") != "":
h.gotCode(q.Get("code"), q.Get("state"))
w.Header().Add("Content-Type", "text/html")
fmt.Fprintf(w, `<html><body>OK<script>window.close()</script></body></html>`)
case m == "GET" && p == "/":
http.Redirect(w, r, h.authCodeURL, 302)
default:
http.Error(w, "Not Found", 404)
}

View File

@@ -3,6 +3,8 @@ package auth
import (
"context"
"fmt"
"log"
"net/http"
oidc "github.com/coreos/go-oidc"
"golang.org/x/oauth2"
@@ -12,49 +14,56 @@ 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
ServerPort 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)
// 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)
}
provider, err := oidc.NewProvider(ctx, c.Issuer)
if err != nil {
return nil, fmt.Errorf("Could not access OIDC issuer: %s", err)
return nil, fmt.Errorf("Could not discovery the OIDC issuer: %s", err)
}
flow := BrowserAuthCodeFlow{
Port: 8000,
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{oidc.ScopeOpenID, "email"},
},
oauth2Config := &oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
RedirectURL: fmt.Sprintf("http://localhost:%d/", c.ServerPort),
}
token, err := flow.GetToken(ctx)
flow := &authCodeFlow{
ServerPort: c.ServerPort,
SkipOpenBrowser: c.SkipOpenBrowser,
Config: oauth2Config,
}
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
}

15
auth/state.go Normal file
View File

@@ -0,0 +1,15 @@
package auth
import (
"crypto/rand"
"encoding/binary"
"fmt"
)
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
}

View File

@@ -2,11 +2,7 @@ package cli
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"
@@ -14,7 +10,6 @@ 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.
@@ -33,9 +28,9 @@ 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"`
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.
@@ -54,7 +49,7 @@ func (c *CLI) Run(ctx context.Context) error {
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)
}
@@ -63,7 +58,7 @@ func (c *CLI) Run(ctx context.Context) error {
if authInfo == nil {
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
authProvider, err := kubeconfig.FindOIDCAuthProvider(authInfo)
if err != nil {
return fmt.Errorf("Could not find auth-provider: %s", err)
}
@@ -71,9 +66,15 @@ func (c *CLI) Run(ctx context.Context) error {
if err != nil {
return fmt.Errorf("Could not configure TLS: %s", err)
}
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())
authConfig := &auth.Config{
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}},
ServerPort: 8000,
SkipOpenBrowser: c.SkipOpenBrowser,
}
token, err := authConfig.GetTokenSet(ctx)
if err != nil {
return fmt.Errorf("Authentication error: %s", err)
}
@@ -84,32 +85,3 @@ func (c *CLI) Run(ctx context.Context) error {
log.Printf("Updated %s", path)
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
}

42
cli/tls.go Normal file
View File

@@ -0,0 +1,42 @@
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, 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
}

View File

@@ -1,4 +1,4 @@
package integration
package e2e
import (
"crypto/rand"

145
e2e/e2e_test.go Normal file
View File

@@ -0,0 +1,145 @@
package e2e
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
"github.com/int128/kubelogin/cli"
"golang.org/x/sync/errgroup"
)
const tlsCACert = "testdata/authserver-ca.crt"
const tlsServerCert = "testdata/authserver.crt"
const tlsServerKey = "testdata/authserver.key"
// 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 kubeconfigValues
cli cli.CLI
startServer func(*testing.T, http.Handler) *http.Server
authClientTLS *tls.Config
}{
"NoTLS": {
kubeconfigValues{Issuer: "http://localhost:9000"},
cli.CLI{},
startServer,
&tls.Config{},
},
"SkipTLSVerify": {
kubeconfigValues{Issuer: "https://localhost:9000"},
cli.CLI{SkipTLSVerify: true},
startServerTLS,
&tls.Config{InsecureSkipVerify: true},
},
"CACert": {
kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: tlsCACert,
},
cli.CLI{},
startServerTLS,
&tls.Config{RootCAs: readCert(t, tlsCACert)},
},
"CACertData": {
kubeconfigValues{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, tlsCACert)),
},
cli.CLI{},
startServerTLS,
&tls.Config{RootCAs: readCert(t, tlsCACert)},
},
}
for name, c := range data {
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
authServer := c.startServer(t, NewAuthHandler(t, c.kubeconfigValues.Issuer))
defer authServer.Shutdown(ctx)
kubeconfig := createKubeconfig(t, &c.kubeconfigValues)
defer os.Remove(kubeconfig)
c.cli.KubeConfig = kubeconfig
c.cli.SkipOpenBrowser = true
var eg errgroup.Group
eg.Go(func() error {
return c.cli.Run(ctx)
})
time.Sleep(50 * time.Millisecond)
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.authClientTLS}}
res, err := client.Get("http://localhost:8000/")
if err != nil {
t.Fatalf("Could not send a request: %s", err)
}
if res.StatusCode != 200 {
t.Fatalf("StatusCode wants 200 but %d", res.StatusCode)
}
if err := eg.Wait(); err != nil {
t.Fatalf("CLI returned error: %s", err)
}
verifyKubeconfig(t, kubeconfig)
})
}
}
func startServer(t *testing.T, h http.Handler) *http.Server {
s := &http.Server{
Addr: "localhost:9000",
Handler: h,
}
go func() {
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return s
}
func startServerTLS(t *testing.T, h http.Handler) *http.Server {
s := &http.Server{
Addr: "localhost:9000",
Handler: h,
}
go func() {
if err := s.ListenAndServeTLS(tlsServerCert, tlsServerKey); err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return s
}
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 e2e
import (
"html/template"

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

@@ -16,51 +16,51 @@ func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
return config.AuthInfos[context.AuthInfo]
}
// ToOIDCAuthProviderConfig converts from api.AuthInfo to OIDCAuthProviderConfig.
func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig, error) {
// FindOIDCAuthProvider returns the OIDC authProvider.
func FindOIDCAuthProvider(authInfo *api.AuthInfo) (*OIDCAuthProvider, 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, did you setup kubectl as listed here: https://github.com/int128/kubelogin")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider `%s` is not supported", 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"]
}
// 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

@@ -7,8 +7,8 @@ import (
"k8s.io/client-go/tools/clientcmd/api"
)
// Load loads the file and returns the Config.
func Load(path string) (*api.Config, error) {
// Read parses the file and returns the Config.
func Read(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)