-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
239 additions
and
258 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.