Compare commits

..

1 Commits
1.1 ... 1.0

Author SHA1 Message Date
Hidetake Iwata
f8fbd81be1 Add windows 32bit binary 2018-03-23 20:04:07 +09:00
16 changed files with 348 additions and 416 deletions

View File

@@ -6,7 +6,7 @@ jobs:
working_directory: /go/src/github.com/int128/kubelogin
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get -v -t -d
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -v
@@ -17,7 +17,7 @@ jobs:
working_directory: /go/src/github.com/int128/kubelogin
steps:
- checkout
- run: go get -v -t -d ./...
- 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}}'

View File

@@ -8,4 +8,3 @@ indent_size = 2
[*.go]
indent_style = tab
indent_size = 4

1
.gitignore vendored
View File

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

114
README.md
View File

@@ -7,33 +7,44 @@
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it as `/usr/local/bin/kubelogin`.
You have to configure `kubectl` to authenticate with OIDC.
See the later section for details.
```sh
kubectl config set-credentials CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
Run `kubelogin`.
Run the command.
```
% kubelogin
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
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:
```
Now your `~/.kube/config` looks like:
And then open http://localhost:8000.
If you cannot access to localhost, you can choose manual interaction instead.
`kubelogin` will update your `~/.kube/config` with the ID token and refresh token, as follows:
```yaml
# ~/.kube/config (snip)
current-context: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
users:
- name: hello.k8s.local
user:
@@ -42,12 +53,12 @@ users:
idp-issuer-url: https://keycloak.example.com/auth/realms/hello
client-id: kubernetes
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
id-token: ey... # kubelogin will update ID token
refresh-token: ey... # kubelogin will update refresh token
name: oidc
```
Make sure you can access to the Kubernetes cluster:
Make sure you can access to the cluster:
```
% kubectl version
@@ -56,13 +67,6 @@ Server Version: version.Info{...}
```
## Configuration
You can set the following environment variable:
- `KUBECONFIG` - Path to the config. Defaults to `~/.kube/config`.
## Prerequisite
You have to setup your OIDC identity provider and Kubernetes cluster.
@@ -74,14 +78,16 @@ 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/`
- Allowed redirect URLs:
- `http://localhost:8000/`
- `urn:ietf:wg:oauth:2.0:oob`
- Groups claim: `groups` (optional for group based access controll)
### 2. Setup Kubernetes API Server
Configure the Kubernetes API server allows your identity provider.
If you are using [kops](https://github.com/kubernetes/kops), `kops edit cluster` and append the following settings:
If you are using kops, `kops edit cluster` and append the following settings:
```yaml
spec:
@@ -93,61 +99,31 @@ spec:
### 3. Setup kubectl
Run the following command to configure `kubectl` to authenticate by your identity provider.
Run the following script to configure `kubectl` uses your identity provider:
```sh
kubectl config set-credentials CLUSTER_NAME \
CLUSTER_NAME=hello.k8s.local
kubectl config set-credentials $CLUSTER_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
In actual team operation, you can share the following config to your team members for easy setup.
```yaml
#!/bin/sh
CLUSTER_NAME="hello.k8s.local"
# Set the certificate
mkdir -p "$HOME/.kube"
cat > "$HOME/.kube/$CLUSTER_NAME.crt" <<EOF
-----BEGIN CERTIFICATE-----
MII...
-----END CERTIFICATE-----
EOF
# Set the cluster
kubectl config set-cluster "$CLUSTER_NAME" \
--server https://api-xxx.xxx.elb.amazonaws.com \
--certificate-authority "$HOME/.kube/$CLUSTER_NAME.crt"
# Set the credentials
kubectl config set-credentials "$CLUSTER_NAME" \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_SECRET
# Set the context
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
# Set the current context
kubectl config use-context "$CLUSTER_NAME"
kubectl config set-context $CLUSTER_NAME --cluster $CLUSTER_NAME --user $CLUSTER_NAME
```
In actual team operation, you can distribute the script to your team members for easy setup.
## Contributions
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests.
### Build
### How to build
```sh
go get github.com/int128/kubelogin
```
### Release
CircleCI publishes the build to GitHub. See [.circleci/config.yml](.circleci/config.yml).

View File

@@ -1,61 +0,0 @@
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
}

View File

@@ -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)
}

View File

@@ -1,97 +0,0 @@
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)
}
}

View File

@@ -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
}

View File

@@ -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
}

46
config.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"os"
"github.com/mitchellh/go-homedir"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
// GetCurrentAuthInfo returns the current authInfo
func GetCurrentAuthInfo(config api.Config) *api.AuthInfo {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil
}
authInfo := config.AuthInfos[context.AuthInfo]
return authInfo
}
// ReadKubeConfig returns the current config
func ReadKubeConfig(path string) (*api.Config, error) {
config, err := clientcmd.LoadFromFile(path)
if err != nil {
return nil, err
}
return config, nil
}
// WriteKubeConfig writes the config
func WriteKubeConfig(config api.Config, path string) error {
return clientcmd.WriteToFile(config, path)
}
// FindKubeConfig returns env:KUBECONFIG or ~/.kube/config
func FindKubeConfig() (string, error) {
env := os.Getenv("KUBECONFIG")
if env != "" {
return env, nil
}
path, err := homedir.Expand("~/.kube/config")
if err != nil {
return "", err
}
return path, nil
}

View File

@@ -1,52 +0,0 @@
package kubeconfig
import (
"fmt"
"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 {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil
}
return config.AuthInfos[context.AuthInfo]
}
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")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
}
type OIDCAuthProviderConfig api.AuthProviderConfig
// IDPIssuerURL returns the idp-issuer-url.
func (c *OIDCAuthProviderConfig) IDPIssuerURL() string {
return c.Config["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c *OIDCAuthProviderConfig) ClientID() string {
return c.Config["client-id"]
}
// ClientSecret returns the client-secret.
func (c *OIDCAuthProviderConfig) ClientSecret() string {
return c.Config["client-secret"]
}
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
c.Config["id-token"] = idToken
}
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
c.Config["refresh-token"] = refreshToken
}

View File

@@ -1,48 +0,0 @@
package kubeconfig
import (
"fmt"
"os"
homedir "github.com/mitchellh/go-homedir"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
const userKubeConfig = "~/.kube/config"
// Find returns path to the kubeconfig file,
// that is given by env:KUBECONFIG or ~/.kube/config.
// This returns an error if it is not found or I/O error occurred.
func Find() (string, error) {
path := os.Getenv("KUBECONFIG")
if path == "" {
var err error
path, err = homedir.Expand(userKubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s: %s", userKubeConfig, err)
}
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("Could not stat %s: %s", userKubeConfig, err)
}
if info.IsDir() {
return "", fmt.Errorf("%s should be a file", userKubeConfig)
}
return path, nil
}
// Load loads the file and returns the Config.
func Load(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
}
// Write writes the config to the file.
func Write(config *api.Config, path string) error {
return clientcmd.WriteToFile(*config, path)
}

62
main.go
View File

@@ -1,41 +1,57 @@
package main
import (
"context"
"log"
"github.com/int128/kubelogin/authn"
"github.com/int128/kubelogin/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
func main() {
path, err := kubeconfig.Find()
kubeConfigPath, err := FindKubeConfig()
if err != nil {
log.Fatalf("Could not find kubeconfig: %s", err)
log.Fatal(err)
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Load(path)
log.Printf("Reading config from %s", kubeConfigPath)
kubeConfig, err := ReadKubeConfig(kubeConfigPath)
if err != nil {
log.Fatalf("Could not load kubeconfig: %s", err)
log.Fatal(err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
log.Printf("Using current context: %s", kubeConfig.CurrentContext)
authInfo := GetCurrentAuthInfo(*kubeConfig)
if authInfo == nil {
log.Fatalf("Could not find current context: %s", cfg.CurrentContext)
log.Fatal("Could not find the current user")
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
if err != nil {
log.Fatalf("Could not find auth-provider: %s", err)
authProvider := authInfo.AuthProvider
if authProvider == nil {
log.Fatal("auth-provider is not set in the config")
}
ctx := context.Background()
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
log.Fatalf("Authentication error: %s", err)
}
switch authProvider.Name {
case "oidc":
if err := mutateConfigWithOIDC(authProvider); err != nil {
log.Fatal(err)
}
WriteKubeConfig(*kubeConfig, kubeConfigPath)
log.Printf("Updated %s", kubeConfigPath)
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
default:
log.Fatalf("Currently auth-provider `%s` is not supported", authProvider.Name)
}
}
func mutateConfigWithOIDC(authProvider *api.AuthProviderConfig) error {
issuer := authProvider.Config["idp-issuer-url"]
clientID := authProvider.Config["client-id"]
clientSecret := authProvider.Config["client-secret"]
log.Printf("Using issuer: %s", issuer)
log.Printf("Using client ID: %s", clientID)
oidcToken, err := GetOIDCToken(issuer, clientID, clientSecret)
if err != nil {
return err
}
authProvider.Config["id-token"] = oidcToken.IDToken
authProvider.Config["refresh-token"] = oidcToken.RefreshToken
return nil
}

130
oidc.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
// OIDCToken is a token set
type OIDCToken struct {
IDToken string
RefreshToken string
}
// GetOIDCToken returns a token retrieved by auth code grant
func GetOIDCToken(issuer string, clientID string, clientSecret string) (*OIDCToken, error) {
port := 8000
provider, err := oidc.NewProvider(oauth2.NoContext, issuer)
if err != nil {
return nil, err
}
state, err := generateState()
if err != nil {
return nil, err
}
webBrowserConfig := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: fmt.Sprintf("http://localhost:%d/", port),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "email"},
}
cliConfig := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "email"},
}
showInstructionToGetToken(webBrowserConfig.RedirectURL, cliConfig.AuthCodeURL(state))
token, err := getTokenByWebBrowserOrCLI(webBrowserConfig, cliConfig, state)
if err != nil {
return nil, err
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
}
log.Printf("Verifying ID token...")
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
idToken, err := verifier.Verify(oauth2.NoContext, rawIDToken)
if err != nil {
return nil, err
}
idTokenClaim := struct {
Email string `json:"email"`
}{}
if err := idToken.Claims(&idTokenClaim); err != nil {
return nil, err
}
log.Printf("You are logged in as %s (%s)", idTokenClaim.Email, idToken.Subject)
return &OIDCToken{
IDToken: rawIDToken,
RefreshToken: token.RefreshToken,
}, nil
}
func getTokenByWebBrowserOrCLI(webBrowserConfig oauth2.Config, cliConfig oauth2.Config, state string) (*oauth2.Token, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
webBrowserAuthCodeCh := make(chan string)
cliAuthCodeCh := make(chan string)
errCh := make(chan error)
go ReceiveAuthCodeFromWebBrowser(ctx, webBrowserConfig.AuthCodeURL(state), state, webBrowserAuthCodeCh, errCh)
go ReceiveAuthCodeFromCLI(ctx, cliAuthCodeCh, errCh)
select {
case err := <-errCh:
return nil, err
case authCode := <-webBrowserAuthCodeCh:
log.Printf("Exchanging code and token...")
return webBrowserConfig.Exchange(ctx, authCode)
case authCode := <-cliAuthCodeCh:
log.Printf("Exchanging code and token...")
return cliConfig.Exchange(ctx, authCode)
}
}
func showInstructionToGetToken(localhostURL string, cliAuthCodeURL string) {
log.Printf("Starting OpenID Connect authentication:")
fmt.Printf(`
## Automatic (recommended)
Open the following URL in the web browser:
%s
## Manual
If you cannot access to localhost, instead open the following URL:
%s
Enter the code: `, localhostURL, cliAuthCodeURL)
}
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
}

16
oidc_cli.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import (
"context"
"fmt"
)
// ReceiveAuthCodeFromCLI receives an auth code from CLI
func ReceiveAuthCodeFromCLI(ctx context.Context, authCodeCh chan<- string, errCh chan<- error) {
var authCode string
if _, err := fmt.Scanln(&authCode); err != nil {
errCh <- err
} else {
authCodeCh <- authCode
}
}

70
oidc_http.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
)
// ReceiveAuthCodeFromWebBrowser starts a server and receives an auth code
func ReceiveAuthCodeFromWebBrowser(ctx context.Context, authCodeURL string, state string, authCodeCh chan<- string, errCh chan<- error) {
server := http.Server{
Addr: ":8000",
Handler: &AuthCodeGrantHandler{
AuthCodeURL: authCodeURL,
State: state,
Resolve: func(authCode string) { authCodeCh <- authCode },
Reject: func(err error) { errCh <- err },
},
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case <-ctx.Done():
server.Shutdown(context.Background())
}
}
// AuthCodeGrantHandler handles requests for OIDC auth code grant
type AuthCodeGrantHandler struct {
AuthCodeURL string
State string
Resolve func(string)
Reject func(error)
}
func (s *AuthCodeGrantHandler) 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 != "" && state == s.State:
s.Resolve(code)
fmt.Fprintf(w, "Please back to the command line.")
case code != "" && state != s.State:
s.Reject(fmt.Errorf("OIDC state did not match. expected=%s, actual=%s", s.State, state))
fmt.Fprintf(w, "Please back to the command line.")
case errorCode != "":
s.Reject(fmt.Errorf("OIDC error: %s %s", errorCode, errorDescription))
fmt.Fprintf(w, "Please back to the command line.")
default:
http.Redirect(w, r, s.AuthCodeURL, 302)
}
default:
http.Error(w, "Not Found", 404)
}
}