Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 committed Aug 10, 2018
1 parent ec675a5 commit 2dc8c7f
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 258 deletions.
41 changes: 7 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,12 @@ Run `kubelogin`.

```
% kubelogin
2018/03/23 18:01:40 Reading config from /home/user/.kube/config
2018/03/23 18:01:40 Using current context: hello.k8s.local
2018/03/23 18:01:40 Using issuer: https://keycloak.example.com/auth/realms/hello
2018/03/23 18:01:40 Using client ID: kubernetes
2018/03/23 18:01:41 Starting OpenID Connect authentication:
## Automatic (recommended)
Open the following URL in the web browser:
http://localhost:8000/
## Manual
If you cannot access to localhost, instead open the following URL:
https://keycloak.example.com/auth/realms/hello/protocol/openid-connect/auth?client_id=kubernetes&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=openid+email&state=********
Enter the code:
```

Open http://localhost:8000 in your browser.
If you cannot access to localhost, you can get the authorization code and enter it manually instead.

Then, `kubelogin` will update your `~/.kube/config` with the ID token and refresh token.

```
2018/03/23 18:01:46 Exchanging code and token...
2018/03/23 18:01:46 Verifying ID token...
2018/03/23 18:01:46 You are logged in as [email protected] (********)
2018/03/23 18:01:46 Updated /home/user/.kube/config
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: devops.hidetake.org
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```

Now your `~/.kube/config` looks like:
Expand Down Expand Up @@ -99,9 +74,7 @@ This tutorial assumes you have created an OIDC client with the following:
- Issuer URL: `https://keycloak.example.com/auth/realms/hello`
- Client ID: `kubernetes`
- Client Secret: `YOUR_CLIENT_SECRET`
- Allowed redirect URLs:
- `http://localhost:8000/`
- `urn:ietf:wg:oauth:2.0:oob`
- Allowed redirect URLs: `http://localhost:8000/`
- Groups claim: `groups` (optional for group based access controll)

### 2. Setup Kubernetes API Server
Expand Down
61 changes: 61 additions & 0 deletions authn/authn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package authn

import (
"context"
"fmt"

oidc "github.com/coreos/go-oidc"
"github.com/int128/kubelogin/authz"
"golang.org/x/oauth2"
)

// TokenSet is a set of tokens and claims.
type TokenSet struct {
IDToken string
RefreshToken string
Claims *Claims
}

// Claims represents properties in the ID token.
type Claims struct {
Email string `json:"email"`
}

// GetTokenSet retrieves a token from the OIDC provider.
func GetTokenSet(ctx context.Context, issuer string, clientID string, clientSecret string) (*TokenSet, error) {
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return nil, fmt.Errorf("Could not access OIDC issuer: %s", err)
}
flow := authz.BrowserAuthCodeFlow{
Port: 8000,
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{oidc.ScopeOpenID, "email"},
},
}
token, err := flow.GetToken(ctx)
if err != nil {
return nil, fmt.Errorf("Could not get a token: %s", err)
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("Could not verify the id_token: %s", err)
}
var claims Claims
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("Could not extract claims from the token response: %s", err)
}
return &TokenSet{
IDToken: rawIDToken,
RefreshToken: token.RefreshToken,
Claims: &claims,
}, nil
}
12 changes: 12 additions & 0 deletions authz/authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package authz

import (
"context"

"golang.org/x/oauth2"
)

// Flow represents an authorization method.
type Flow interface {
GetToken(context.Context) (*oauth2.Token, error)
}
97 changes: 97 additions & 0 deletions authz/browser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package authz

import (
"context"
"fmt"
"log"
"net/http"

"golang.org/x/oauth2"
)

// BrowserAuthCodeFlow is a flow to get a token by browser interaction.
type BrowserAuthCodeFlow struct {
oauth2.Config
Port int // HTTP server port
}

// 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()
if err != nil {
return nil, err
}
log.Printf("Open http://localhost:%d for authorization", f.Port)
code, err := f.getCode(ctx, &f.Config, state)
if err != nil {
return nil, err
}
token, err := f.Config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
}
return token, nil
}

func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config, state string) (string, error) {
codeCh := make(chan string)
errCh := make(chan error)
server := http.Server{
Addr: fmt.Sprintf(":%d", f.Port),
Handler: &handler{
AuthCodeURL: config.AuthCodeURL(state),
Callback: func(code string, actualState string, err error) {
switch {
case err != nil:
errCh <- err
case actualState != state:
errCh <- fmt.Errorf("OAuth state did not match, should be %s but %s", state, actualState)
default:
codeCh <- code
}
},
},
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
server.Shutdown(ctx)
return "", err
case code := <-codeCh:
server.Shutdown(ctx)
return code, nil
}
}

type handler struct {
AuthCodeURL string
Callback func(code string, state string, err error)
}

func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.RequestURI)
switch r.URL.Path {
case "/":
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
errorCode := r.URL.Query().Get("error")
errorDescription := r.URL.Query().Get("error_description")
switch {
case code != "":
s.Callback(code, state, nil)
fmt.Fprintf(w, "Back to command line.")
case errorCode != "":
s.Callback("", "", fmt.Errorf("OAuth Error: %s %s", errorCode, errorDescription))
fmt.Fprintf(w, "Back to command line.")
default:
http.Redirect(w, r, s.AuthCodeURL, 302)
}
default:
http.Error(w, "Not Found", 404)
}
}
35 changes: 35 additions & 0 deletions authz/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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
}
15 changes: 15 additions & 0 deletions authz/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
}
20 changes: 12 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package main

import (
"context"
"log"

"github.com/int128/kubelogin/authn"
"github.com/int128/kubelogin/kubeconfig"
)

Expand All @@ -16,22 +18,24 @@ func main() {
if err != nil {
log.Fatalf("Could not load kubeconfig: %s", err)
}
log.Printf("Using current-context: %s", cfg.CurrentContext)
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
log.Fatalf("Could not find the current-context: %s", cfg.CurrentContext)
log.Fatalf("Could not find current context: %s", cfg.CurrentContext)
}
provider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
if err != nil {
log.Fatalf("Could not find the OIDC auth-provider: %s", err)
log.Fatalf("Could not find auth-provider: %s", err)
}
token, err := GetOIDCToken(provider.IDPIssuerURL(), provider.ClientID(), provider.ClientSecret())

ctx := context.Background()
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
log.Fatalf("OIDC authentication error: %s", err)
log.Fatalf("Authentication error: %s", err)
}

provider.SetIDToken(token.IDToken)
provider.SetRefreshToken(token.RefreshToken)
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
}
Loading

0 comments on commit 2dc8c7f

Please sign in to comment.