Compare commits

...

15 Commits

Author SHA1 Message Date
Hidetake Iwata
000711f52e Add support of HTTP_PROXY, HTTPS_PROXY and NO_PROXY (#51) 2019-04-09 13:17:12 +09:00
Hidetake Iwata
e465c4852b Introduce dig container (#50) 2019-04-09 12:37:44 +09:00
Hidetake Iwata
003badb0bc Fix nil pointer error (#49) 2019-04-09 10:19:25 +09:00
Hidetake Iwata
0873a193a5 Refactor: extract adaptors.Logger 2019-04-08 16:07:26 +09:00
Hidetake Iwata
5e80b1858e Refactor: logs 2019-04-08 16:07:26 +09:00
Hidetake Iwata
c816281657 Refactor: add test of usecases.Login 2019-04-08 13:35:01 +09:00
Hidetake Iwata
0db49860f9 Refactor: extract adaptors.HTTPClientConfig 2019-04-08 13:35:01 +09:00
Hidetake Iwata
d70c9db036 Refactor: extract adaptors.HTTP 2019-04-08 13:35:01 +09:00
Hidetake Iwata
675b5e5fff Refactor: extract adaptors.OIDC 2019-04-08 13:35:01 +09:00
Hidetake Iwata
e3bfc321a2 Refactor: extract adaptors.KubeConfig 2019-04-08 13:35:01 +09:00
Hidetake Iwata
b34d0fb32f Refactor: add di package 2019-04-08 13:35:01 +09:00
Hidetake Iwata
4c61a71ed4 Add test of adaptors.Cmd (#45) 2019-04-07 19:33:53 +09:00
Hidetake Iwata
8a02ed0fb0 Split cli package into adaptors and use-cases (#44) 2019-04-05 14:52:15 +09:00
Hidetake Iwata
3485c5408e Replace with errors.Errorf() and errors.Wrapf() (#43) 2019-04-05 12:12:00 +09:00
Hidetake Iwata
fb99977e98 Update README.md 2019-04-05 10:53:32 +09:00
36 changed files with 1308 additions and 329 deletions

View File

@@ -10,7 +10,7 @@ all: $(TARGET)
check:
golint
go vet
$(MAKE) -C cli_test/authserver/testdata
$(MAKE) -C adaptors_test/authserver/testdata
go test -v ./...
$(TARGET): $(wildcard *.go)

View File

@@ -4,7 +4,7 @@ This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](h
It gets a token from the OIDC provider and writes it to the kubeconfig.
## TL;DR
## Getting Started
You need to setup the following components:
@@ -13,11 +13,21 @@ You need to setup the following components:
- Role for your group or user
- kubectl authentication
You can install this by brew tap or from the [releases](https://github.com/int128/kubelogin/releases).
You can install [the latest release](https://github.com/int128/kubelogin/releases) by the following ways:
```sh
# Homebrew
brew tap int128/kubelogin
brew install kubelogin
# Krew (experimental)
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.0/oidc-login.yaml
kubectl krew install --manifest oidc-login.yaml
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
After initial setup or when the token has been expired, just run:
@@ -31,7 +41,7 @@ After initial setup or when the token has been expired, just run:
2018/08/27 15:03:09 Updated /home/user/.kube/config
```
or run as a plugin:
or run as a kubectl plugin:
```
% kubectl oidc-login
@@ -118,6 +128,12 @@ kubectl config set-credentials keycloak \
If kubelogin could not parse the certificate, it shows a warning and skips it.
### HTTP Proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
## Contributions
This is an open source software licensed under Apache License 2.0.

73
adaptors/cmd.go Normal file
View File

@@ -0,0 +1,73 @@
package adaptors
import (
"context"
"fmt"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"go.uber.org/dig"
)
func NewCmd(i Cmd) adaptors.Cmd {
return &i
}
type Cmd struct {
dig.In
Login usecases.Login
Logger adaptors.Logger
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var o cmdOptions
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.Logf("Error: %s", err)
return 1
}
if len(args) > 0 {
cmd.Logger.Logf("Error: too many arguments")
return 1
}
kubeConfig, err := o.ExpandKubeConfig()
if err != nil {
cmd.Logger.Logf("Error: invalid option: %s", err)
return 1
}
in := usecases.LoginIn{
KubeConfig: kubeConfig,
ListenPort: o.ListenPort,
SkipTLSVerify: o.SkipTLSVerify,
SkipOpenBrowser: o.SkipOpenBrowser,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Logf("Error: %s", err)
return 1
}
return 0
}
type cmdOptions struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *cmdOptions) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig)
}
return d, nil
}

87
adaptors/cmd_test.go Normal file
View File

@@ -0,0 +1,87 @@
package adaptors
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/mitchellh/go-homedir"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 8000,
})
cmd := Cmd{
Login: login,
Logger: t,
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 10080,
SkipTLSVerify: true,
SkipOpenBrowser: true,
})
cmd := Cmd{
Login: login,
Logger: t,
}
exitCode := cmd.Run(ctx, []string{executable,
"--listen-port", "10080",
"--insecure-skip-tls-verify",
"--skip-open-browser",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: t,
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
}
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
}

74
adaptors/http.go Normal file
View File

@@ -0,0 +1,74 @@
package adaptors
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/pkg/errors"
)
func NewHTTP() adaptors.HTTP {
return &HTTP{}
}
type HTTP struct{}
func (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
return &httpClientConfig{
certPool: x509.NewCertPool(),
}
}
func (*HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: config.TLSConfig(),
Proxy: http.ProxyFromEnvironment,
},
}, nil
}
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 c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate from %s", filename)
}
return nil
}
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 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

@@ -0,0 +1,58 @@
package adaptors
import (
"context"
"crypto/tls"
"net/http"
"github.com/coreos/go-oidc"
"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,HTTPClientConfig,OIDC
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type KubeConfig interface {
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 interface {
AddCertificateFromFile(filename string) error
AddEncodedCertificate(base64String string) error
SetSkipTLSVerify(b bool)
TLSConfig() *tls.Config
}
type OIDC interface {
Authenticate(ctx context.Context, in OIDCAuthenticateIn) (*OIDCAuthenticateOut, error)
}
type OIDCAuthenticateIn struct {
Issuer string
ClientID string
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
type OIDCAuthenticateOut struct {
VerifiedIDToken *oidc.IDToken
IDToken string
RefreshToken string
}
type Logger interface {
Logf(format string, v ...interface{})
}

30
adaptors/kubeconfig.go Normal file
View File

@@ -0,0 +1,30 @@
package adaptors
import (
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func NewKubeConfig() adaptors.KubeConfig {
return &KubeConfig{}
}
type KubeConfig struct{}
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 config, err
}
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)
}
return err
}

17
adaptors/logger.go Normal file
View File

@@ -0,0 +1,17 @@
package adaptors
import (
"log"
"github.com/int128/kubelogin/adaptors/interfaces"
)
func NewLogger() adaptors.Logger {
return &Logger{}
}
type Logger struct{}
func (*Logger) Logf(format string, v ...interface{}) {
log.Printf(format, v...)
}

View File

@@ -0,0 +1,216 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,OIDC)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
import (
context "context"
tls "crypto/tls"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
api "k8s.io/client-go/tools/clientcmd/api"
http "net/http"
reflect "reflect"
)
// MockKubeConfig is a mock of KubeConfig interface
type MockKubeConfig struct {
ctrl *gomock.Controller
recorder *MockKubeConfigMockRecorder
}
// MockKubeConfigMockRecorder is the mock recorder for MockKubeConfig
type MockKubeConfigMockRecorder struct {
mock *MockKubeConfig
}
// NewMockKubeConfig creates a new mock instance
func NewMockKubeConfig(ctrl *gomock.Controller) *MockKubeConfig {
mock := &MockKubeConfig{ctrl: ctrl}
mock.recorder = &MockKubeConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockKubeConfig) EXPECT() *MockKubeConfigMockRecorder {
return m.recorder
}
// LoadFromFile mocks base method
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*api.Config, error) {
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
ret0, _ := ret[0].(*api.Config)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadFromFile indicates an expected call of LoadFromFile
func (mr *MockKubeConfigMockRecorder) LoadFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadFromFile", reflect.TypeOf((*MockKubeConfig)(nil).LoadFromFile), arg0)
}
// WriteToFile mocks base method
func (m *MockKubeConfig) WriteToFile(arg0 *api.Config, arg1 string) error {
ret := m.ctrl.Call(m, "WriteToFile", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// WriteToFile indicates an expected call of WriteToFile
func (mr *MockKubeConfigMockRecorder) WriteToFile(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteToFile", reflect.TypeOf((*MockKubeConfig)(nil).WriteToFile), arg0, arg1)
}
// MockHTTP is a mock of HTTP interface
type MockHTTP struct {
ctrl *gomock.Controller
recorder *MockHTTPMockRecorder
}
// MockHTTPMockRecorder is the mock recorder for MockHTTP
type MockHTTPMockRecorder struct {
mock *MockHTTP
}
// NewMockHTTP creates a new mock instance
func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP {
mock := &MockHTTP{ctrl: ctrl}
mock.recorder = &MockHTTPMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder {
return m.recorder
}
// NewClient mocks base method
func (m *MockHTTP) NewClient(arg0 interfaces.HTTPClientConfig) (*http.Client, error) {
ret := m.ctrl.Call(m, "NewClient", arg0)
ret0, _ := ret[0].(*http.Client)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewClient indicates an expected call of NewClient
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
recorder *MockOIDCMockRecorder
}
// MockOIDCMockRecorder is the mock recorder for MockOIDC
type MockOIDCMockRecorder struct {
mock *MockOIDC
}
// NewMockOIDC creates a new mock instance
func NewMockOIDC(ctrl *gomock.Controller) *MockOIDC {
mock := &MockOIDC{ctrl: ctrl}
mock.recorder = &MockOIDCMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
return m.recorder
}
// Authenticate mocks base method
func (m *MockOIDC) Authenticate(arg0 context.Context, arg1 interfaces.OIDCAuthenticateIn) (*interfaces.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1)
ret0, _ := ret[0].(*interfaces.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authenticate indicates an expected call of Authenticate
func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1)
}

56
adaptors/oidc.go Normal file
View File

@@ -0,0 +1,56 @@
package adaptors
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
func NewOIDC() adaptors.OIDC {
return &OIDC{}
}
type OIDC struct{}
func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn) (*adaptors.OIDCAuthenticateOut, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
Scopes: append(in.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
}
token, err := flow.GetToken(ctx)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
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")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}

View File

@@ -13,6 +13,7 @@ import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
)
type handler struct {
@@ -89,7 +90,7 @@ func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
if h.Scope != q.Get("scope") {
return fmt.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
return errors.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.authCode)
http.Redirect(w, r, to, 302)
@@ -100,7 +101,7 @@ func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
return err
}
if h.authCode != r.Form.Get("code") {
return fmt.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
return errors.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
}
w.Header().Add("Content-Type", "application/json")
if err := h.token.Execute(w, h); err != nil {

View File

@@ -1,20 +1,21 @@
package cli_test
package adaptors_test
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
"github.com/int128/kubelogin/cli"
"github.com/int128/kubelogin/cli_test/authserver"
"github.com/int128/kubelogin/cli_test/kubeconfig"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors_test/authserver"
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
"github.com/int128/kubelogin/di"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
@@ -25,16 +26,17 @@ import (
// 3. Open a request for port 8000.
// 4. Wait for the CLI.
// 5. Shutdown the auth server.
func TestE2E(t *testing.T) {
//
func TestCmd_Run(t *testing.T) {
data := map[string]struct {
kubeconfigValues kubeconfig.Values
cli cli.CLI
args []string
serverConfig authserver.Config
clientTLS *tls.Config
}{
"NoTLS": {
kubeconfig.Values{Issuer: "http://localhost:9000"},
cli.CLI{},
[]string{"kubelogin"},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
@@ -43,7 +45,7 @@ func TestE2E(t *testing.T) {
Issuer: "http://localhost:9000",
ExtraScopes: "profile groups",
},
cli.CLI{},
[]string{"kubelogin"},
authserver.Config{
Issuer: "http://localhost:9000",
Scope: "profile groups openid",
@@ -52,7 +54,7 @@ func TestE2E(t *testing.T) {
},
"SkipTLSVerify": {
kubeconfig.Values{Issuer: "https://localhost:9000"},
cli.CLI{SkipTLSVerify: true},
[]string{"kubelogin", "--insecure-skip-tls-verify"},
authserver.Config{
Issuer: "https://localhost:9000",
Cert: authserver.ServerCert,
@@ -65,7 +67,7 @@ func TestE2E(t *testing.T) {
Issuer: "https://localhost:9000",
IDPCertificateAuthority: authserver.CACert,
},
cli.CLI{},
[]string{"kubelogin"},
authserver.Config{
Issuer: "https://localhost:9000",
Cert: authserver.ServerCert,
@@ -75,10 +77,10 @@ func TestE2E(t *testing.T) {
},
"CACertData": {
kubeconfig.Values{
Issuer: "https://localhost:9000",
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)),
},
cli.CLI{},
[]string{"kubelogin"},
authserver.Config{
Issuer: "https://localhost:9000",
Cert: authserver.ServerCert,
@@ -88,19 +90,19 @@ func TestE2E(t *testing.T) {
},
"InvalidCACertShouldBeSkipped": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
IDPCertificateAuthority: "e2e_test.go",
Issuer: "http://localhost:9000",
IDPCertificateAuthority: "cmd_test.go",
},
cli.CLI{},
[]string{"kubelogin"},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
"InvalidCACertDataShouldBeSkipped": {
kubeconfig.Values{
Issuer: "http://localhost:9000",
Issuer: "http://localhost:9000",
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")),
},
cli.CLI{},
[]string{"kubelogin"},
authserver.Config{Issuer: "http://localhost:9000"},
&tls.Config{},
},
@@ -114,13 +116,16 @@ func TestE2E(t *testing.T) {
defer server.Shutdown(ctx)
kcfg := kubeconfig.Create(t, &c.kubeconfigValues)
defer os.Remove(kcfg)
c.cli.KubeConfig = kcfg
c.cli.SkipOpenBrowser = true
c.cli.ListenPort = 8000
args := append(c.args, "--kubeconfig", kcfg, "--skip-open-browser")
var eg errgroup.Group
eg.Go(func() error {
return c.cli.Run(ctx)
return di.Invoke(func(cmd adaptors.Cmd) {
exitCode := cmd.Run(ctx, args, "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
})
})
if err := openBrowserRequest(c.clientTLS); err != nil {
cancel()
@@ -139,10 +144,10 @@ func openBrowserRequest(tlsConfig *tls.Config) error {
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
res, err := client.Get("http://localhost:8000/")
if err != nil {
return fmt.Errorf("Could not send a request: %s", err)
return errors.Wrapf(err, "could not send a request")
}
if res.StatusCode != 200 {
return fmt.Errorf("StatusCode wants 200 but %d", res.StatusCode)
return errors.Errorf("StatusCode wants 200 but %d", res.StatusCode)
}
return nil
}

View File

@@ -1,87 +0,0 @@
package auth
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"os"
oidc "github.com/coreos/go-oidc"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
)
// TokenSet is a set of tokens and claims.
type TokenSet struct {
IDToken string
RefreshToken string
}
// Config represents OIDC configuration.
type Config struct {
Issuer string
ClientID string
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
// GetTokenSet retrives a token from the OIDC provider and returns a TokenSet.
func (c *Config) GetTokenSet(ctx context.Context) (*TokenSet, error) {
if c.Client != nil {
// https://github.com/int128/kubelogin/issues/31
val, ok := os.LookupEnv("HTTPS_PROXY")
if ok {
proxyURL, err := url.Parse(val)
if err != nil {
log.Printf("HTTPS_PROXY %s cannot be parsed into a URL\n", val)
} else {
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
c.Client = &http.Client{
Transport: transport,
}
}
}
//
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.Client)
}
provider, err := oidc.NewProvider(ctx, c.Issuer)
if err != nil {
return nil, fmt.Errorf("Could not discovery the OIDC issuer: %s", err)
}
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: c.LocalServerPort,
SkipOpenBrowser: c.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
}
token, err := flow.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("Could not get a token: %s", err)
}
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: c.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, fmt.Errorf("Could not verify the id_token: %s", err)
}
log.Printf("Got token for subject=%s", verifiedIDToken.Subject)
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}

View File

@@ -1,92 +0,0 @@
package cli
import (
"context"
"fmt"
"log"
"net/http"
"github.com/int128/kubelogin/auth"
"github.com/int128/kubelogin/kubeconfig"
flags "github.com/jessevdk/go-flags"
homedir "github.com/mitchellh/go-homedir"
)
// Parse parses command line arguments and returns a CLI instance.
func Parse(osArgs []string, version string) (*CLI, error) {
var cli CLI
parser := flags.NewParser(&cli, flags.HelpFlag)
parser.LongDescription = fmt.Sprintf(`Version %s
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
version)
args, err := parser.ParseArgs(osArgs[1:])
if err != nil {
return nil, err
}
if len(args) > 0 {
return nil, fmt.Errorf("Too many argument")
}
return &cli, nil
}
// 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"`
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *CLI) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s: %s", c.KubeConfig, err)
}
return d, nil
}
// Run performs this command.
func (c *CLI) Run(ctx context.Context) error {
log.Printf("Reading %s", c.KubeConfig)
path, err := c.ExpandKubeConfig()
if err != nil {
return err
}
cfg, err := kubeconfig.Read(path)
if err != nil {
return fmt.Errorf("Could not read kubeconfig: %s", err)
}
log.Printf("Using current-context: %s", cfg.CurrentContext)
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
if err != nil {
return fmt.Errorf(`Could not find OIDC configuration in kubeconfig: %s
Did you setup kubectl for OIDC authentication?
kubectl config set-credentials %s \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`,
err, cfg.CurrentContext)
}
tlsConfig := c.tlsConfig(authProvider)
authConfig := &auth.Config{
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}},
LocalServerPort: c.ListenPort,
SkipOpenBrowser: c.SkipOpenBrowser,
}
token, err := authConfig.GetTokenSet(ctx)
if err != nil {
return fmt.Errorf("Could not get token from OIDC provider: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", c.KubeConfig)
return nil
}

View File

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

View File

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

34
di/di.go Normal file
View File

@@ -0,0 +1,34 @@
// Package di provides dependency injection.
package di
import (
"github.com/int128/kubelogin/adaptors"
adaptorsInterfaces "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"go.uber.org/dig"
)
var constructors = []interface{}{
usecases.NewLogin,
adaptors.NewCmd,
adaptors.NewKubeConfig,
adaptors.NewOIDC,
adaptors.NewHTTP,
adaptors.NewLogger,
}
// Invoke runs the function with an adaptors.Cmd instance.
func Invoke(f func(cmd adaptorsInterfaces.Cmd)) error {
c := dig.New()
for _, constructor := range constructors {
if err := c.Provide(constructor); err != nil {
return errors.Wrapf(err, "could not provide the constructor")
}
}
if err := c.Invoke(f); err != nil {
return errors.Wrapf(err, "could not invoke")
}
return nil
}

18
di/di_test.go Normal file
View File

@@ -0,0 +1,18 @@
package di_test
import (
"testing"
adaptors "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
func TestInvoke(t *testing.T) {
if err := di.Invoke(func(cmd adaptors.Cmd) {
if cmd == nil {
t.Errorf("cmd wants non-nil but nil")
}
}); err != nil {
t.Fatalf("Invoke returned error: %+v", err)
}
}

3
go.mod
View File

@@ -5,6 +5,7 @@ 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.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.1.0
@@ -13,9 +14,11 @@ require (
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 // 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

4
go.sum
View File

@@ -8,6 +8,8 @@ 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.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=
@@ -41,6 +43,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
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/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=

View File

@@ -1,9 +1,9 @@
package kubeconfig
import (
"fmt"
"strings"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
@@ -13,17 +13,17 @@ import (
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil, fmt.Errorf("context %s does not exist", config.CurrentContext)
return nil, errors.Errorf("context %s does not exist", config.CurrentContext)
}
authInfo := config.AuthInfos[context.AuthInfo]
if authInfo == nil {
return nil, fmt.Errorf("auth-info %s does not exist", context.AuthInfo)
return nil, errors.Errorf("auth-info %s does not exist", context.AuthInfo)
}
if authInfo.AuthProvider == nil {
return nil, fmt.Errorf("auth-provider is not set")
return nil, errors.Errorf("auth-provider is not set")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
return nil, errors.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
}

View File

@@ -1,16 +0,0 @@
package kubeconfig
import (
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
// Read parses the file and returns the Config.
func Read(path string) (*api.Config, error) {
return clientcmd.LoadFromFile(path)
}
// Write writes the config to the file.
func Write(config *api.Config, path string) error {
return clientcmd.WriteToFile(*config, path)
}

12
main.go
View File

@@ -5,18 +5,16 @@ import (
"log"
"os"
"github.com/int128/kubelogin/cli"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
var version = "HEAD"
func main() {
c, err := cli.Parse(os.Args, version)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := c.Run(ctx); err != nil {
if err := di.Invoke(func(cmd adaptors.Cmd) {
os.Exit(cmd.Run(context.Background(), os.Args, version))
}); err != nil {
log.Fatalf("Error: %s", err)
}
}

View File

@@ -0,0 +1,16 @@
package usecases
import "context"
//go:generate mockgen -package mock_usecases -destination ../mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases/interfaces Login
type Login interface {
Do(ctx context.Context, in LoginIn) error
}
type LoginIn struct {
KubeConfig string
SkipTLSVerify bool
SkipOpenBrowser bool
ListenPort int
}

87
usecases/login.go Normal file
View File

@@ -0,0 +1,87 @@
package usecases
import (
"context"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/pkg/errors"
"go.uber.org/dig"
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
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 \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
func NewLogin(i Login) usecases.Login {
return &i
}
type Login struct {
dig.In
KubeConfig adaptors.KubeConfig
HTTP adaptors.HTTP
OIDC adaptors.OIDC
Logger adaptors.Logger
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
cfg, err := u.KubeConfig.LoadFromFile(in.KubeConfig)
if err != nil {
return errors.Wrapf(err, "could not read the kubeconfig")
}
u.Logger.Logf("Using current-context: %s", cfg.CurrentContext)
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
if err != nil {
u.Logger.Logf(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.Logf("Using the certificate %s", filename)
if err := clientConfig.AddCertificateFromFile(filename); err != nil {
u.Logger.Logf("Skip the certificate %s: %s", filename, err)
}
}
if authProvider.IDPCertificateAuthorityData() != "" {
encoded := authProvider.IDPCertificateAuthorityData()
u.Logger.Logf("Using certificate of idp-certificate-authority-data")
if err := clientConfig.AddEncodedCertificate(encoded); err != nil {
u.Logger.Logf("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")
}
out, err := u.OIDC.Authenticate(ctx, adaptors.OIDCAuthenticateIn{
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: hc,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
})
if err != nil {
return errors.Wrapf(err, "could not get token from OIDC provider")
}
u.Logger.Logf("Got a token for subject=%s", out.VerifiedIDToken.Subject)
authProvider.SetIDToken(out.IDToken)
authProvider.SetRefreshToken(out.RefreshToken)
if err := u.KubeConfig.WriteToFile(cfg, in.KubeConfig); err != nil {
return errors.Wrapf(err, "could not update the kubeconfig")
}
u.Logger.Logf("Updated %s", in.KubeConfig)
return nil
}

426
usecases/login_test.go Normal file
View File

@@ -0,0 +1,426 @@
package usecases
import (
"context"
"net/http"
"testing"
"github.com/coreos/go-oidc"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
func TestLogin_Do(t *testing.T) {
httpClient := &http.Client{}
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, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
}).
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: t,
}
if err := u.Do(ctx, usecases.LoginIn{
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()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(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,
}).
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: t,
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
SkipTLSVerify: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("SkipOpenBrowser", 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, adaptors.OIDCAuthenticateIn{
Issuer: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{},
LocalServerPort: 10000,
Client: httpClient,
SkipOpenBrowser: true,
}).
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: t,
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
SkipOpenBrowser: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
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()
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
httpClientConfig.EXPECT().
SetSkipTLSVerify(false)
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,
}).
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: t,
}
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", 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,
}).
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: t,
}
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,
}).
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: t,
}
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,
}).
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: t,
}
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,
}).
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: t,
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfig: "/path/to/kubeconfig",
ListenPort: 10000,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
}

View File

@@ -0,0 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases/interfaces (interfaces: Login)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/usecases/interfaces"
reflect "reflect"
)
// MockLogin is a mock of Login interface
type MockLogin struct {
ctrl *gomock.Controller
recorder *MockLoginMockRecorder
}
// MockLoginMockRecorder is the mock recorder for MockLogin
type MockLoginMockRecorder struct {
mock *MockLogin
}
// NewMockLogin creates a new mock instance
func NewMockLogin(ctrl *gomock.Controller) *MockLogin {
mock := &MockLogin{ctrl: ctrl}
mock.recorder = &MockLoginMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLogin) EXPECT() *MockLoginMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockLogin) Do(arg0 context.Context, arg1 interfaces.LoginIn) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Do indicates an expected call of Do
func (mr *MockLoginMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLogin)(nil).Do), arg0, arg1)
}