Compare commits

..

24 Commits
1.4 ... 1.7

Author SHA1 Message Date
Hidetake Iwata
9c67c52b34 Fix test for #17 2018-09-14 15:06:05 +09:00
Hidetake Iwata
550396e1dd Merge pull request #17 from stang/allow-to-change-listening-port
Allow to change listening port
2018-09-14 11:20:08 +09:00
Stephane Tang
34f0578b59 Allow to change listening port
It's using port 8000 by default, which is identical as the original behavior.

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

View File

@@ -10,7 +10,7 @@ jobs:
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -v
- run: make -C integration-test/testdata
- run: make -C e2e/authserver/testdata
- run: go test -v ./...
release:
@@ -20,19 +20,21 @@ jobs:
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get github.com/mitchellh/gox
- run: go get github.com/tcnksm/ghr
- run: gox --osarch 'darwin/amd64 linux/amd64 windows/amd64 windows/386' -output 'dist/{{.Dir}}_{{.OS}}_{{.Arch}}'
- run: ghr -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" "$CIRCLE_TAG" dist
- run: curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
build:
all:
jobs:
- build
- build:
filters:
tags:
only: /.*/
- release:
filters:
branches:
ignore: /.*/
tags:
only: /.*/
requires:
- build

2
.gitignore vendored
View File

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

19
.goreleaser.yml Normal file
View File

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

159
README.md
View File

@@ -1,29 +1,44 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin)
`kubelogin` is a command to get an OpenID Connect (OIDC) token for `kubectl` authentication.
This is a command for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
It gets a token from the OIDC provider and writes it to the kubeconfig.
This should work with all OIDC providers, e.g. Keycloak, Google Identity Platform and Azure AD.
## TL;DR
1. Setup your OpenID Connect provider, e.g. Google Identity Platform or Keycloak.
1. Setup your Kubernetes cluster.
1. Setup your `kubectl`.
You need to setup the OIDC provider, Kubernetes API server and kubectl.
You can install this from the brew tap or [releases](https://github.com/int128/kubelogin/releases).
```sh
brew tap int128/kubelogin
brew install kubelogin
```
After initial setup or when the token has been expired, just run `kubelogin`.
```
% kubelogin --help
2018/08/15 19:08:58 Usage:
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
Help Options:
-h, --help Show this help message
% kubelogin
2018/08/27 15:03:06 Reading /home/user/.kube/config
2018/08/27 15:03:06 Using current context: hello.k8s.local
2018/08/27 15:03:07 Open http://localhost:8000 for authorization
```
It opens the browser and you can log in to the provider.
And then it writes the ID token and refresh token to the kubeconfig.
```
2018/08/27 15:03:07 GET /
2018/08/27 15:03:08 GET /?state=a51081925f20c043&session_state=5637cbdf-ffdc-4fab-9fc7-68a3e6f2e73f&code=ey...
2018/08/27 15:03:09 Got token for subject=cf228a73-47fe-4986-a2a8-b2ced80a884b
2018/08/27 15:03:09 Updated /home/user/.kube/config
```
Please see the later section for details.
## Getting Started with Google Account
@@ -62,20 +77,18 @@ subjects:
name: https://accounts.google.com#1234567890
```
### 3. Setup kubectl and kubelogin
### 3. Setup kubectl and Run kubelogin
Setup `kubectl` to authenticate with your identity provider.
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials CLUSTER_NAME \
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
Run `kubelogin` and open http://localhost:8000 in your browser.
```
@@ -156,27 +169,54 @@ subjects:
name: /kubernetes:admin
```
### 3. Setup kubectl and kubelogin
### 3. Setup kubectl and Run kubelogin
Setup `kubectl` to authenticate with your identity provider.
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials CLUSTER_NAME \
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
Run `kubelogin` and make sure you can access to the cluster.
See the previous section for details.
## Configuration
### Kubeconfig
```
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
Help Options:
-h, --help Show this help message
```
This supports the following keys of `auth-provider` in kubeconfig.
See also [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
Key | Direction | Value
----|-----------|------
`idp-issuer-url` | IN (Required) | Issuer URL of the provider.
`client-id` | IN (Required) | Client ID of the provider.
`client-secret` | IN (Required) | Client Secret of the provider.
`idp-certificate-authority` | IN (Optional) | CA certificate path of the provider.
`idp-certificate-authority-data` | IN (Optional) | Base64 encoded CA certificate of the provider.
`extra-scopes` | IN (Optional) | Scopes to request to the provider (comma separated).
`id-token` | OUT | ID token got from the provider.
`refresh-token` | OUT | Refresh token got from the provider.
### Kubeconfig path
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
@@ -185,37 +225,41 @@ Default to `~/.kube/config`.
export KUBECONFIG="$PWD/.kubeconfig"
```
### OpenID Connect Provider CA Certificate
### Team onboarding
You can specify the CA certificate of your OpenID Connect provider by [`idp-certificate-authority` or `idp-certificate-authority-data` in the kubeconfig](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
You can share the kubeconfig to your team members for easy setup.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider-arg idp-certificate-authority=$PWD/ca.crt
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
auth-provider:
name: oidc
config:
client-id: YOUR_CLIEND_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: YOUR_ISSUER
```
### Setup script
In actual team operation, you can share the following script to your team members for easy setup.
If you are using kops, export the kubeconfig and edit it.
```sh
#!/bin/sh -xe
CLUSTER_NAME="hello.k8s.local"
export KUBECONFIG="$PWD/.kubeconfig"
kubectl config set-cluster "$CLUSTER_NAME" \
--server https://api-xxx.xxx.elb.amazonaws.com \
--certificate-authority "$PWD/cluster.crt"
kubectl config set-credentials "$CLUSTER_NAME" \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
kubectl config use-context "$CLUSTER_NAME"
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
vim .kubeconfig
```
@@ -224,12 +268,19 @@ kubectl config use-context "$CLUSTER_NAME"
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests.
### Build
### Build and Test
```sh
go get github.com/int128/kubelogin
```
```sh
cd $GOPATH/src/github.com/int128/kubelogin
make -C e2e/authserver/testdata
go test -v ./...
```
### Release
CircleCI publishes the build to GitHub. See [.circleci/config.yml](.circleci/config.yml).
CircleCI publishes the build to GitHub.
See [.circleci/config.yml](.circleci/config.yml).

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"time"
"github.com/pkg/browser"
"golang.org/x/oauth2"
@@ -12,6 +13,7 @@ import (
type authCodeFlow struct {
Config *oauth2.Config
AuthCodeOptions []oauth2.AuthCodeOption
ServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
@@ -40,7 +42,7 @@ func (f *authCodeFlow) getAuthCode(ctx context.Context) (string, error) {
server := http.Server{
Addr: fmt.Sprintf("localhost:%d", f.ServerPort),
Handler: &authCodeHandler{
authCodeURL: f.Config.AuthCodeURL(state),
authCodeURL: f.Config.AuthCodeURL(state, f.AuthCodeOptions...),
gotCode: func(code string, gotState string) {
if gotState == state {
codeCh <- code
@@ -61,6 +63,7 @@ func (f *authCodeFlow) getAuthCode(ctx context.Context) (string, error) {
go func() {
log.Printf("Open http://localhost:%d for authorization", f.ServerPort)
if !f.SkipOpenBrowser {
time.Sleep(500 * time.Millisecond)
browser.OpenURL(fmt.Sprintf("http://localhost:%d/", f.ServerPort))
}
}()

View File

@@ -47,6 +47,7 @@ func (c *Config) GetTokenSet(ctx context.Context) (*TokenSet, error) {
ServerPort: c.ServerPort,
SkipOpenBrowser: c.SkipOpenBrowser,
Config: oauth2Config,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
}
token, err := flow.getToken(ctx)
if err != nil {

View File

@@ -13,10 +13,13 @@ import (
)
// Parse parses command line arguments and returns a CLI instance.
func Parse(args []string) (*CLI, error) {
func Parse(osArgs []string, version string) (*CLI, error) {
var cli CLI
parser := flags.NewParser(&cli, flags.HelpFlag)
args, err := parser.Parse()
parser.LongDescription = fmt.Sprintf(`Version %s
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
version)
args, err := parser.ParseArgs(osArgs[1:])
if err != nil {
return nil, err
}
@@ -29,6 +32,7 @@ func Parse(args []string) (*CLI, error) {
// CLI represents an interface of this command.
type CLI struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
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."`
}
@@ -37,30 +41,33 @@ type CLI struct {
func (c *CLI) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s", c.KubeConfig)
return "", fmt.Errorf("Could not expand %s: %s", c.KubeConfig, err)
}
return d, nil
}
// Run performs this command.
func (c *CLI) Run(ctx context.Context) error {
log.Printf("Reading %s", c.KubeConfig)
path, err := c.ExpandKubeConfig()
if err != nil {
return err
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Read(path)
if err != nil {
return fmt.Errorf("Could not load kubeconfig: %s", err)
return fmt.Errorf("Could not read kubeconfig: %s", err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.FindOIDCAuthProvider(authInfo)
log.Printf("Using current-context: %s", cfg.CurrentContext)
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
if err != nil {
return fmt.Errorf("Could not find auth-provider: %s", err)
return fmt.Errorf(`Could not find OIDC configuration in kubeconfig: %s
Did you setup kubectl for OIDC authentication?
kubectl config set-credentials %s \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`,
err, cfg.CurrentContext)
}
tlsConfig, err := c.tlsConfig(authProvider)
if err != nil {
@@ -70,18 +77,19 @@ func (c *CLI) Run(ctx context.Context) error {
Issuer: authProvider.IDPIssuerURL(),
ClientID: authProvider.ClientID(),
ClientSecret: authProvider.ClientSecret(),
ExtraScopes: authProvider.ExtraScopes(),
Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}},
ServerPort: 8000,
ServerPort: c.ListenPort,
SkipOpenBrowser: c.SkipOpenBrowser,
}
token, err := authConfig.GetTokenSet(ctx)
if err != nil {
return fmt.Errorf("Authentication error: %s", err)
return fmt.Errorf("Could not get token from OIDC provider: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
log.Printf("Updated %s", c.KubeConfig)
return nil
}

35
cli/cli_test.go Normal file
View File

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

View File

@@ -13,25 +13,25 @@ import (
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) (*tls.Config, error) {
p := x509.NewCertPool()
if authProvider.IDPCertificateAuthority() != "" {
b, err := ioutil.ReadFile(authProvider.IDPCertificateAuthority())
if ca := authProvider.IDPCertificateAuthority(); ca != "" {
b, err := ioutil.ReadFile(ca)
if err != nil {
return nil, fmt.Errorf("Could not read idp-certificate-authority: %s", err)
return nil, fmt.Errorf("Could not read %s: %s", ca, err)
}
if p.AppendCertsFromPEM(b) != true {
return nil, fmt.Errorf("Could not load CA certificate from idp-certificate-authority: %s", err)
return nil, fmt.Errorf("Could not append CA certificate from %s", ca)
}
log.Printf("Using CA certificate: %s", authProvider.IDPCertificateAuthority())
log.Printf("Using CA certificate: %s", ca)
}
if authProvider.IDPCertificateAuthorityData() != "" {
b, err := base64.StdEncoding.DecodeString(authProvider.IDPCertificateAuthorityData())
if ca := authProvider.IDPCertificateAuthorityData(); ca != "" {
b, err := base64.StdEncoding.DecodeString(ca)
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)
return nil, fmt.Errorf("Could not append CA certificate from idp-certificate-authority-data")
}
log.Printf("Using CA certificate of idp-certificate-authority-data")
log.Printf("Using CA certificate: idp-certificate-authority-data")
}
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}

View File

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

View File

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

View File

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

148
e2e/e2e_test.go Normal file
View File

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

View File

@@ -1,4 +1,4 @@
package integration
package e2e
import (
"html/template"
@@ -9,6 +9,7 @@ import (
type kubeconfigValues struct {
Issuer string
ExtraScopes string
IDPCertificateAuthority string
IDPCertificateAuthorityData string
}

View File

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

View File

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

View File

@@ -2,27 +2,28 @@ package kubeconfig
import (
"fmt"
"strings"
"k8s.io/client-go/tools/clientcmd/api"
)
// FindCurrentAuthInfo returns the authInfo of current context.
// If the current context does not exist, this returns nil.
func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
// FindOIDCAuthProvider returns the current OIDC authProvider.
// If the context, auth-info or auth-provider does not exist, this returns an error.
// If auth-provider is not "oidc", this returns an error.
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil
return nil, fmt.Errorf("context %s does not exist", config.CurrentContext)
}
authInfo := config.AuthInfos[context.AuthInfo]
if authInfo == nil {
return nil, fmt.Errorf("auth-info %s does not exist", context.AuthInfo)
}
return config.AuthInfos[context.AuthInfo]
}
// FindOIDCAuthProvider returns the OIDC authProvider.
func FindOIDCAuthProvider(authInfo *api.AuthInfo) (*OIDCAuthProvider, error) {
if authInfo.AuthProvider == nil {
return nil, fmt.Errorf("auth-provider is not set, did you setup kubectl as listed here: https://github.com/int128/kubelogin")
return nil, fmt.Errorf("auth-provider is not set")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
return nil, fmt.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
}
@@ -55,6 +56,14 @@ func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
return c.Config["idp-certificate-authority-data"]
}
// ExtraScopes returns the extra-scopes.
func (c *OIDCAuthProvider) ExtraScopes() []string {
if c.Config["extra-scopes"] == "" {
return []string{}
}
return strings.Split(c.Config["extra-scopes"], ",")
}
// SetIDToken replaces the id-token.
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
c.Config["id-token"] = idToken

View File

@@ -1,19 +1,13 @@
package kubeconfig
import (
"fmt"
"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) {
config, err := clientcmd.LoadFromFile(path)
if err != nil {
return nil, fmt.Errorf("Could not load kubeconfig from %s: %s", path, err)
}
return config, nil
return clientcmd.LoadFromFile(path)
}
// Write writes the config to the file.

View File

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