Compare commits

..

2 Commits

Author SHA1 Message Date
Hidetake Iwata
fc31d467e4 Dump all claims of ID token to debug log (#68)
* Dump all claims of ID token to debug log

* Add dump when a user already has a token
2019-04-19 10:47:32 +09:00
Hidetake Iwata
6ae8e36683 Fix error of go get and remove golint (#69)
* Remove golint

* Fix error of go get github.com/int128/ghcp
2019-04-19 10:46:07 +09:00
27 changed files with 815 additions and 1101 deletions

View File

@@ -14,9 +14,9 @@ jobs:
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.3.0/ghcp_linux_amd64
chmod +x ~/bin/ghcp
- run: |
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.16.0
- run: go get github.com/int128/goxzst
- run: go get github.com/tcnksm/ghr
go get -v \
github.com/int128/goxzst \
github.com/tcnksm/ghr
- checkout
# workaround for https://github.com/golang/go/issues/27925
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum

2
.gitignore vendored
View File

@@ -2,4 +2,4 @@
/dist
/kubelogin
/kubectl-oidc_login
/.kubeconfig*
/.kubeconfig

View File

@@ -8,7 +8,7 @@ LDFLAGS := -X main.version=$(CIRCLE_TAG)
all: $(TARGET)
check:
golangci-lint run
go vet
$(MAKE) -C adaptors_test/keys/testdata
go test -v -race ./...

View File

@@ -1,7 +1,7 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin)
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
It updates the kubeconfig file with an ID token and refresh token got from the OIDC provider.
It gets a token from the OIDC provider and writes it to the kubeconfig.
## Getting Started
@@ -24,7 +24,7 @@ brew install kubelogin
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.11.0/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.1/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
@@ -33,9 +33,10 @@ After initial setup or when the token has been expired, just run:
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
Using current-context: hello.k8s.local
Open http://localhost:8000 for authorization
Got a token for subject 0123456789 (valid until 2019-04-12 11:00:49 +0900 JST)
Updated ~/.kube/config
```
or run as a kubectl plugin:
@@ -53,30 +54,28 @@ For more, see the following documents:
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
If you are using other platforms, please contribute documents via pull requests.
## Configuration
This document is for the development version.
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
Kubelogin supports the following options.
This supports the following options.
```
Options:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-v, --v int If set to 1 or greater, it shows debug log
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]
-v, --v= If set to 1 or greater, show debug log (default: 0)
Help Options:
-h, --help Show this help message
```
It supports the following keys of `auth-provider` in a kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
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).
Key | Direction | Value
----|-----------|------
@@ -90,21 +89,15 @@ Key | Direction | Value
`refresh-token` | Write | Refresh token got from the provider.
### Kubeconfig
### Kubeconfig path
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
It defaults to `~/.kube/config`.
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
```sh
# by the option
kubelogin --kubeconfig /path/to/kubeconfig
# by the environment variable
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
export KUBECONFIG="$PWD/.kubeconfig"
```
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
### Extra scopes
@@ -122,30 +115,17 @@ sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
### Redirect URIs
By default kubelogin starts the local server at port 8000 or 18000.
You need to register the following redirect URIs to the OIDC provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the ports by the option:
```sh
kubelogin --listen-port 12345 --listen-port 23456
```
### CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by kubeconfig or option.
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.
### HTTP Proxy

View File

@@ -2,29 +2,16 @@ package adaptors
import (
"context"
"strings"
"fmt"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/spf13/pflag"
"github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"go.uber.org/dig"
)
const usage = `Login to the OpenID Connect provider and update the kubeconfig.
kubelogin %[2]s
Examples:
# Login to the current provider and update ~/.kube/config
%[1]s
Options:
%[3]s
Usage:
%[1]s [options]`
var defaultListenPort = []int{8000, 18000}
func NewCmd(i Cmd) adaptors.Cmd {
return &i
}
@@ -36,43 +23,32 @@ type Cmd struct {
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
executable := executableName(args[0])
f := pflag.NewFlagSet(executable, pflag.ContinueOnError)
f.SortFlags = false
f.Usage = func() {
cmd.Logger.Printf(usage, executable, version, f.FlagUsages())
}
var o cmdOptions
f.StringVar(&o.KubeConfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.KubeContext, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.KubeUser, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
if err := f.Parse(args[1:]); err != nil {
if err == pflag.ErrHelp {
return 1
}
cmd.Logger.Printf("Error: invalid arguments: %s", err)
parser := flags.NewParser(&o, flags.HelpFlag)
parser.LongDescription = fmt.Sprintf(`Version %s
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
version)
args, err := parser.ParseArgs(args[1:])
if err != nil {
cmd.Logger.Printf("Error: %s", err)
return 1
}
if len(f.Args()) > 0 {
if len(args) > 0 {
cmd.Logger.Printf("Error: too many arguments")
return 1
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
kubeConfig, err := o.ExpandKubeConfig()
if err != nil {
cmd.Logger.Printf("Error: invalid option: %s", err)
return 1
}
in := usecases.LoginIn{
KubeConfigFilename: o.KubeConfig,
KubeContextName: kubeconfig.ContextName(o.KubeContext),
KubeUserName: kubeconfig.UserName(o.KubeUser),
CertificateAuthorityFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
KubeConfig: kubeConfig,
ListenPort: o.ListenPort,
SkipTLSVerify: o.SkipTLSVerify,
SkipOpenBrowser: o.SkipOpenBrowser,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("Error: %s", err)
@@ -82,19 +58,18 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
}
type cmdOptions struct {
KubeConfig string
KubeContext string
KubeUser string
CertificateAuthority string
SkipTLSVerify bool
ListenPort []int
SkipOpenBrowser bool
Verbose int
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."`
Verbose int `long:"v" short:"v" default:"0" description:"If set to 1 or greater, show debug log"`
}
func executableName(arg0 string) string {
if strings.HasPrefix(arg0, "kubectl-") {
return strings.ReplaceAll(strings.ReplaceAll(arg0, "-", " "), "_", "-")
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *cmdOptions) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig)
}
return arg0
return d, nil
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/mitchellh/go-homedir"
)
func TestCmd_Run(t *testing.T) {
@@ -23,7 +24,8 @@ func TestCmd_Run(t *testing.T) {
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
ListenPort: defaultListenPort,
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 8000,
})
logger := mock_adaptors.NewLogger(t, ctrl)
@@ -48,13 +50,10 @@ func TestCmd_Run(t *testing.T) {
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeConfigFilename: "/path/to/kubeconfig",
KubeContextName: "hello.k8s.local",
KubeUserName: "google",
CertificateAuthorityFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 10080,
SkipTLSVerify: true,
SkipOpenBrowser: true,
})
logger := mock_adaptors.NewLogger(t, ctrl)
@@ -66,14 +65,9 @@ func TestCmd_Run(t *testing.T) {
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"--skip-open-browser",
"-v1",
}, version)
if exitCode != 0 {
@@ -95,17 +89,10 @@ func TestCmd_Run(t *testing.T) {
})
}
func TestCmd_executableName(t *testing.T) {
t.Run("kubelogin", func(t *testing.T) {
e := executableName("kubelogin")
if e != "kubelogin" {
t.Errorf("executableName wants kubelogin but %s", e)
}
})
t.Run("kubectl-oidc_login", func(t *testing.T) {
e := executableName("kubectl-oidc_login")
if e != "kubectl oidc-login" {
t.Errorf("executableName wants kubectl oidc-login but %s", e)
}
})
func expand(t *testing.T, path string) string {
d, err := homedir.Expand(path)
if err != nil {
t.Fatalf("could not expand: %s", err)
}
return d
}

View File

@@ -22,39 +22,17 @@ type HTTP struct {
Logger adaptors.Logger
}
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
pool := x509.NewCertPool()
if filename := config.OIDCConfig.IDPCertificateAuthority(); filename != "" {
h.Logger.Debugf(1, "Loading the certificate %s", filename)
err := appendCertificateFromFile(pool, filename)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority")
}
}
if data := config.OIDCConfig.IDPCertificateAuthorityData(); data != "" {
h.Logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, data)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority-data")
}
}
if config.CertificateAuthorityFilename != "" {
h.Logger.Debugf(1, "Loading the certificate %s", config.CertificateAuthorityFilename)
err := appendCertificateFromFile(pool, config.CertificateAuthorityFilename)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate")
}
func (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
return &httpClientConfig{
certPool: x509.NewCertPool(),
}
}
var tlsConfig tls.Config
if len(pool.Subjects()) > 0 {
tlsConfig.RootCAs = pool
}
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
return &http.Client{
Transport: &infrastructure.LoggingTransport{
Base: &http.Transport{
TLSClientConfig: &tlsConfig,
TLSClientConfig: config.TLSConfig(),
Proxy: http.ProxyFromEnvironment,
},
Logger: h.Logger,
@@ -62,24 +40,43 @@ func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error)
}, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
type httpClientConfig struct {
certPool *x509.CertPool
skipTLSVerify bool
}
func (c *httpClientConfig) AddCertificateFromFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "could not read %s", filename)
}
if !pool.AppendCertsFromPEM(b) {
if c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
func (c *httpClientConfig) AddEncodedCertificate(base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return errors.Wrapf(err, "could not decode base64")
}
if !pool.AppendCertsFromPEM(b) {
if c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate")
}
return nil
}
func (c *httpClientConfig) TLSConfig() *tls.Config {
tlsConfig := &tls.Config{
InsecureSkipVerify: c.skipTLSVerify,
}
if len(c.certPool.Subjects()) > 0 {
tlsConfig.RootCAs = c.certPool
}
return tlsConfig
}
func (c *httpClientConfig) SetSkipTLSVerify(b bool) {
c.skipTLSVerify = b
}

View File

@@ -2,43 +2,49 @@ package adaptors
import (
"context"
"crypto/tls"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,OIDC,Logger
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type KubeConfig interface {
LoadByDefaultRules(filename string) (*kubeconfig.Config, error)
LoadFromFile(filename string) (*kubeconfig.Config, error)
WriteToFile(config *kubeconfig.Config, filename string) error
LoadFromFile(filename string) (*api.Config, error)
WriteToFile(config *api.Config, filename string) error
}
type HTTP interface {
NewClientConfig() HTTPClientConfig
NewClient(config HTTPClientConfig) (*http.Client, error)
}
type HTTPClientConfig struct {
OIDCConfig kubeconfig.OIDCConfig
CertificateAuthorityFilename string
SkipTLSVerify bool
type HTTPClientConfig interface {
AddCertificateFromFile(filename string) error
AddEncodedCertificate(base64String string) error
SetSkipTLSVerify(b bool)
TLSConfig() *tls.Config
}
type OIDC interface {
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*oidc.IDToken, error)
VerifyIDToken(ctx context.Context, in OIDCVerifyTokenIn) (*oidc.IDToken, error)
}
type OIDCAuthenticateIn struct {
Config kubeconfig.OIDCConfig
Issuer string
ClientID string
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort []int // HTTP server port candidates
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
@@ -52,9 +58,11 @@ type OIDCAuthenticateOut struct {
RefreshToken string
}
type OIDCVerifyIn struct {
Config kubeconfig.OIDCConfig
Client *http.Client
type OIDCVerifyTokenIn struct {
IDToken string
Issuer string
ClientID string
Client *http.Client
}
type Logger interface {

View File

@@ -2,7 +2,6 @@ package adaptors
import (
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/kubeconfig"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
@@ -14,29 +13,16 @@ func NewKubeConfig() adaptors.KubeConfig {
type KubeConfig struct{}
// LoadByDefaultRules loads the config by the default rules, that is same as kubectl.
func (*KubeConfig) LoadByDefaultRules(filename string) (*kubeconfig.Config, error) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = filename
config, err := rules.Load()
if err != nil {
return nil, errors.Wrapf(err, "could not read the kubeconfig")
}
return (*kubeconfig.Config)(config), err
}
// LoadFromFile loads the config from the single file.
func (*KubeConfig) LoadFromFile(filename string) (*kubeconfig.Config, error) {
func (*KubeConfig) LoadFromFile(filename string) (*api.Config, error) {
config, err := clientcmd.LoadFromFile(filename)
if err != nil {
return nil, errors.Wrapf(err, "could not read the kubeconfig from %s", filename)
}
return (*kubeconfig.Config)(config), err
return config, err
}
// WriteToFile writes the config to the single file.
func (*KubeConfig) WriteToFile(config *kubeconfig.Config, filename string) error {
err := clientcmd.WriteToFile(*(*api.Config)(config), filename)
func (*KubeConfig) WriteToFile(config *api.Config, filename string) error {
err := clientcmd.WriteToFile(*config, filename)
if err != nil {
return errors.Wrapf(err, "could not write the kubeconfig to %s", filename)
}

View File

@@ -1,74 +0,0 @@
package adaptors
import (
"os"
"testing"
)
func TestKubeConfig_LoadByDefaultRules(t *testing.T) {
var adaptor KubeConfig
t.Run("google.yaml>keycloak.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := adaptor.LoadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "google@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
t.Run("keycloak.yaml>google.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := adaptor.LoadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "keycloak@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

View File

@@ -1,15 +1,16 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,OIDC,Logger)
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
import (
context "context"
tls "crypto/tls"
go_oidc "github.com/coreos/go-oidc"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
kubeconfig "github.com/int128/kubelogin/kubeconfig"
api "k8s.io/client-go/tools/clientcmd/api"
http "net/http"
reflect "reflect"
)
@@ -37,23 +38,10 @@ func (m *MockKubeConfig) EXPECT() *MockKubeConfigMockRecorder {
return m.recorder
}
// LoadByDefaultRules mocks base method
func (m *MockKubeConfig) LoadByDefaultRules(arg0 string) (*kubeconfig.Config, error) {
ret := m.ctrl.Call(m, "LoadByDefaultRules", arg0)
ret0, _ := ret[0].(*kubeconfig.Config)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadByDefaultRules indicates an expected call of LoadByDefaultRules
func (mr *MockKubeConfigMockRecorder) LoadByDefaultRules(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadByDefaultRules", reflect.TypeOf((*MockKubeConfig)(nil).LoadByDefaultRules), arg0)
}
// LoadFromFile mocks base method
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*kubeconfig.Config, error) {
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*api.Config, error) {
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
ret0, _ := ret[0].(*kubeconfig.Config)
ret0, _ := ret[0].(*api.Config)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -64,7 +52,7 @@ func (mr *MockKubeConfigMockRecorder) LoadFromFile(arg0 interface{}) *gomock.Cal
}
// WriteToFile mocks base method
func (m *MockKubeConfig) WriteToFile(arg0 *kubeconfig.Config, arg1 string) error {
func (m *MockKubeConfig) WriteToFile(arg0 *api.Config, arg1 string) error {
ret := m.ctrl.Call(m, "WriteToFile", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
@@ -111,6 +99,87 @@ func (mr *MockHTTPMockRecorder) NewClient(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockHTTP)(nil).NewClient), arg0)
}
// NewClientConfig mocks base method
func (m *MockHTTP) NewClientConfig() interfaces.HTTPClientConfig {
ret := m.ctrl.Call(m, "NewClientConfig")
ret0, _ := ret[0].(interfaces.HTTPClientConfig)
return ret0
}
// NewClientConfig indicates an expected call of NewClientConfig
func (mr *MockHTTPMockRecorder) NewClientConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientConfig", reflect.TypeOf((*MockHTTP)(nil).NewClientConfig))
}
// MockHTTPClientConfig is a mock of HTTPClientConfig interface
type MockHTTPClientConfig struct {
ctrl *gomock.Controller
recorder *MockHTTPClientConfigMockRecorder
}
// MockHTTPClientConfigMockRecorder is the mock recorder for MockHTTPClientConfig
type MockHTTPClientConfigMockRecorder struct {
mock *MockHTTPClientConfig
}
// NewMockHTTPClientConfig creates a new mock instance
func NewMockHTTPClientConfig(ctrl *gomock.Controller) *MockHTTPClientConfig {
mock := &MockHTTPClientConfig{ctrl: ctrl}
mock.recorder = &MockHTTPClientConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTPClientConfig) EXPECT() *MockHTTPClientConfigMockRecorder {
return m.recorder
}
// AddCertificateFromFile mocks base method
func (m *MockHTTPClientConfig) AddCertificateFromFile(arg0 string) error {
ret := m.ctrl.Call(m, "AddCertificateFromFile", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddCertificateFromFile indicates an expected call of AddCertificateFromFile
func (mr *MockHTTPClientConfigMockRecorder) AddCertificateFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertificateFromFile", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddCertificateFromFile), arg0)
}
// AddEncodedCertificate mocks base method
func (m *MockHTTPClientConfig) AddEncodedCertificate(arg0 string) error {
ret := m.ctrl.Call(m, "AddEncodedCertificate", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddEncodedCertificate indicates an expected call of AddEncodedCertificate
func (mr *MockHTTPClientConfigMockRecorder) AddEncodedCertificate(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEncodedCertificate", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddEncodedCertificate), arg0)
}
// SetSkipTLSVerify mocks base method
func (m *MockHTTPClientConfig) SetSkipTLSVerify(arg0 bool) {
m.ctrl.Call(m, "SetSkipTLSVerify", arg0)
}
// SetSkipTLSVerify indicates an expected call of SetSkipTLSVerify
func (mr *MockHTTPClientConfigMockRecorder) SetSkipTLSVerify(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSkipTLSVerify", reflect.TypeOf((*MockHTTPClientConfig)(nil).SetSkipTLSVerify), arg0)
}
// TLSConfig mocks base method
func (m *MockHTTPClientConfig) TLSConfig() *tls.Config {
ret := m.ctrl.Call(m, "TLSConfig")
ret0, _ := ret[0].(*tls.Config)
return ret0
}
// TLSConfig indicates an expected call of TLSConfig
func (mr *MockHTTPClientConfigMockRecorder) TLSConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TLSConfig", reflect.TypeOf((*MockHTTPClientConfig)(nil).TLSConfig))
}
// MockOIDC is a mock of OIDC interface
type MockOIDC struct {
ctrl *gomock.Controller
@@ -147,17 +216,17 @@ func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1, arg2 interface{}) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1, arg2)
}
// Verify mocks base method
func (m *MockOIDC) Verify(arg0 context.Context, arg1 interfaces.OIDCVerifyIn) (*go_oidc.IDToken, error) {
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
// VerifyIDToken mocks base method
func (m *MockOIDC) VerifyIDToken(arg0 context.Context, arg1 interfaces.OIDCVerifyTokenIn) (*go_oidc.IDToken, error) {
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
ret0, _ := ret[0].(*go_oidc.IDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify
func (mr *MockOIDCMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDC)(nil).Verify), arg0, arg1)
// VerifyIDToken indicates an expected call of VerifyIDToken
func (mr *MockOIDCMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockOIDC)(nil).VerifyIDToken), arg0, arg1)
}
// MockLogger is a mock of Logger interface

View File

@@ -20,23 +20,23 @@ func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, c
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL())
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2cli.Config{
OAuth2Config: oauth2.Config{
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID(),
ClientSecret: in.Config.ClientSecret(),
Scopes: append(in.Config.ExtraScopes(), oidc.ScopeOpenID),
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
Scopes: append(in.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
ShowLocalServerURL: cb.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
token, err := flow.GetToken(ctx)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
@@ -44,7 +44,7 @@ func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, c
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID()})
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
@@ -56,16 +56,16 @@ func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, c
}, nil
}
func (*OIDC) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*oidc.IDToken, error) {
func (*OIDC) VerifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) (*oidc.IDToken, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL())
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID()})
verifiedIDToken, err := verifier.Verify(ctx, in.Config.IDToken())
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}

View File

@@ -1,17 +0,0 @@
apiVersion: v1
clusters: []
contexts:
- context:
cluster: hello.k8s.local
user: google
name: google@hello.k8s.local
current-context: google@hello.k8s.local
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
client-id: CLIENT_ID.apps.googleusercontent.com
name: oidc

View File

@@ -1,16 +0,0 @@
apiVersion: v1
contexts:
- context:
cluster: hello.k8s.local
user: keycloak
name: keycloak@hello.k8s.local
current-context: keycloak@hello.k8s.local
kind: Config
preferences: {}
users:
- name: keycloak
user:
auth-provider:
config:
client-id: kubernetes
name: oidc

View File

@@ -8,7 +8,6 @@ import (
// Config represents server configuration.
type Config struct {
Addr string
Issuer string
Scope string
TLSServerCert string
@@ -21,7 +20,7 @@ type Config struct {
// Start starts a HTTP server.
func Start(t *testing.T, c Config) *http.Server {
s := &http.Server{
Addr: c.Addr,
Addr: "localhost:9000",
Handler: newHandler(t, c),
}
go func() {

View File

@@ -5,7 +5,6 @@ import (
"crypto/tls"
"net/http"
"os"
"sync"
"testing"
"time"
@@ -19,11 +18,10 @@ import (
)
// Run the integration tests.
// This assumes that port 800x and 900x are available.
//
// 1. Start the auth server at port 900x.
// 1. Start the auth server at port 9000.
// 2. Run the Cmd.
// 3. Open a request for port 800x.
// 3. Open a request for port 8000.
// 4. Wait for the Cmd.
// 5. Shutdown the auth server.
//
@@ -31,64 +29,26 @@ func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("NoTLS", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9001")
idToken := newIDToken(t, "http://localhost:9000")
serverConfig := authserver.Config{
Addr: "localhost:9001",
Issuer: "http://localhost:9001",
Issuer: "http://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: "http://localhost:9000",
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8001", nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8001")
wg.Wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9002")
serverConfig := authserver.Config{
Addr: "localhost:9002",
Issuer: "http://localhost:9002",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8002", nil)
runCmd(t, ctx, "--skip-open-browser", "--listen-port", "8002")
wg.Wait()
startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
@@ -96,32 +56,28 @@ func TestCmd_Run(t *testing.T) {
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9003")
idToken := newIDToken(t, "http://localhost:9000")
serverConfig := authserver.Config{
Addr: "localhost:9003",
Issuer: "http://localhost:9003",
Issuer: "http://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: "http://localhost:9000",
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8003", nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8003")
wg.Wait()
startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
@@ -129,14 +85,12 @@ func TestCmd_Run(t *testing.T) {
})
t.Run("CACert", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9004")
idToken := newIDToken(t, "https://localhost:9000")
serverConfig := authserver.Config{
Addr: "localhost:9004",
Issuer: "https://localhost:9004",
Issuer: "https://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
@@ -144,18 +98,16 @@ func TestCmd_Run(t *testing.T) {
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: "https://localhost:9000",
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8004", keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8004")
wg.Wait()
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
@@ -163,14 +115,12 @@ func TestCmd_Run(t *testing.T) {
})
t.Run("CACertData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9005")
idToken := newIDToken(t, "https://localhost:9000")
serverConfig := authserver.Config{
Addr: "localhost:9005",
Issuer: "https://localhost:9005",
Issuer: "https://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
@@ -178,18 +128,16 @@ func TestCmd_Run(t *testing.T) {
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8005", keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8005")
wg.Wait()
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
@@ -197,21 +145,19 @@ func TestCmd_Run(t *testing.T) {
})
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
serverConfig := authserver.Config{
Addr: "localhost:9006",
Issuer: "http://localhost:9006",
Issuer: "http://localhost:9000",
IDTokenKeyPair: keys.JWSKeyPair,
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
defer server.Shutdown(ctx)
idToken := newIDToken(t, serverConfig.Issuer)
idToken := newIDToken(t, "http://localhost:9000")
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: "http://localhost:9000",
IDToken: idToken,
})
defer os.Remove(kubeConfigFilename)
@@ -260,17 +206,16 @@ func runCmd(t *testing.T, ctx context.Context, args ...string) {
}
}
func startBrowserRequest(t *testing.T, ctx context.Context, wg *sync.WaitGroup, url string, tlsConfig *tls.Config) {
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) {
t.Helper()
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequest("GET", "http://localhost:8000/", nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
go func() {
defer wg.Done()
time.Sleep(50 * time.Millisecond)
resp, err := client.Do(req)
if err != nil {
@@ -282,25 +227,4 @@ func startBrowserRequest(t *testing.T, ctx context.Context, wg *sync.WaitGroup,
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}()
wg.Add(1)
}
func shutdown(t *testing.T, ctx context.Context, s *http.Server) {
if err := s.Shutdown(ctx); err != nil {
t.Errorf("Could not shutdown the auth server: %s", err)
}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

View File

@@ -1,4 +0,0 @@
apiVersion: v1
current-context: dummy
kind: Config
preferences: {}

View File

@@ -2,37 +2,30 @@
## Prerequisite
- You have an administrator role of the Keycloak realm.
- You have an administrator role of the Kubernetes cluster.
- 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.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Keycloak
Open the Keycloak and create an OIDC client as follows:
- Client ID: `kubernetes`
- Valid Redirect URLs:
- `http://localhost:8000`
- `http://localhost:18000` (used if the port 8000 is already in use)
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
You can associate client roles by adding the following mapper:
- Name: `groups`
- Mapper Type: `User Client Role`
- Client ID: `kubernetes`
- Client Role prefix: `kubernetes:`
- Token Claim Name: `groups`
- Add to ID token: on
- Groups claim: `groups`
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
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).
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
### kops
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
@@ -57,7 +50,7 @@ roleRef:
name: cluster-admin
subjects:
- kind: Group
name: kubernetes:admin
name: /kubernetes:admin
```
You can create a custom role and assign it as well.

11
go.mod
View File

@@ -5,19 +5,24 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.3.1
github.com/golang/mock v1.2.0
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/int128/oauth2cli v1.4.0
github.com/int128/oauth2cli v1.2.1
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.6 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/pflag v1.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.3.0 // indirect
go.uber.org/dig v1.7.0
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
golang.org/x/net v0.0.0-20190328230028-74de082e2cca // indirect
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.3.0 // indirect

26
go.sum
View File

@@ -8,20 +8,26 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/int128/oauth2cli v1.4.0 h1:Xt4uk2lIb9Mf9Xyd5o43Hf9iV5izb2jYK3zRX/cPgh0=
github.com/int128/oauth2cli v1.4.0/go.mod h1:81pWOyFVt1TRyZ7lZDtZuAGOE/S/jEpb1mpocRopI6U=
github.com/int128/oauth2cli v1.1.0 h1:qAT6C8GyaLaSf0aseQUTcJvZ+j2MueETzGkpoFow0kc=
github.com/int128/oauth2cli v1.1.0/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
github.com/int128/oauth2cli v1.2.1 h1:rhYQ++Kijz/sleAfzy2u2qEsQJCQSHVYjANgOM/LfLA=
github.com/int128/oauth2cli v1.2.1/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
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.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.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=
@@ -41,17 +47,18 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
go.uber.org/dig v1.7.0/go.mod h1:z+dSd2TP9Usi48jL8M3v63iSBVkiwtVyMKxMZYYauPg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
@@ -59,7 +66,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -2,55 +2,79 @@ package kubeconfig
import (
"strings"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
// OIDCConfig represents config of an oidc auth-provider.
type OIDCConfig map[string]string
// 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, errors.Errorf("context %s does not exist", config.CurrentContext)
}
authInfo := config.AuthInfos[context.AuthInfo]
if authInfo == nil {
return nil, errors.Errorf("auth-info %s does not exist", context.AuthInfo)
}
if authInfo.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is not set")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
}
// OIDCAuthProvider represents OIDC configuration in the kubeconfig.
type OIDCAuthProvider api.AuthProviderConfig
// IDPIssuerURL returns the idp-issuer-url.
func (c OIDCConfig) IDPIssuerURL() string {
return c["idp-issuer-url"]
func (c *OIDCAuthProvider) IDPIssuerURL() string {
return c.Config["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c OIDCConfig) ClientID() string {
return c["client-id"]
func (c *OIDCAuthProvider) ClientID() string {
return c.Config["client-id"]
}
// ClientSecret returns the client-secret.
func (c OIDCConfig) ClientSecret() string {
return c["client-secret"]
func (c *OIDCAuthProvider) ClientSecret() string {
return c.Config["client-secret"]
}
// IDPCertificateAuthority returns the idp-certificate-authority.
func (c OIDCConfig) IDPCertificateAuthority() string {
return c["idp-certificate-authority"]
func (c *OIDCAuthProvider) IDPCertificateAuthority() string {
return c.Config["idp-certificate-authority"]
}
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
func (c OIDCConfig) IDPCertificateAuthorityData() string {
return c["idp-certificate-authority-data"]
func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
return c.Config["idp-certificate-authority-data"]
}
// ExtraScopes returns the extra-scopes.
func (c OIDCConfig) ExtraScopes() []string {
if c["extra-scopes"] == "" {
func (c *OIDCAuthProvider) ExtraScopes() []string {
if c.Config["extra-scopes"] == "" {
return []string{}
}
return strings.Split(c["extra-scopes"], ",")
return strings.Split(c.Config["extra-scopes"], ",")
}
// IDToken returns the id-token.
func (c OIDCConfig) IDToken() string {
return c["id-token"]
func (c *OIDCAuthProvider) IDToken() string {
return c.Config["id-token"]
}
// SetIDToken replaces the id-token.
func (c OIDCConfig) SetIDToken(idToken string) {
c["id-token"] = idToken
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
c.Config["id-token"] = idToken
}
// SetRefreshToken replaces the refresh-token.
func (c OIDCConfig) SetRefreshToken(refreshToken string) {
c["refresh-token"] = refreshToken
func (c *OIDCAuthProvider) SetRefreshToken(refreshToken string) {
c.Config["refresh-token"] = refreshToken
}

View File

@@ -1,66 +0,0 @@
// Package kubeconfig provides the models of kubeconfig file.
package kubeconfig
import (
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
type ContextName string
type UserName string
// Config represents a config.
type Config api.Config
// Context represents a context.
type Context api.Context
// User represents a user.
type User api.AuthInfo
// CurrentAuth represents the current authentication, that is,
// context, user and auth-provider.
type CurrentAuth struct {
ContextName ContextName // empty if UserName is given
Context *Context // nil if UserName is given
UserName UserName
User *User
OIDCConfig OIDCConfig
}
// FindCurrentAuth resolves the current context and user.
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func FindCurrentAuth(config *Config, contextName ContextName, userName UserName) (*CurrentAuth, error) {
var kubeContext *Context
if userName == "" {
if contextName == "" {
contextName = ContextName(config.CurrentContext)
}
contextNode := config.Contexts[string(contextName)]
if contextNode == nil {
return nil, errors.Errorf("context %s does not exist", contextName)
}
kubeContext = (*Context)(contextNode)
userName = UserName(kubeContext.AuthInfo)
}
userNode := config.AuthInfos[string(userName)]
if userNode == nil {
return nil, errors.Errorf("user %s does not exist", userName)
}
user := (*User)(userNode)
if user.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is missing")
}
if user.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider must be oidc but is %s", user.AuthProvider.Name)
}
return &CurrentAuth{
ContextName: contextName,
Context: kubeContext,
UserName: userName,
User: user,
OIDCConfig: user.AuthProvider.Config,
}, nil
}

View File

@@ -3,7 +3,7 @@ class Kubelogin < Formula
homepage "https://github.com/int128/kubelogin"
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
version "{{ env "VERSION" }}"
sha256 "{{ sha256 .darwin_amd64_archive }}"
sha256 "{{ .darwin_amd64_zip_sha256 }}"
def install
bin.install "kubelogin" => "kubelogin"
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"

View File

@@ -26,7 +26,7 @@ spec:
version: {{ env "VERSION" }}
platforms:
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
sha256: "{{ sha256 .linux_amd64_archive }}"
sha256: "{{ .linux_amd64_zip_sha256 }}"
bin: kubelogin
files:
- from: "kubelogin"
@@ -36,7 +36,7 @@ spec:
os: linux
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip
sha256: "{{ sha256 .darwin_amd64_archive }}"
sha256: "{{ .darwin_amd64_zip_sha256 }}"
bin: kubelogin
files:
- from: "kubelogin"
@@ -46,7 +46,7 @@ spec:
os: darwin
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_windows_amd64.zip
sha256: "{{ sha256 .windows_amd64_archive }}"
sha256: "{{ .windows_amd64_zip_sha256 }}"
bin: kubelogin.exe
files:
- from: "kubelogin.exe"

View File

@@ -1,10 +1,6 @@
package usecases
import (
"context"
"github.com/int128/kubelogin/kubeconfig"
)
import "context"
//go:generate mockgen -package mock_usecases -destination ../mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases/interfaces Login
@@ -13,11 +9,8 @@ type Login interface {
}
type LoginIn struct {
KubeConfigFilename string // Default to the environment variable or global config as kubectl
KubeContextName kubeconfig.ContextName // Default to the current context but ignored if KubeUserName is set
KubeUserName kubeconfig.UserName // Default to the user of the context
CertificateAuthorityFilename string // Optional
SkipTLSVerify bool
SkipOpenBrowser bool
ListenPort []int
KubeConfig string
SkipTLSVerify bool
SkipOpenBrowser bool
ListenPort int
}

View File

@@ -12,7 +12,7 @@ import (
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials CONTEXT_NAME \
kubectl config set-credentials %[1]s \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
@@ -31,47 +31,59 @@ type Login struct {
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
u.Logger.Debugf(1, "WARNING: Log may contain your secrets, e.g. token or password")
mergedKubeConfig, err := u.KubeConfig.LoadByDefaultRules(in.KubeConfigFilename)
u.Logger.Debugf(1, "Loading %s", in.KubeConfig)
cfg, err := u.KubeConfig.LoadFromFile(in.KubeConfig)
if err != nil {
return errors.Wrapf(err, "could not load the kubeconfig")
}
auth, err := kubeconfig.FindCurrentAuth(mergedKubeConfig, in.KubeContextName, in.KubeUserName)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage)
return errors.Wrapf(err, "could not find the current authentication provider")
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
destinationKubeConfigFilename := auth.User.LocationOfOrigin
if destinationKubeConfigFilename == "" {
return errors.Errorf("could not determine the kubeconfig to write")
}
u.Logger.Debugf(1, "A token will be written to %s", destinationKubeConfigFilename)
hc, err := u.HTTP.NewClient(adaptors.HTTPClientConfig{
OIDCConfig: auth.OIDCConfig,
CertificateAuthorityFilename: in.CertificateAuthorityFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not set up a HTTP client")
return errors.Wrapf(err, "could not read the kubeconfig")
}
if auth.OIDCConfig.IDToken() != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := u.OIDC.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig, Client: hc})
if err == nil {
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
u.dumpIDToken(token)
return nil
u.Logger.Printf("Using current-context: %s", cfg.CurrentContext)
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage, cfg.CurrentContext)
return errors.Wrapf(err, "could not find an oidc auth-provider in the kubeconfig")
}
clientConfig := u.HTTP.NewClientConfig()
clientConfig.SetSkipTLSVerify(in.SkipTLSVerify)
if authProvider.IDPCertificateAuthority() != "" {
filename := authProvider.IDPCertificateAuthority()
u.Logger.Printf("Using the certificate %s", filename)
if err := clientConfig.AddCertificateFromFile(filename); err != nil {
u.Logger.Printf("Skip the certificate %s: %s", filename, err)
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
if authProvider.IDPCertificateAuthorityData() != "" {
encoded := authProvider.IDPCertificateAuthorityData()
u.Logger.Printf("Using the certificate of idp-certificate-authority-data")
if err := clientConfig.AddEncodedCertificate(encoded); err != nil {
u.Logger.Printf("Skip the certificate of idp-certificate-authority-data: %s", err)
}
}
hc, err := u.HTTP.NewClient(clientConfig)
if err != nil {
return errors.Wrapf(err, "could not create a HTTP client")
}
if token := u.verifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
IDToken: authProvider.IDToken(),
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
Client: hc,
}); token != nil {
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
u.dumpIDToken(token)
return nil
}
out, err := u.OIDC.Authenticate(ctx,
adaptors.OIDCAuthenticateIn{
Config: auth.OIDCConfig,
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: hc,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
@@ -82,14 +94,19 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
},
})
if err != nil {
return errors.Wrapf(err, "could not get a token from the OIDC provider")
return errors.Wrapf(err, "could not get token from OIDC provider")
}
u.Logger.Printf("You got a valid token until %s", out.VerifiedIDToken.Expiry)
u.dumpIDToken(out.VerifiedIDToken)
authProvider.SetIDToken(out.IDToken)
authProvider.SetRefreshToken(out.RefreshToken)
if err := u.writeToken(destinationKubeConfigFilename, auth.UserName, out); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", in.KubeConfig)
if err := u.KubeConfig.WriteToFile(cfg, in.KubeConfig); err != nil {
return errors.Wrapf(err, "could not update the kubeconfig")
}
u.Logger.Printf("Updated %s", in.KubeConfig)
return nil
}
@@ -103,21 +120,14 @@ func (u *Login) dumpIDToken(token *oidc.IDToken) {
}
}
func (u *Login) writeToken(filename string, userName kubeconfig.UserName, out *adaptors.OIDCAuthenticateOut) error {
config, err := u.KubeConfig.LoadFromFile(filename)
func (u *Login) verifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) *oidc.IDToken {
if in.IDToken == "" {
return nil
}
token, err := u.OIDC.VerifyIDToken(ctx, in)
if err != nil {
return errors.Wrapf(err, "could not load %s", filename)
u.Logger.Debugf(1, "Could not verify the ID token in the kubeconfig: %s", err)
return nil
}
auth, err := kubeconfig.FindCurrentAuth(config, "", userName)
if err != nil {
return errors.Wrapf(err, "could not find the user %s in %s", userName, filename)
}
auth.OIDCConfig.SetIDToken(out.IDToken)
auth.OIDCConfig.SetRefreshToken(out.RefreshToken)
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", filename)
if err := u.KubeConfig.WriteToFile(config, filename); err != nil {
return errors.Wrapf(err, "could not update %s", filename)
}
u.Logger.Printf("Updated %s", filename)
return nil
return token
}

View File

@@ -9,174 +9,89 @@ import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
type loginTestFixture struct {
googleOIDCConfig kubeconfig.OIDCConfig
googleOIDCConfigWithToken kubeconfig.OIDCConfig
googleKubeConfig *kubeconfig.Config
googleKubeConfigWithToken *kubeconfig.Config
keycloakOIDCConfig kubeconfig.OIDCConfig
keycloakOIDCConfigWithToken kubeconfig.OIDCConfig
keycloakKubeConfig *kubeconfig.Config
keycloakKubeConfigWithToken *kubeconfig.Config
mergedKubeConfig *kubeconfig.Config
}
func newLoginTestFixture() loginTestFixture {
var f loginTestFixture
f.googleOIDCConfig = kubeconfig.OIDCConfig{
"client-id": "GOOGLE_CLIENT_ID",
"client-secret": "GOOGLE_CLIENT_SECRET",
"idp-issuer-url": "https://accounts.google.com",
}
f.googleKubeConfig = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"googleContext": {
LocationOfOrigin: "/path/to/google",
AuthInfo: "google",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
LocationOfOrigin: "/path/to/google",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.googleOIDCConfig,
},
},
},
}
f.googleOIDCConfigWithToken = kubeconfig.OIDCConfig{
"client-id": "GOOGLE_CLIENT_ID",
"client-secret": "GOOGLE_CLIENT_SECRET",
"idp-issuer-url": "https://accounts.google.com",
"id-token": "YOUR_ID_TOKEN",
"refresh-token": "YOUR_REFRESH_TOKEN",
}
f.googleKubeConfigWithToken = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"googleContext": {
LocationOfOrigin: "/path/to/google",
AuthInfo: "google",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
LocationOfOrigin: "/path/to/google",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.googleOIDCConfigWithToken,
},
},
},
}
f.keycloakOIDCConfig = kubeconfig.OIDCConfig{
"client-id": "KEYCLOAK_CLIENT_ID",
"client-secret": "KEYCLOAK_CLIENT_SECRET",
"idp-issuer-url": "https://keycloak.example.com",
}
f.keycloakKubeConfig = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"keycloakContext": {
LocationOfOrigin: "/path/to/keycloak",
AuthInfo: "keycloak",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"keycloak": {
LocationOfOrigin: "/path/to/keycloak",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.keycloakOIDCConfig,
},
},
},
}
f.keycloakOIDCConfigWithToken = kubeconfig.OIDCConfig{
"client-id": "KEYCLOAK_CLIENT_ID",
"client-secret": "KEYCLOAK_CLIENT_SECRET",
"idp-issuer-url": "https://keycloak.example.com",
"id-token": "YOUR_ID_TOKEN",
"refresh-token": "YOUR_REFRESH_TOKEN",
}
f.keycloakKubeConfigWithToken = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"keycloakContext": {
LocationOfOrigin: "/path/to/keycloak",
AuthInfo: "keycloak",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"keycloak": {
LocationOfOrigin: "/path/to/keycloak",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.keycloakOIDCConfigWithToken,
},
},
},
}
f.mergedKubeConfig = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"googleContext": {
LocationOfOrigin: "/path/to/google",
AuthInfo: "google",
Cluster: "example.k8s.local",
},
"keycloakContext": {
LocationOfOrigin: "/path/to/keycloak",
AuthInfo: "keycloak",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
LocationOfOrigin: "/path/to/google",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.googleOIDCConfig,
},
},
"keycloak": {
LocationOfOrigin: "/path/to/keycloak",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.keycloakOIDCConfig,
},
},
},
}
return f
}
func TestLogin_Do(t *testing.T) {
httpClient := &http.Client{}
newMockOIDC := func(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateIn) *mock_adaptors.MockOIDC {
newMockKubeConfig := func(ctrl *gomock.Controller, in *api.Config, out *api.Config) adaptors.KubeConfig {
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
kubeConfig.EXPECT().
LoadFromFile("/path/to/kubeconfig").
Return(in, nil)
kubeConfig.EXPECT().
WriteToFile(out, "/path/to/kubeconfig")
return kubeConfig
}
newMockHTTP := func(ctrl *gomock.Controller, config adaptors.HTTPClientConfig) adaptors.HTTP {
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClientConfig().
Return(config)
mockHTTP.EXPECT().
NewClient(config).
Return(httpClient, nil)
return mockHTTP
}
newInConfig := func() *api.Config {
return &api.Config{
APIVersion: "v1",
CurrentContext: "default",
Contexts: map[string]*api.Context{
"default": {
AuthInfo: "google",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"client-id": "YOUR_CLIENT_ID",
"client-secret": "YOUR_CLIENT_SECRET",
"idp-issuer-url": "https://accounts.google.com",
},
},
},
},
}
}
newOutConfig := func(in *api.Config) *api.Config {
config := in.DeepCopy()
config.AuthInfos["google"].AuthProvider.Config["id-token"] = "YOUR_ID_TOKEN"
config.AuthInfos["google"].AuthProvider.Config["refresh-token"] = "YOUR_REFRESH_TOKEN"
return config
}
t.Run("Defaults", func(t *testing.T) {
inConfig := newInConfig()
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, in, gomock.Any()).
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Do(func(_ context.Context, _ adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) {
cb.ShowLocalServerURL("http://localhost:10000")
}).
@@ -185,218 +100,58 @@ func TestLogin_Do(t *testing.T) {
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDC
}
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfigFilename", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("/path/to/kubeconfig").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfigFilename: "/path/to/kubeconfig",
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeContextName", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.keycloakOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/keycloak").
Return(f.keycloakKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.keycloakKubeConfigWithToken, "/path/to/keycloak")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.keycloakOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeContextName: "keycloakContext",
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeUserName", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.keycloakOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/keycloak").
Return(f.keycloakKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.keycloakKubeConfigWithToken, "/path/to/keycloak")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.keycloakOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeUserName: "keycloak",
ListenPort: []int{10000},
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
inConfig := newInConfig()
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
SkipTLSVerify: true,
}).
Return(httpClient, nil)
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(true)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
SkipTLSVerify: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -404,43 +159,43 @@ func TestLogin_Do(t *testing.T) {
})
t.Run("SkipOpenBrowser", func(t *testing.T) {
inConfig := newInConfig()
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
SkipOpenBrowser: true,
}
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
SkipOpenBrowser: true,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
SkipOpenBrowser: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -448,137 +203,317 @@ func TestLogin_Do(t *testing.T) {
})
t.Run("KubeConfig/ValidToken", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "VALID"
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
f.googleOIDCConfig.SetIDToken("VALID_TOKEN")
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
kubeConfig.EXPECT().
LoadFromFile("/path/to/kubeconfig").
Return(inConfig, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{
Config: f.googleOIDCConfig,
Client: httpClient,
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
IDToken: "VALID",
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
Client: httpClient,
}).
Return(&oidc.IDToken{}, nil)
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
KubeConfig: kubeConfig,
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/InvalidToken", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "EXPIRED"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
f.googleOIDCConfig.SetIDToken("EXPIRED_TOKEN")
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
mockOIDC := newMockOIDC(ctrl, ctx, adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
})
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{
Config: f.googleOIDCConfig,
Client: httpClient,
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
IDToken: "EXPIRED",
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
Client: httpClient,
}).
Return(nil, errors.New("token is expired"))
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("Certificates", func(t *testing.T) {
t.Run("KubeConfig/extra-scopes", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["extra-scopes"] = "email,profile"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
f.googleOIDCConfig["idp-certificate-authority"] = "/path/to/cert2"
f.googleOIDCConfig["idp-certificate-authority-data"] = "base64encoded"
f.googleOIDCConfigWithToken["idp-certificate-authority"] = "/path/to/cert2"
f.googleOIDCConfigWithToken["idp-certificate-authority-data"] = "base64encoded"
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
CertificateAuthorityFilename: "/path/to/cert1",
}).
Return(httpClient, nil)
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
CertificateAuthorityFilename: "/path/to/cert1",
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority"] = "/path/to/cert"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddCertificateFromFile("/path/to/cert")
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority/error", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority"] = "/path/to/cert"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddCertificateFromFile("/path/to/cert").
Return(errors.New("not found"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority-data", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority-data"] = "base64encoded"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddEncodedCertificate("base64encoded")
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/idp-certificate-authority-data/error", func(t *testing.T) {
inConfig := newInConfig()
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority-data"] = "base64encoded"
outConfig := newOutConfig(inConfig)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
httpClientConfig.EXPECT().
AddEncodedCertificate("base64encoded").
Return(errors.New("invalid"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}, gomock.Any()).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Login{
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
HTTP: newMockHTTP(ctrl, httpClientConfig),
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}