mirror of
https://github.com/int128/kubelogin.git
synced 2026-03-10 12:50:23 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7bbcd44e1 | ||
|
|
7726ac6c51 | ||
|
|
adaeba4c24 | ||
|
|
e8acaa28b3 | ||
|
|
031f9fb81a | ||
|
|
8a7da83338 |
@@ -10,6 +10,8 @@ jobs:
|
||||
- run: go get github.com/golang/lint/golint
|
||||
- run: golint
|
||||
- run: go build -v
|
||||
- run: make -C integration-test/testdata
|
||||
- run: go test -v ./...
|
||||
|
||||
release:
|
||||
docker:
|
||||
|
||||
@@ -9,3 +9,7 @@ indent_size = 2
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
13
README.md
13
README.md
@@ -173,9 +173,9 @@ Run `kubelogin` and make sure you can access to the cluster.
|
||||
See the previous section for details.
|
||||
|
||||
|
||||
## Tips
|
||||
## Configuration
|
||||
|
||||
### Config file
|
||||
### Kubeconfig
|
||||
|
||||
You can set the environment variable `KUBECONFIG` to point the config file.
|
||||
Default to `~/.kube/config`.
|
||||
@@ -184,6 +184,15 @@ Default to `~/.kube/config`.
|
||||
export KUBECONFIG="$PWD/.kubeconfig"
|
||||
```
|
||||
|
||||
### OpenID Connect Provider CA Certificate
|
||||
|
||||
You can specify the CA certificate of your OpenID Connect provider by [`idp-certificate-authority` or `idp-certificate-authority-data` in the kubeconfig](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials CLUSTER_NAME \
|
||||
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
|
||||
```
|
||||
|
||||
### Setup script
|
||||
|
||||
In actual team operation, you can share the following script to your team members for easy setup.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package authz
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -18,9 +20,9 @@ type BrowserAuthCodeFlow struct {
|
||||
// GetToken returns a token.
|
||||
func (f *BrowserAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
|
||||
f.Config.RedirectURL = fmt.Sprintf("http://localhost:%d/", f.Port)
|
||||
state, err := generateOAuthState()
|
||||
state, err := generateState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Could not generate state parameter: %s", err)
|
||||
}
|
||||
log.Printf("Open http://localhost:%d for authorization", f.Port)
|
||||
code, err := f.getCode(ctx, &f.Config, state)
|
||||
@@ -34,6 +36,14 @@ func (f *BrowserAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, erro
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func generateState() (string, error) {
|
||||
var n uint64
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", n), nil
|
||||
}
|
||||
|
||||
func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config, state string) (string, error) {
|
||||
codeCh := make(chan string)
|
||||
errCh := make(chan error)
|
||||
@@ -1,11 +1,10 @@
|
||||
package authn
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/authz"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -27,7 +26,7 @@ func GetTokenSet(ctx context.Context, issuer string, clientID string, clientSecr
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not access OIDC issuer: %s", err)
|
||||
}
|
||||
flow := authz.BrowserAuthCodeFlow{
|
||||
flow := BrowserAuthCodeFlow{
|
||||
Port: 8000,
|
||||
Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
@@ -1,12 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Flow represents an authorization method.
|
||||
type Flow interface {
|
||||
GetToken(context.Context) (*oauth2.Token, error)
|
||||
}
|
||||
35
authz/cli.go
35
authz/cli.go
@@ -1,35 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// CLIAuthCodeFlow is a flow to get a token by keyboard interaction.
|
||||
type CLIAuthCodeFlow struct {
|
||||
oauth2.Config
|
||||
}
|
||||
|
||||
// GetToken returns a token by browser interaction.
|
||||
func (f *CLIAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
|
||||
f.Config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
|
||||
state, err := generateOAuthState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authCodeURL := f.Config.AuthCodeURL(state)
|
||||
log.Printf("Open %s for authorization", authCodeURL)
|
||||
fmt.Print("Enter code: ")
|
||||
var code string
|
||||
if _, err := fmt.Scanln(&code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := f.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateOAuthState() (string, error) {
|
||||
var n uint64
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", n), nil
|
||||
}
|
||||
115
cli/cli.go
Normal file
115
cli/cli.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"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"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Parse parses command line arguments and returns a CLI instance.
|
||||
func Parse(args []string) (*CLI, error) {
|
||||
var cli CLI
|
||||
parser := flags.NewParser(&cli, flags.HelpFlag)
|
||||
args, err := parser.Parse()
|
||||
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"`
|
||||
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
|
||||
// CertificateAuthority string `long:"certificate-authority" env:"KUBELOGIN_CERTIFICATE_AUTHORITY" description:"Path to a cert file for the certificate authority"`
|
||||
}
|
||||
|
||||
// ExpandKubeConfig returns an expanded KubeConfig path.
|
||||
func (c *CLI) ExpandKubeConfig() (string, error) {
|
||||
d, err := homedir.Expand(c.KubeConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not expand %s", c.KubeConfig)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Run performs this command.
|
||||
func (c *CLI) Run(ctx context.Context) error {
|
||||
path, err := c.ExpandKubeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Reading %s", path)
|
||||
cfg, err := kubeconfig.Load(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not load kubeconfig: %s", err)
|
||||
}
|
||||
log.Printf("Using current context: %s", cfg.CurrentContext)
|
||||
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
|
||||
if authInfo == nil {
|
||||
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
|
||||
}
|
||||
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not find auth-provider: %s", err)
|
||||
}
|
||||
tlsConfig, err := c.tlsConfig(authProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not configure TLS: %s", err)
|
||||
}
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
token, err := auth.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Authentication error: %s", err)
|
||||
}
|
||||
|
||||
authProvider.SetIDToken(token.IDToken)
|
||||
authProvider.SetRefreshToken(token.RefreshToken)
|
||||
kubeconfig.Write(cfg, path)
|
||||
log.Printf("Updated %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProviderConfig) (*tls.Config, error) {
|
||||
p := x509.NewCertPool()
|
||||
if authProvider.IDPCertificateAuthority() != "" {
|
||||
b, err := ioutil.ReadFile(authProvider.IDPCertificateAuthority())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read idp-certificate-authority: %s", err)
|
||||
}
|
||||
if p.AppendCertsFromPEM(b) != true {
|
||||
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority: %s", err)
|
||||
}
|
||||
log.Printf("Using CA certificate: %s", authProvider.IDPCertificateAuthority())
|
||||
}
|
||||
if authProvider.IDPCertificateAuthorityData() != "" {
|
||||
b, err := base64.StdEncoding.DecodeString(authProvider.IDPCertificateAuthorityData())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not decode idp-certificate-authority-data: %s", err)
|
||||
}
|
||||
if p.AppendCertsFromPEM(b) != true {
|
||||
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority-data: %s", err)
|
||||
}
|
||||
log.Printf("Using CA certificate of idp-certificate-authority-data")
|
||||
}
|
||||
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}
|
||||
if len(p.Subjects()) > 0 {
|
||||
cfg.RootCAs = p
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
104
integration-test/auth.go
Normal file
104
integration-test/auth.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// AuthHandler provides the stub handler for OIDC authentication.
|
||||
type AuthHandler struct {
|
||||
// Values in templates
|
||||
Issuer string
|
||||
AuthCode string
|
||||
IDToken string
|
||||
PrivateKey struct{ N, E string }
|
||||
|
||||
// Response templates
|
||||
discoveryJSON *template.Template
|
||||
tokenJSON *template.Template
|
||||
jwksJSON *template.Template
|
||||
}
|
||||
|
||||
// NewAuthHandler returns a new AuthHandler.
|
||||
func NewAuthHandler(t *testing.T, issuer string) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Issuer: issuer,
|
||||
AuthCode: "0b70006b-f62a-4438-aba5-c0b96775d8e5",
|
||||
discoveryJSON: template.Must(template.ParseFiles("testdata/oidc-discovery.json")),
|
||||
tokenJSON: template.Must(template.ParseFiles("testdata/oidc-token.json")),
|
||||
jwksJSON: template.Must(template.ParseFiles("testdata/oidc-jwks.json")),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
|
||||
Issuer: h.Issuer,
|
||||
Audience: "kubernetes",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
k, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate a key pair: %s", err)
|
||||
}
|
||||
h.IDToken, err = token.SignedString(k)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate an ID token: %s", err)
|
||||
}
|
||||
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
|
||||
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
m := r.Method
|
||||
p := r.URL.Path
|
||||
log.Printf("[auth-server] %s %s", m, r.RequestURI)
|
||||
switch {
|
||||
case m == "GET" && p == "/.well-known/openid-configuration":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := s.discoveryJSON.Execute(w, s); err != nil {
|
||||
return err
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/auth":
|
||||
// Authentication Response
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
|
||||
q := r.URL.Query()
|
||||
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), s.AuthCode)
|
||||
http.Redirect(w, r, to, 302)
|
||||
case m == "POST" && p == "/protocol/openid-connect/token":
|
||||
// Token Response
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.AuthCode != r.Form.Get("code") {
|
||||
return fmt.Errorf("code wants %s but %s", s.AuthCode, r.Form.Get("code"))
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := s.tokenJSON.Execute(w, s); err != nil {
|
||||
return err
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/certs":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := s.jwksJSON.Execute(w, s); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Not Found", 404)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.serveHTTP(w, r); err != nil {
|
||||
log.Printf("[auth-server] Error: %s", err)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
160
integration-test/integration_test.go
Normal file
160
integration-test/integration_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/cli"
|
||||
)
|
||||
|
||||
const caCert = "testdata/authserver-ca.crt"
|
||||
const tlsCert = "testdata/authserver.crt"
|
||||
const tlsKey = "testdata/authserver.key"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "http://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "http://localhost:9000",
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServe(t, authServer)
|
||||
go authenticate(t, &tls.Config{})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func TestWithSkipTLSVerify(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServeTLS(t, authServer)
|
||||
go authenticate(t, &tls.Config{InsecureSkipVerify: true})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
SkipTLSVerify: true,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func TestWithCACert(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: caCert,
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServeTLS(t, authServer)
|
||||
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func TestWithCACertData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authServer := &http.Server{
|
||||
Addr: "localhost:9000",
|
||||
Handler: NewAuthHandler(t, "https://localhost:9000"),
|
||||
}
|
||||
defer authServer.Shutdown(ctx)
|
||||
b, err := ioutil.ReadFile(caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kubeconfig := createKubeconfig(t, &kubeconfigValues{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(b),
|
||||
})
|
||||
defer os.Remove(kubeconfig)
|
||||
|
||||
go listenAndServeTLS(t, authServer)
|
||||
go authenticate(t, &tls.Config{RootCAs: loadCACert(t)})
|
||||
|
||||
c := cli.CLI{
|
||||
KubeConfig: kubeconfig,
|
||||
}
|
||||
if err := c.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
}
|
||||
|
||||
func authenticate(t *testing.T, tlsConfig *tls.Config) {
|
||||
t.Helper()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get("http://localhost:8000/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d: res=%+v", res.StatusCode, res)
|
||||
}
|
||||
}
|
||||
|
||||
func loadCACert(t *testing.T) *x509.CertPool {
|
||||
p := x509.NewCertPool()
|
||||
b, err := ioutil.ReadFile(caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !p.AppendCertsFromPEM(b) {
|
||||
t.Fatalf("Could not AppendCertsFromPEM")
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func listenAndServe(t *testing.T, s *http.Server) {
|
||||
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func listenAndServeTLS(t *testing.T, s *http.Server) {
|
||||
if err := s.ListenAndServeTLS(tlsCert, tlsKey); err != nil && err != http.ErrServerClosed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
44
integration-test/kubeconfig.go
Normal file
44
integration-test/kubeconfig.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type kubeconfigValues struct {
|
||||
Issuer string
|
||||
IDPCertificateAuthority string
|
||||
IDPCertificateAuthorityData string
|
||||
}
|
||||
|
||||
func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
|
||||
t.Helper()
|
||||
f, err := ioutil.TempFile("", "kubeconfig")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
tpl, err := template.ParseFiles("testdata/kubeconfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tpl.Execute(f, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func verifyKubeconfig(t *testing.T, kubeconfig string) {
|
||||
b, err := ioutil.ReadFile(kubeconfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Index(string(b), "id-token: ey") == -1 {
|
||||
t.Errorf("kubeconfig wants id-token but %s", string(b))
|
||||
}
|
||||
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
|
||||
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
|
||||
}
|
||||
}
|
||||
4
integration-test/testdata/.gitignore
vendored
Normal file
4
integration-test/testdata/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/CA
|
||||
*.key
|
||||
*.csr
|
||||
*.crt
|
||||
50
integration-test/testdata/Makefile
vendored
Normal file
50
integration-test/testdata/Makefile
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: authserver.crt authserver-ca.crt
|
||||
|
||||
clean:
|
||||
rm -v authserver*
|
||||
|
||||
authserver-ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
authserver-ca.csr: openssl.cnf authserver-ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key authserver-ca.key \
|
||||
-subj "/CN=Hello CA" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
authserver-ca.crt: authserver-ca.csr authserver-ca.key
|
||||
openssl x509 -req \
|
||||
-signkey authserver-ca.key \
|
||||
-in authserver-ca.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
authserver.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
authserver.csr: openssl.cnf authserver.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key authserver.key \
|
||||
-subj "/CN=localhost" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
authserver.crt: openssl.cnf authserver.csr authserver-ca.key authserver-ca.crt
|
||||
rm -fr ./CA
|
||||
mkdir -p ./CA
|
||||
touch CA/index.txt
|
||||
touch CA/index.txt.attr
|
||||
echo 00 > CA/serial
|
||||
openssl ca -config openssl.cnf \
|
||||
-extensions v3_req \
|
||||
-batch \
|
||||
-cert authserver-ca.crt \
|
||||
-keyfile authserver-ca.key \
|
||||
-in authserver.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
28
integration-test/testdata/kubeconfig.yaml
vendored
Normal file
28
integration-test/testdata/kubeconfig.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.hello.k8s.example.com
|
||||
name: hello.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: hello.k8s.local
|
||||
name: hello.k8s.local
|
||||
current-context: hello.k8s.local
|
||||
preferences: {}
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: kubernetes
|
||||
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
|
||||
idp-issuer-url: {{ .Issuer }}
|
||||
#{{ if .IDPCertificateAuthority }}
|
||||
idp-certificate-authority: {{ .IDPCertificateAuthority }}
|
||||
#{{ end }}
|
||||
#{{ if .IDPCertificateAuthorityData }}
|
||||
idp-certificate-authority-data: {{ .IDPCertificateAuthorityData }}
|
||||
#{{ end }}
|
||||
name: oidc
|
||||
85
integration-test/testdata/oidc-discovery.json
vendored
Normal file
85
integration-test/testdata/oidc-discovery.json
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"issuer": "{{ .Issuer }}",
|
||||
"authorization_endpoint": "{{ .Issuer }}/protocol/openid-connect/auth",
|
||||
"token_endpoint": "{{ .Issuer }}/protocol/openid-connect/token",
|
||||
"token_introspection_endpoint": "{{ .Issuer }}/protocol/openid-connect/token/introspect",
|
||||
"userinfo_endpoint": "{{ .Issuer }}/protocol/openid-connect/userinfo",
|
||||
"end_session_endpoint": "{{ .Issuer }}/protocol/openid-connect/logout",
|
||||
"jwks_uri": "{{ .Issuer }}/protocol/openid-connect/certs",
|
||||
"check_session_iframe": "{{ .Issuer }}/protocol/openid-connect/login-status-iframe.html",
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"implicit",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"client_credentials"
|
||||
],
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"none",
|
||||
"id_token",
|
||||
"token",
|
||||
"id_token token",
|
||||
"code id_token",
|
||||
"code token",
|
||||
"code id_token token"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public",
|
||||
"pairwise"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"userinfo_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"request_object_signing_alg_values_supported": [
|
||||
"none",
|
||||
"RS256"
|
||||
],
|
||||
"response_modes_supported": [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post"
|
||||
],
|
||||
"registration_endpoint": "{{ .Issuer }}/clients-registrations/openid-connect",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"private_key_jwt",
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt"
|
||||
],
|
||||
"token_endpoint_auth_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"claims_supported": [
|
||||
"sub",
|
||||
"iss",
|
||||
"auth_time",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"preferred_username",
|
||||
"email"
|
||||
],
|
||||
"claim_types_supported": [
|
||||
"normal"
|
||||
],
|
||||
"claims_parameter_supported": false,
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"offline_access",
|
||||
"phone",
|
||||
"address",
|
||||
"email",
|
||||
"profile"
|
||||
],
|
||||
"request_parameter_supported": true,
|
||||
"request_uri_parameter_supported": true,
|
||||
"code_challenge_methods_supported": [
|
||||
"plain",
|
||||
"S256"
|
||||
],
|
||||
"tls_client_certificate_bound_access_tokens": true
|
||||
}
|
||||
12
integration-test/testdata/oidc-jwks.json
vendored
Normal file
12
integration-test/testdata/oidc-jwks.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"alg": "RS256",
|
||||
"use": "sig",
|
||||
"kid": "xxx",
|
||||
"n": "{{ .PrivateKey.N }}",
|
||||
"e": "{{ .PrivateKey.E }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
integration-test/testdata/oidc-token.json
vendored
Normal file
7
integration-test/testdata/oidc-token.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": "44df4c82-5ce7-4260-b54d-1da0d396ef2a",
|
||||
"expires_in": 3600,
|
||||
"id_token": "{{ .IDToken }}"
|
||||
}
|
||||
37
integration-test/testdata/openssl.cnf
vendored
Normal file
37
integration-test/testdata/openssl.cnf
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = ./CA
|
||||
certs = $dir
|
||||
crl_dir = $dir
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
default_days = 365
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[ req_distinguished_name ]
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = DNS:localhost
|
||||
|
||||
[ v3_ca ]
|
||||
basicConstraints = CA:true
|
||||
@@ -16,6 +16,7 @@ func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
|
||||
return config.AuthInfos[context.AuthInfo]
|
||||
}
|
||||
|
||||
// ToOIDCAuthProviderConfig converts from api.AuthInfo to OIDCAuthProviderConfig.
|
||||
func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig, error) {
|
||||
if authInfo.AuthProvider == nil {
|
||||
return nil, fmt.Errorf("auth-provider is not set, did you setup kubectl as listed here: https://github.com/int128/kubelogin#3-setup-kubectl")
|
||||
@@ -26,6 +27,7 @@ func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig,
|
||||
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
|
||||
}
|
||||
|
||||
// OIDCAuthProviderConfig represents OIDC configuration in the kubeconfig.
|
||||
type OIDCAuthProviderConfig api.AuthProviderConfig
|
||||
|
||||
// IDPIssuerURL returns the idp-issuer-url.
|
||||
@@ -43,10 +45,22 @@ func (c *OIDCAuthProviderConfig) ClientSecret() string {
|
||||
return c.Config["client-secret"]
|
||||
}
|
||||
|
||||
// IDPCertificateAuthority returns the idp-certificate-authority.
|
||||
func (c *OIDCAuthProviderConfig) IDPCertificateAuthority() string {
|
||||
return c.Config["idp-certificate-authority"]
|
||||
}
|
||||
|
||||
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
|
||||
func (c *OIDCAuthProviderConfig) IDPCertificateAuthorityData() string {
|
||||
return c.Config["idp-certificate-authority-data"]
|
||||
}
|
||||
|
||||
// SetIDToken replaces the id-token.
|
||||
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
|
||||
c.Config["id-token"] = idToken
|
||||
}
|
||||
|
||||
// SetRefreshToken replaces the refresh-token.
|
||||
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
|
||||
c.Config["refresh-token"] = refreshToken
|
||||
}
|
||||
|
||||
72
main.go
72
main.go
@@ -2,81 +2,19 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/authn"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"golang.org/x/oauth2"
|
||||
"github.com/int128/kubelogin/cli"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
|
||||
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
|
||||
// CertificateAuthority string `long:"certificate-authority" env:"KUBELOGIN_CERTIFICATE_AUTHORITY" description:"Path to a cert file for the certificate authority"`
|
||||
}
|
||||
|
||||
func (o *options) ExpandKubeConfig() (string, error) {
|
||||
d, err := homedir.Expand(o.KubeConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not expand %s", o.KubeConfig)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func parseOptions() (*options, error) {
|
||||
var o options
|
||||
parser := flags.NewParser(&o, flags.HelpFlag)
|
||||
args, err := parser.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(args) > 0 {
|
||||
return nil, fmt.Errorf("Too many argument")
|
||||
}
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts, err := parseOptions()
|
||||
c, err := cli.Parse(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
path, err := opts.ExpandKubeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Reading %s", path)
|
||||
cfg, err := kubeconfig.Load(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not load kubeconfig: %s", err)
|
||||
}
|
||||
log.Printf("Using current context: %s", cfg.CurrentContext)
|
||||
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
|
||||
if authInfo == nil {
|
||||
log.Fatalf("Could not find current context: %s", cfg.CurrentContext)
|
||||
}
|
||||
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not find auth-provider: %s", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: opts.SkipTLSVerify},
|
||||
}}
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
|
||||
if err != nil {
|
||||
log.Fatalf("Authentication error: %s", err)
|
||||
if err := c.Run(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
authProvider.SetIDToken(token.IDToken)
|
||||
authProvider.SetRefreshToken(token.RefreshToken)
|
||||
kubeconfig.Write(cfg, path)
|
||||
log.Printf("Updated %s", path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user