Compare commits

...

18 Commits

Author SHA1 Message Date
Hidetake Iwata
79d8056c35 Check nonce of ID token on authentication (#112) 2019-07-10 09:59:03 +09:00
Hidetake Iwata
4138991339 Refactor: split fat handler to handler/service (#111) 2019-07-09 19:06:03 +09:00
Hidetake Iwata
f650827a5f Show caller filename and line on debug log (#110) 2019-07-05 10:31:27 +09:00
Hidetake Iwata
56904e15b1 Refactor (#109)
* Refactor: rename to kubeconfig.AuthProvider

* Refactor: add comments
2019-07-05 10:22:28 +09:00
Hidetake Iwata
dd05c11359 Create DESIGN.md 2019-07-04 20:45:27 +09:00
Hidetake Iwata
ce61e09acf Refresh the ID token if it has expired (#108) 2019-07-04 20:15:51 +09:00
Hidetake Iwata
e4057db5b5 Add authentication sequence diagram 2019-06-28 10:09:08 +09:00
Hidetake Iwata
bb288b69d3 Update README.md 2019-06-26 17:39:04 +09:00
Hidetake Iwata
e220267de5 Update README.md 2019-06-26 13:57:20 +09:00
Hidetake Iwata
391754e1ce Cache oidc.Provider to reduce discovery requests (#107) 2019-06-26 10:16:10 +09:00
Hidetake Iwata
10c7b6a84f Refactor: extract tls package and add tests (#106) 2019-06-24 22:50:26 +09:00
dependabot-preview[bot]
2176105a91 Bump github.com/google/wire from 0.2.2 to 0.3.0 (#101)
* Bump github.com/google/wire from 0.2.2 to 0.3.0

Bumps [github.com/google/wire](https://github.com/google/wire) from 0.2.2 to 0.3.0.
- [Release notes](https://github.com/google/wire/releases)
- [Commits](https://github.com/google/wire/compare/v0.2.2...v0.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* go mod tidy

* Move to google/wire@v0.3.0
2019-06-21 15:40:49 +09:00
Hidetake Iwata
3c79a614ff Refactor: move to xerrors (#103) 2019-06-21 14:22:21 +09:00
Hidetake Iwata
f6dec8e3db Add note of password grant (refs #102) 2019-06-21 10:19:31 +09:00
dependabot-preview[bot]
1ea9027677 Bump github.com/int128/oauth2cli from 1.4.0 to 1.4.1 (#100)
Bumps [github.com/int128/oauth2cli](https://github.com/int128/oauth2cli) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/int128/oauth2cli/releases)
- [Commits](https://github.com/int128/oauth2cli/compare/v1.4.0...v1.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-06-18 09:11:02 +09:00
Hidetake Iwata
cfa38455ab Refactor (#99)
* Refactor: rename files

* Refactor: rename to e2e_test

* Refactor: export wire.Set in each components
2019-06-17 09:29:55 +09:00
dependabot-preview[bot]
0412f6a1b0 Bump github.com/spf13/cobra from 0.0.4 to 0.0.5 (#98)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 0.0.4 to 0.0.5.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v0.0.4...0.0.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-06-12 16:04:13 +09:00
Hidetake Iwata
afc7974b79 Update README.md 2019-06-06 09:36:16 +09:00
59 changed files with 2529 additions and 1371 deletions

51
DESIGN.md Normal file
View File

@@ -0,0 +1,51 @@
# Design of kubelogin
This explains design of kubelogin.
## Use cases
Kubelogin is a command line tool and designed to run as both a standalone command and a kubectl plugin.
It respects the following flags, commonly used in kubectl:
```
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-v, --v int If set to 1 or greater, it shows debug log
```
As well as it respects the environment variable `KUBECONFIG`.
### Login by the command
TODO
### Wrap kubectl and login transparently
TODO
## Architecture
Kubelogin consists of the following layers:
- `usecases`: This provides the use-cases.
- `adaptor`: This provides external access and converts objects between the use-cases and external system.
### Use-cases
This provides the use-cases mentioned in the previous section.
This layer should not contain external access such as HTTP requests and system calls.
### Adaptor
This provides external access such as command line interface and HTTP requests.

View File

@@ -3,13 +3,13 @@ TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
.PHONY: check run release clean
.PHONY: check run diagram release clean
all: $(TARGET)
check:
golangci-lint run
$(MAKE) -C adaptors_test/keys/testdata
$(MAKE) -C e2e_test/keys/testdata
go test -v -race -cover -coverprofile=coverage.out ./...
$(TARGET): $(wildcard *.go)
@@ -21,6 +21,11 @@ $(TARGET_PLUGIN): $(TARGET)
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
diagram: docs/authn.png
%.png: %.seqdiag
seqdiag -a -f /Library/Fonts/Verdana.ttf $<
dist:
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
mv dist/gh/kubelogin.rb dist/

View File

@@ -1,7 +1,9 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
It gets a token from the OIDC provider and writes it to the kubeconfig.
In Kubernetes OIDC authentication, kubectl does not provide actual authentication and we need to manually set an ID token and refresh token to the kubeconfig.
Kubelogin provides browser based authentication and writes an ID token and refresh token to the kubeconfig.
## Getting Started
@@ -17,28 +19,22 @@ brew install kubelogin
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.11.0/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.12.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
You need to setup the following components:
- OIDC provider
- Kubernetes API server
- kubectl authentication
- Role binding
For more, see the following documents:
You need to configure the OIDC provider, Kubernetes API server, kubectl authentication and role binding.
See the following documents for more:
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
### Login manually
### Login by the command
Just run:
Just run the command:
```sh
kubelogin
@@ -68,15 +64,18 @@ NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the token is valid, kubelogin does nothing.
If the ID token is valid, kubelogin does nothing.
```
% kubelogin
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
```
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
If the refresh token has expired, kubelogin will proceed the authentication.
### Login transparently
### Wrap kubectl and login transparently
You can wrap kubectl to transparently login to the provider.
@@ -97,7 +96,7 @@ NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the token is valid, it just executes kubectl.
If the ID token is valid, it just executes kubectl.
```
% kubectl get pods
@@ -105,7 +104,10 @@ NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
It respects kubectl options passed to the extra arguments.
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
If the refresh token has expired, kubelogin will proceed the authentication.
Kubelogin respects kubectl options passed to the extra arguments.
For example, if you run `kubectl --kubeconfig .kubeconfig`,
it will update `.kubeconfig` and execute kubectl.
@@ -191,7 +193,7 @@ If you set multiple files, kubelogin will find the file which has the current au
Kubelogin performs the authorization code flow by default.
It starts the local server at port 8000 or 18000 by default.
You need to register the following redirect URIs to the OIDC provider:
You need to register the following redirect URIs to the provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
@@ -205,18 +207,9 @@ kubelogin --listen-port 12345 --listen-port 23456
#### Resource owner password credentials grant flow
You can use the resource owner password credentials grant flow.
Note that not all providers support this flow as:
> The resource owner password credentials grant type is suitable in
> cases where the resource owner has a trust relationship with the
> client, such as the device operating system or a highly privileged
> application.
> The authorization server should take special care when
> enabling this grant type and only allow it when other flows are not
> viable.
>
> <https://tools.ietf.org/html/rfc6749#section-4.3>
As well as you can use the resource owner password credentials grant flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
Most OIDC providers do not support this flow.
You can pass the username and password:
@@ -224,7 +217,7 @@ You can pass the username and password:
% kubelogin --username USER --password PASS
```
or ask the password:
or use the password prompt:
```
% kubelogin --username USER
@@ -234,13 +227,13 @@ Password:
### Extra scopes
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
Note that kubectl does not accept multiple scopes and you need to edit the kubeconfig as like:
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
@@ -250,7 +243,7 @@ sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
### CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by kubeconfig or option.
You can use your self-signed certificates for the provider.
```sh
kubectl config set-credentials keycloak \
@@ -267,5 +260,17 @@ See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyF
## Contributions
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests for improving code and documents.
### Development
Go 1.12 or later is required.
```sh
# Run lint and tests
make check
# Compile and run the command
make
./kubelogin
```

View File

@@ -5,12 +5,19 @@ import (
"fmt"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// Set provides an implementation and interface for Cmd.
var Set = wire.NewSet(
wire.Struct(new(Cmd), "*"),
wire.Bind(new(adaptors.Cmd), new(*Cmd)),
)
const examples = ` # Login to the provider using authorization code grant.
@@ -24,12 +31,15 @@ const examples = ` # Login to the provider using authorization code grant.
var defaultListenPort = []int{8000, 18000}
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Login usecases.Login
LoginAndExec usecases.LoginAndExec
Logger adaptors.Logger
}
// Run parses the command line arguments and executes the specified use-case.
// It returns an exit code, that is 0 on success or 1 on error.
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var exitCode int
executable := filepath.Base(args[0])
@@ -70,10 +80,10 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
Short: "Login transparently and execute the kubectl command",
Args: func(execCmd *cobra.Command, args []string) error {
if execCmd.ArgsLenAtDash() == -1 {
return errors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
return xerrors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
}
if len(args) < 1 {
return errors.New("too few arguments")
return xerrors.New("too few arguments")
}
return nil
},
@@ -133,6 +143,7 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
return exitCode
}
// kubectlOptions represents kubectl specific options.
type kubectlOptions struct {
Kubeconfig string
Context string
@@ -152,6 +163,7 @@ func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
}
// kubeloginOptions represents application specific options.
type kubeloginOptions struct {
ListenPort []int
SkipOpenBrowser bool

20
adaptors/env/env.go vendored
View File

@@ -7,8 +7,16 @@ import (
"os/exec"
"syscall"
"github.com/pkg/errors"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
)
// Set provides an implementation and interface for Env.
var Set = wire.NewSet(
wire.Struct(new(Env), "*"),
wire.Bind(new(adaptors.Env), new(*Env)),
)
// Env provides environment specific facilities.
@@ -17,18 +25,20 @@ type Env struct{}
// ReadPassword reads a password from the stdin without echo back.
func (*Env) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, "Password: "); err != nil {
return "", errors.Wrapf(err, "could not write the prompt")
return "", xerrors.Errorf("could not write the prompt: %w", err)
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", errors.Wrapf(err, "could not read")
return "", xerrors.Errorf("could not read: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", errors.Wrapf(err, "could not write a new line")
return "", xerrors.Errorf("could not write a new line: %w", err)
}
return string(b), nil
}
// Exec executes the command and returns the exit code.
// Unlike the exec package, this does not return an error even if the command exited with non-zero code.
func (*Env) Exec(ctx context.Context, executable string, args []string) (int, error) {
c := exec.CommandContext(ctx, executable, args...)
c.Stdin = os.Stdin
@@ -38,7 +48,7 @@ func (*Env) Exec(ctx context.Context, executable string, args []string) (int, er
if err, ok := err.(*exec.ExitError); ok {
return err.ExitCode(), nil
}
return 0, errors.Wrapf(err, "could not execute the command")
return 0, xerrors.Errorf("could not execute the command: %w", err)
}
return 0, nil
}

View File

@@ -2,8 +2,8 @@ package adaptors
import (
"context"
"time"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/models/kubeconfig"
)
@@ -14,14 +14,15 @@ type Cmd interface {
}
type Kubeconfig interface {
GetCurrentAuth(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error)
UpdateAuth(auth *kubeconfig.Auth) error
GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error)
UpdateAuthProvider(auth *kubeconfig.AuthProvider) error
}
type OIDC interface {
New(config OIDCClientConfig) (OIDCClient, error)
New(ctx context.Context, config OIDCClientConfig) (OIDCClient, error)
}
// OIDCClientConfig represents a configuration of an OIDCClient to create.
type OIDCClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
@@ -31,30 +32,47 @@ type OIDCClientConfig struct {
type OIDCClient interface {
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*oidc.IDToken, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*OIDCVerifyOut, error)
Refresh(ctx context.Context, in OIDCRefreshIn) (*OIDCAuthenticateOut, error)
}
// OIDCAuthenticateByCodeIn represents an input DTO of OIDCClient.AuthenticateByCode.
type OIDCAuthenticateByCodeIn struct {
Config kubeconfig.OIDCConfig
LocalServerPort []int // HTTP server port candidates
SkipOpenBrowser bool // skip opening browser if true
ShowLocalServerURL interface{ ShowLocalServerURL(url string) }
}
// OIDCAuthenticateByPasswordIn represents an input DTO of OIDCClient.AuthenticateByPassword.
type OIDCAuthenticateByPasswordIn struct {
Config kubeconfig.OIDCConfig
Username string
Password string
}
// OIDCAuthenticateOut represents an output DTO of
// OIDCClient.AuthenticateByCode, OIDCClient.AuthenticateByPassword and OIDCClient.Refresh.
type OIDCAuthenticateOut struct {
VerifiedIDToken *oidc.IDToken
IDToken string
RefreshToken string
IDToken string
RefreshToken string
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
// OIDCVerifyIn represents an input DTO of OIDCClient.Verify.
type OIDCVerifyIn struct {
Config kubeconfig.OIDCConfig
IDToken string
RefreshToken string
}
// OIDCVerifyIn represents an output DTO of OIDCClient.Verify.
type OIDCVerifyOut struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
// OIDCRefreshIn represents an input DTO of OIDCClient.Refresh.
type OIDCRefreshIn struct {
RefreshToken string
}
type Env interface {

View File

@@ -1,3 +1,14 @@
package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
)
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Kubeconfig), "*"),
wire.Bind(new(adaptors.Kubeconfig), new(*Kubeconfig)),
)
type Kubeconfig struct{}

View File

@@ -4,19 +4,19 @@ import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/pkg/errors"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func (*Kubeconfig) GetCurrentAuth(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error) {
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, errors.WithStack(err)
return nil, xerrors.Errorf("could not load kubeconfig: %w", err)
}
auth, err := findCurrentAuth(config, contextName, userName)
auth, err := findCurrentAuthProvider(config, contextName, userName)
if err != nil {
return nil, errors.Wrapf(err, "could not find the current auth provider")
return nil, xerrors.Errorf("could not find the current auth provider: %w", err)
}
return auth, nil
}
@@ -26,40 +26,40 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, errors.Wrapf(err, "could not load the kubeconfig")
return nil, xerrors.Errorf("error while loading config: %w", err)
}
return config, err
}
// findCurrentAuth resolves the current auth provider.
// findCurrentAuthProvider resolves the current auth provider.
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func findCurrentAuth(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error) {
func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
if userName == "" {
if contextName == "" {
contextName = kubeconfig.ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, errors.Errorf("context %s does not exist", contextName)
return nil, xerrors.Errorf("context %s does not exist", contextName)
}
userName = kubeconfig.UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
return nil, errors.Errorf("user %s does not exist", userName)
return nil, xerrors.Errorf("user %s does not exist", userName)
}
if userNode.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is missing")
return nil, xerrors.New("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
return nil, xerrors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
}
if userNode.AuthProvider.Config == nil {
return nil, errors.Errorf("auth-provider.config is missing")
return nil, xerrors.New("auth-provider.config is missing")
}
return &kubeconfig.Auth{
return &kubeconfig.AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,

View File

@@ -75,9 +75,9 @@ func unsetenv(t *testing.T, key string) {
}
}
func Test_findCurrentAuth(t *testing.T) {
func Test_findCurrentAuthProvider(t *testing.T) {
t.Run("CurrentContext", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
auth, err := findCurrentAuthProvider(&api.Config{
CurrentContext: "theContext",
Contexts: map[string]*api.Context{
"theContext": {
@@ -106,7 +106,7 @@ func Test_findCurrentAuth(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
want := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
@@ -127,7 +127,7 @@ func Test_findCurrentAuth(t *testing.T) {
})
t.Run("ByContextName", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
auth, err := findCurrentAuthProvider(&api.Config{
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
@@ -148,7 +148,7 @@ func Test_findCurrentAuth(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
want := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
@@ -162,7 +162,7 @@ func Test_findCurrentAuth(t *testing.T) {
})
t.Run("ByUserName", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
auth, err := findCurrentAuthProvider(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
@@ -178,7 +178,7 @@ func Test_findCurrentAuth(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
want := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
@@ -191,7 +191,7 @@ func Test_findCurrentAuth(t *testing.T) {
})
t.Run("NoConfig", func(t *testing.T) {
_, err := findCurrentAuth(&api.Config{
_, err := findCurrentAuthProvider(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
@@ -207,7 +207,7 @@ func Test_findCurrentAuth(t *testing.T) {
})
t.Run("NotOIDC", func(t *testing.T) {
_, err := findCurrentAuth(&api.Config{
_, err := findCurrentAuthProvider(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",

View File

@@ -4,28 +4,28 @@ import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/pkg/errors"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuth(auth *kubeconfig.Auth) error {
func (*Kubeconfig) UpdateAuthProvider(auth *kubeconfig.AuthProvider) error {
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
if err != nil {
return errors.Wrapf(err, "could not load %s", auth.LocationOfOrigin)
return xerrors.Errorf("could not load %s: %w", auth.LocationOfOrigin, err)
}
userNode, ok := config.AuthInfos[string(auth.UserName)]
if !ok {
return errors.Errorf("user %s does not exist", auth.UserName)
return xerrors.Errorf("user %s does not exist", auth.UserName)
}
if userNode.AuthProvider == nil {
return errors.Errorf("auth-provider is missing")
return xerrors.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return errors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
return xerrors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
}
copyOIDCConfig(auth.OIDCConfig, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, auth.LocationOfOrigin); err != nil {
return errors.Wrapf(err, "could not update %s", auth.LocationOfOrigin)
return xerrors.Errorf("could not update %s: %w", auth.LocationOfOrigin, err)
}
return nil
}

View File

@@ -18,7 +18,7 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuth(&kubeconfig.Auth{
if err := k.UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
@@ -66,7 +66,7 @@ users:
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuth(&kubeconfig.Auth{
if err := k.UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{

View File

@@ -1,42 +1,49 @@
package logger
import (
"fmt"
"log"
"os"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
)
// Set provides an implementation and interface for Logger.
var Set = wire.NewSet(
New,
)
// New returns a Logger with the standard log.Logger for messages and debug.
func New() adaptors.Logger {
return &Logger{
stdLogger: log.New(os.Stderr, "", 0),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds|log.Lshortfile),
}
}
// FromStdLogger returns a Logger with the given standard log.Logger.
func FromStdLogger(l stdLogger) *Logger {
return &Logger{
stdLogger: l,
debugLogger: l,
}
func NewWith(s stdLogger, d debugLogger) *Logger {
return &Logger{s, d, 0}
}
type stdLogger interface {
Printf(format string, v ...interface{})
}
type debugLogger interface {
Output(calldepth int, s string) error
}
// Logger wraps the standard log.Logger and just provides debug level.
type Logger struct {
stdLogger
debugLogger stdLogger
level adaptors.LogLevel
debugLogger
level adaptors.LogLevel
}
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
if l.IsEnabled(level) {
l.debugLogger.Printf(format, v...)
_ = l.debugLogger.Output(2, fmt.Sprintf(format, v...))
}
}

View File

@@ -7,12 +7,13 @@ import (
"github.com/int128/kubelogin/adaptors"
)
type mockStdLogger struct {
type mockDebugLogger struct {
count int
}
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
func (l *mockDebugLogger) Output(int, string) error {
l.count++
return nil
}
func TestLogger_Debugf(t *testing.T) {
@@ -33,7 +34,7 @@ func TestLogger_Debugf(t *testing.T) {
{2, 3, 0},
} {
t.Run(fmt.Sprintf("%+v", c), func(t *testing.T) {
m := &mockStdLogger{}
m := &mockDebugLogger{}
l := &Logger{debugLogger: m, level: c.loggerLevel}
l.Debugf(c.debugfLevel, "hello")
if m.count != c.count {
@@ -43,6 +44,14 @@ func TestLogger_Debugf(t *testing.T) {
}
}
type mockStdLogger struct {
count int
}
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
l.count++
}
func TestLogger_Printf(t *testing.T) {
m := &mockStdLogger{}
l := &Logger{stdLogger: m}

View File

@@ -6,7 +6,6 @@ package mock_adaptors
import (
context "context"
go_oidc "github.com/coreos/go-oidc"
gomock "github.com/golang/mock/gomock"
adaptors "github.com/int128/kubelogin/adaptors"
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
@@ -36,29 +35,29 @@ func (m *MockKubeconfig) EXPECT() *MockKubeconfigMockRecorder {
return m.recorder
}
// GetCurrentAuth mocks base method
func (m *MockKubeconfig) GetCurrentAuth(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.Auth, error) {
ret := m.ctrl.Call(m, "GetCurrentAuth", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.Auth)
// GetCurrentAuthProvider mocks base method
func (m *MockKubeconfig) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCurrentAuth indicates an expected call of GetCurrentAuth
func (mr *MockKubeconfigMockRecorder) GetCurrentAuth(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuth", reflect.TypeOf((*MockKubeconfig)(nil).GetCurrentAuth), arg0, arg1, arg2)
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
func (mr *MockKubeconfigMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuth mocks base method
func (m *MockKubeconfig) UpdateAuth(arg0 *kubeconfig.Auth) error {
ret := m.ctrl.Call(m, "UpdateAuth", arg0)
// UpdateAuthProvider mocks base method
func (m *MockKubeconfig) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuth indicates an expected call of UpdateAuth
func (mr *MockKubeconfigMockRecorder) UpdateAuth(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuth", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuth), arg0)
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
func (mr *MockKubeconfigMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuthProvider), arg0)
}
// MockOIDC is a mock of OIDC interface
@@ -85,16 +84,16 @@ func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
}
// New mocks base method
func (m *MockOIDC) New(arg0 adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
ret := m.ctrl.Call(m, "New", arg0)
func (m *MockOIDC) New(arg0 context.Context, arg1 adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(adaptors.OIDCClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockOIDCMockRecorder) New(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOIDC)(nil).New), arg0)
func (mr *MockOIDCMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOIDC)(nil).New), arg0, arg1)
}
// MockOIDCClient is a mock of OIDCClient interface
@@ -146,10 +145,23 @@ func (mr *MockOIDCClientMockRecorder) AuthenticateByPassword(arg0, arg1 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByPassword), arg0, arg1)
}
// Refresh mocks base method
func (m *MockOIDCClient) Refresh(arg0 context.Context, arg1 adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockOIDCClientMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockOIDCClient)(nil).Refresh), arg0, arg1)
}
// Verify mocks base method
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*go_oidc.IDToken, error) {
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
ret0, _ := ret[0].(*go_oidc.IDToken)
ret0, _ := ret[0].(*adaptors.OIDCVerifyOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}

View File

@@ -2,117 +2,227 @@ package oidc
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/int128/kubelogin/adaptors/oidc/tls"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(adaptors.OIDC), new(*Factory)),
)
type Factory struct {
Logger adaptors.Logger
}
func (f *Factory) New(config adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
hc, err := newHTTPClient(config, f.Logger)
// New returns an instance of adaptors.OIDCClient with the given configuration.
func (f *Factory) New(ctx context.Context, config adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
tlsConfig, err := tls.NewConfig(config, f.Logger)
if err != nil {
return nil, errors.Wrapf(err, "could not create a HTTP client")
return nil, xerrors.Errorf("could not initialize TLS config: %w", err)
}
return &Client{hc}, nil
baseTransport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.Config.IDPIssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDC issuer: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.Config.ClientID,
ClientSecret: config.Config.ClientSecret,
Scopes: append(config.Config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
}, nil
}
type Client struct {
hc *http.Client
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger adaptors.Logger
}
func (c *Client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
// AuthenticateByCode performs the authorization code flow.
func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
nonce, err := newNonce()
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
return nil, xerrors.Errorf("could not generate a nonce parameter")
}
config := oauth2cli.Config{
OAuth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID,
ClientSecret: in.Config.ClientSecret,
Scopes: append(in.Config.ExtraScopes, oidc.ScopeOpenID),
},
OAuth2Config: c.oauth2Config,
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
ShowLocalServerURL: in.ShowLocalServerURL.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
if verifiedIDToken.Nonce != nonce {
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func (c *Client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
func newNonce() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", xerrors.Errorf("error while reading random: %w", err)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
return fmt.Sprintf("%x", n), nil
}
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, in.Username, in.Password)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID,
ClientSecret: in.Config.ClientSecret,
Scopes: append(in.Config.ExtraScopes, oidc.ScopeOpenID),
}
token, err := config.PasswordCredentialsToken(ctx, in.Username, in.Password)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func (c *Client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*oidc.IDToken, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
// Verify checks client ID and signature of the ID token.
// This does not check the expiration and caller should check it.
func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
verifier := c.provider.Verifier(&oidc.Config{
ClientID: c.oauth2Config.ClientID,
SkipExpiryCheck: true,
})
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, in.Config.IDToken)
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return verifiedIDToken, nil
return &adaptors.OIDCVerifyOut{
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: in.RefreshToken,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return &adaptors.OIDCAuthenticateOut{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
claims := make(map[string]string)
for k, v := range rawClaims {
switch v.(type) {
case float64:
claims[k] = fmt.Sprintf("%f", v.(float64))
default:
claims[k] = fmt.Sprintf("%s", v)
}
}
if err != nil {
return claims, xerrors.Errorf("error while decoding the ID token: %w", err)
}
return claims, nil
}

29
adaptors/oidc/tls/testdata/Makefile vendored Normal file
View File

@@ -0,0 +1,29 @@
.PHONY: clean
all: ca1.crt ca1.crt.base64 ca2.crt ca2.crt.base64 ca3.crt ca3.crt.base64
clean:
rm -v *.key *.csr *.crt *.base64
%.key:
openssl genrsa -out $@ 1024
%.csr: %.key
openssl req \
-new \
-key $< \
-subj "/CN=Hello" \
-days 3650 \
-out $@
openssl req -noout -text -in $@
%.crt: %.csr %.key
openssl x509 -req \
-signkey $*.key \
-in $*.csr \
-days 3650 \
-out $@
openssl x509 -text -in $@
%.crt.base64: %.crt
base64 -i $< -o $@

11
adaptors/oidc/tls/testdata/ca1.crt vendored Normal file
View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAQACCQCDf7Inwu3vkzANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVI
ZWxsbzAeFw0xOTA2MjQxMDQ0NDhaFw0yOTA2MjExMDQ0NDhaMBAxDjAMBgNVBAMM
BUhlbGxvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCitZv5Go16nuHDRa2u
nT5m1Q9tkr668pnhcP0TkyjD+oEB0lUz2SJEZEvOd1XVRRrPMSXrtybo9p0TqSGp
Ig1gORWis/j/IR1sYdFutLKhtp6k1HvUiNosdO/K8K/AbO4QPWTGBAcqg//QkMKd
ccgLY2PYczK/t8+6C7JYEHe5AwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACkPsyme
JFlj75fO54NH5WXZxBtoY7kV3yd5oO88BngE8ittaHuauQkkw/sC5x733SsJlPlF
trah4CDMjq5d/okIbIJFKe7NGLi82f9zJ+o1fjDp97UvZHC0zhUx+RiEu3iZRfYM
31Ht7QG63V5ScV3Zmi1nzfQc4jn8d40kXXcn
-----END CERTIFICATE-----

View File

@@ -0,0 +1 @@
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJsekNDQVFBQ0NRQ0RmN0lud3Uzdmt6QU5CZ2txaGtpRzl3MEJBUVVGQURBUU1RNHdEQVlEVlFRRERBVkkKWld4c2J6QWVGdzB4T1RBMk1qUXhNRFEwTkRoYUZ3MHlPVEEyTWpFeE1EUTBORGhhTUJBeERqQU1CZ05WQkFNTQpCVWhsYkd4dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2l0WnY1R28xNm51SERSYTJ1Cm5UNW0xUTl0a3I2NjhwbmhjUDBUa3lqRCtvRUIwbFV6MlNKRVpFdk9kMVhWUlJyUE1TWHJ0eWJvOXAwVHFTR3AKSWcxZ09SV2lzL2ovSVIxc1lkRnV0TEtodHA2azFIdlVpTm9zZE8vSzhLL0FiTzRRUFdUR0JBY3FnLy9Ra01LZApjY2dMWTJQWWN6Sy90OCs2QzdKWUVIZTVBd0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDa1BzeW1lCkpGbGo3NWZPNTROSDVXWFp4QnRvWTdrVjN5ZDVvTzg4Qm5nRThpdHRhSHVhdVFra3cvc0M1eDczM1NzSmxQbEYKdHJhaDRDRE1qcTVkL29rSWJJSkZLZTdOR0xpODJmOXpKK28xZmpEcDk3VXZaSEMwemhVeCtSaUV1M2laUmZZTQozMUh0N1FHNjNWNVNjVjNabWkxbnpmUWM0am44ZDQwa1hYY24KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

11
adaptors/oidc/tls/testdata/ca2.crt vendored Normal file
View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAQACCQCuudlGZuJvODANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVI
ZWxsbzAeFw0xOTA2MjQxMDQ0NDhaFw0yOTA2MjExMDQ0NDhaMBAxDjAMBgNVBAMM
BUhlbGxvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHnrC3q5aCXhULGTTg
w7psUbrH3gpHEExxlw6Zj+UBZHFhxOccGYfHPvqKwfRAKfqkP6VzLdYsfF0fuMOX
ZzFk2hB1eAdl2dsFIn4hMll+jDdo9x+7NKvAXgsFF174ZMVTW26aAME8s4OrNuZT
Fdrp7byuUkwUbSzDC/B/ct9MFQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAGXsF0IA
yP3g1UTLpld2P38dvMXLGN6gwn0S0oh7AQMckJ35yh8CN/2rAkBVujyvGILLhh2/
teoIjM2BcZsrsKZ+Jkr177fRIunsd7a+v18M/3/pVvxPZdnztXspycxIacd7yVbG
5wjN+X7rkoBLhd+BT9+W9O/i+Cu7K89JOO64
-----END CERTIFICATE-----

View File

@@ -0,0 +1 @@
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJsekNDQVFBQ0NRQ3V1ZGxHWnVKdk9EQU5CZ2txaGtpRzl3MEJBUVVGQURBUU1RNHdEQVlEVlFRRERBVkkKWld4c2J6QWVGdzB4T1RBMk1qUXhNRFEwTkRoYUZ3MHlPVEEyTWpFeE1EUTBORGhhTUJBeERqQU1CZ05WQkFNTQpCVWhsYkd4dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRREhuckMzcTVhQ1hoVUxHVFRnCnc3cHNVYnJIM2dwSEVFeHhsdzZaaitVQlpIRmh4T2NjR1lmSFB2cUt3ZlJBS2Zxa1A2VnpMZFlzZkYwZnVNT1gKWnpGazJoQjFlQWRsMmRzRkluNGhNbGwrakRkbzl4KzdOS3ZBWGdzRkYxNzRaTVZUVzI2YUFNRThzNE9yTnVaVApGZHJwN2J5dVVrd1ViU3pEQy9CL2N0OU1GUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFHWHNGMElBCnlQM2cxVVRMcGxkMlAzOGR2TVhMR042Z3duMFMwb2g3QVFNY2tKMzV5aDhDTi8yckFrQlZ1anl2R0lMTGhoMi8KdGVvSWpNMkJjWnNyc0taK0prcjE3N2ZSSXVuc2Q3YSt2MThNLzMvcFZ2eFBaZG56dFhzcHljeElhY2Q3eVZiRwo1d2pOK1g3cmtvQkxoZCtCVDkrVzlPL2krQ3U3Szg5Sk9PNjQKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

11
adaptors/oidc/tls/testdata/ca3.crt vendored Normal file
View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAQACCQDyFQsG5rDcJTANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVI
ZWxsbzAeFw0xOTA2MjQxMDQ0NDhaFw0yOTA2MjExMDQ0NDhaMBAxDjAMBgNVBAMM
BUhlbGxvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUBsXHnAjPL3ejyNRs
INI0cp4Sv4HzgXmL6nypzTdEOT3UcqfvZYj3dr4FWZytxb6XgvyvIzoV++GS22cf
arXwwv0Z6CWiJXI+WQFdsQRQoAt4ucIa046b18p6mCiHfaH98aCOq9K3sxTfNOm3
kWAi6oFzB5C+6HalQ+rWFSVWHQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALQfsBMR
pxD9vzTmhw+rc0HKei9QMViIC3KYPdzvCCe0lMjrWzvcmTtUyCNJm2J2GwBVfyok
zeUskYjinppBy/ZmzpWTeqTLOoeozgAh/Jgya5cPh01BP+pPFYmcQ5wOZHK5PPSP
jvfqMeYs8TjXJRjdBKcMuZAN/8g2Ubtn+QbM
-----END CERTIFICATE-----

View File

@@ -0,0 +1 @@
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJsekNDQVFBQ0NRRHlGUXNHNXJEY0pUQU5CZ2txaGtpRzl3MEJBUVVGQURBUU1RNHdEQVlEVlFRRERBVkkKWld4c2J6QWVGdzB4T1RBMk1qUXhNRFEwTkRoYUZ3MHlPVEEyTWpFeE1EUTBORGhhTUJBeERqQU1CZ05WQkFNTQpCVWhsYkd4dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRFVCc1hIbkFqUEwzZWp5TlJzCklOSTBjcDRTdjRIemdYbUw2bnlwelRkRU9UM1VjcWZ2WllqM2RyNEZXWnl0eGI2WGd2eXZJem9WKytHUzIyY2YKYXJYd3d2MFo2Q1dpSlhJK1dRRmRzUVJRb0F0NHVjSWEwNDZiMThwNm1DaUhmYUg5OGFDT3E5SzNzeFRmTk9tMwprV0FpNm9GekI1Qys2SGFsUStyV0ZTVldIUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFMUWZzQk1SCnB4RDl2elRtaHcrcmMwSEtlaTlRTVZpSUMzS1lQZHp2Q0NlMGxNanJXenZjbVR0VXlDTkptMkoyR3dCVmZ5b2sKemVVc2tZamlucHBCeS9abXpwV1RlcVRMT29lb3pnQWgvSmd5YTVjUGgwMUJQK3BQRlltY1E1d09aSEs1UFBTUApqdmZxTWVZczhUalhKUmpkQktjTXVaQU4vOGcyVWJ0bitRYk0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

View File

@@ -1,64 +1,55 @@
package oidc
package tls
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/pkg/errors"
"golang.org/x/xerrors"
)
func newHTTPClient(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*http.Client, error) {
// NewConfig returns a tls.Config with the given certificates and options.
func NewConfig(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*tls.Config, error) {
pool := x509.NewCertPool()
if config.Config.IDPCertificateAuthority != "" {
logger.Debugf(1, "Loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority")
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if config.Config.IDPCertificateAuthorityData != "" {
logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority-data")
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if config.CACertFilename != "" {
logger.Debugf(1, "Loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate")
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
var tlsConfig tls.Config
if len(pool.Subjects()) > 0 {
tlsConfig.RootCAs = pool
c := &tls.Config{
InsecureSkipVerify: config.SkipTLSVerify,
}
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
return &http.Client{
Transport: &logging.Transport{
Base: &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
},
Logger: logger,
},
}, nil
if len(pool.Subjects()) > 0 {
c.RootCAs = pool
}
return c, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "could not read %s", filename)
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !pool.AppendCertsFromPEM(b) {
return errors.Errorf("could not append certificate from %s", filename)
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
@@ -66,10 +57,10 @@ func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return errors.Wrapf(err, "could not decode base64")
return xerrors.Errorf("could not decode base64: %w", err)
}
if !pool.AppendCertsFromPEM(b) {
return errors.Errorf("could not append certificate")
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,89 @@
package tls
import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/models/kubeconfig"
)
func TestNewConfig(t *testing.T) {
testingLogger := logger.New(t)
testingLogger.SetLevel(1)
t.Run("Defaults", func(t *testing.T) {
c, err := NewConfig(adaptors.OIDCClientConfig{}, testingLogger)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants false but true")
}
if c.RootCAs != nil {
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
config := adaptors.OIDCClientConfig{
SkipTLSVerify: true,
}
c, err := NewConfig(config, testingLogger)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if !c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants true but false")
}
if c.RootCAs != nil {
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
}
})
t.Run("AllCertificates", func(t *testing.T) {
config := adaptors.OIDCClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/ca2.crt.base64")),
},
CACertFilename: "testdata/ca3.crt",
}
c, err := NewConfig(config, testingLogger)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
if c.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify wants false but true")
}
if c.RootCAs == nil {
t.Fatalf("RootCAs wants non-nil but nil")
}
subjects := c.RootCAs.Subjects()
if len(subjects) != 3 {
t.Errorf("len(subjects) wants 3 but %d", len(subjects))
}
})
t.Run("InvalidCertificate", func(t *testing.T) {
config := adaptors.OIDCClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/ca2.crt.base64")),
},
CACertFilename: "testdata/Makefile", // invalid cert
}
_, err := NewConfig(config, testingLogger)
if err == nil {
t.Fatalf("NewConfig wants non-nil but nil")
}
t.Logf("expected error: %+v", err)
})
}
func readFile(t *testing.T, filename string) []byte {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return b
}

View File

@@ -1,107 +0,0 @@
package authserver
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
"net/http"
"testing"
"github.com/pkg/errors"
)
// CodeConfig represents a config for Authorization Code Grant.
type CodeConfig struct {
Issuer string
Scope string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
Code string
}
type codeHandler struct {
t *testing.T
c CodeConfig
templates templates
values templateValues
}
func NewCodeHandler(t *testing.T, c CodeConfig) *codeHandler {
if c.Scope == "" {
c.Scope = "openid"
}
if c.Code == "" {
c.Code = "3d24a8bd-35e6-457d-999e-e04bb1dfcec7"
}
h := codeHandler{
t: t,
c: c,
templates: parseTemplates(t),
values: templateValues{
Issuer: c.Issuer,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
},
}
if c.IDTokenKeyPair != nil {
h.values.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.values.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *codeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("authserver/codeHandler: Error: %s", err)
w.WriteHeader(500)
}
}
func (h *codeHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("authserver/codeHandler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.discovery.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
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()
if h.c.Scope != q.Get("scope") {
return errors.Errorf("scope wants %s but %s", h.c.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.c.Code)
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 errors.Wrapf(err, "could not parse the form")
}
grantType, code := r.Form.Get("grant_type"), r.Form.Get("code")
if grantType != "authorization_code" {
return errors.Errorf("grant_type wants authorization_code but %s", grantType)
}
if h.c.Code != code {
return errors.Errorf("code wants %s but %s", h.c.Code, code)
}
w.Header().Add("Content-Type", "application/json")
if err := h.templates.token.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.jwks.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -1,98 +0,0 @@
package authserver
import (
"crypto/rsa"
"encoding/base64"
"math/big"
"net/http"
"testing"
"github.com/pkg/errors"
)
// PasswordConfig represents a config for Resource Owner Password Credentials Grant.
type PasswordConfig struct {
Issuer string
Scope string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
Username string
Password string
}
type passwordHandler struct {
t *testing.T
c PasswordConfig
templates templates
values templateValues
}
func NewPasswordHandler(t *testing.T, c PasswordConfig) *passwordHandler {
if c.Scope == "" {
c.Scope = "openid"
}
h := passwordHandler{
t: t,
c: c,
templates: parseTemplates(t),
values: templateValues{
Issuer: c.Issuer,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
},
}
if c.IDTokenKeyPair != nil {
h.values.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.values.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *passwordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("authserver/passwordHandler: Error: %s", err)
w.WriteHeader(500)
}
}
func (h *passwordHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("authserver/passwordHandler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.discovery.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// https://tools.ietf.org/html/rfc6749#section-4.3
if err := r.ParseForm(); err != nil {
return errors.Wrapf(err, "could not parse the form")
}
grantType, username, password := r.Form.Get("grant_type"), r.Form.Get("username"), r.Form.Get("password")
if grantType != "password" {
return errors.Errorf("grant_type wants password but %s", grantType)
}
if h.c.Username != username {
return errors.Errorf("username wants %s but %s", h.c.Username, username)
}
if h.c.Password != password {
return errors.Errorf("password wants %s but %s", h.c.Password, password)
}
w.Header().Add("Content-Type", "application/json")
if err := h.templates.token.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.jwks.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -1,35 +0,0 @@
package authserver
import (
"testing"
"text/template"
)
func parseTemplates(t *testing.T) templates {
tpl, err := template.ParseFiles(
"authserver/testdata/oidc-discovery.json",
"authserver/testdata/oidc-token.json",
"authserver/testdata/oidc-jwks.json",
)
if err != nil {
t.Fatalf("could not read the templates: %s", err)
}
return templates{
discovery: tpl.Lookup("oidc-discovery.json"),
token: tpl.Lookup("oidc-token.json"),
jwks: tpl.Lookup("oidc-jwks.json"),
}
}
type templates struct {
discovery *template.Template
token *template.Template
jwks *template.Template
}
type templateValues struct {
Issuer string
IDToken string
RefreshToken string
PrivateKey struct{ N, E string }
}

View File

@@ -1,85 +0,0 @@
{
"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
}

View File

@@ -1,12 +0,0 @@
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "xxx",
"n": "{{ .PrivateKey.N }}",
"e": "{{ .PrivateKey.E }}"
}
]
}

View File

@@ -1,7 +0,0 @@
{
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
"token_type": "Bearer",
"refresh_token": "{{ .RefreshToken }}",
"expires_in": 3600,
"id_token": "{{ .IDToken }}"
}

View File

@@ -1,343 +0,0 @@
package adaptors_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"sync"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/adaptors_test/authserver"
"github.com/int128/kubelogin/adaptors_test/keys"
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
"github.com/int128/kubelogin/adaptors_test/logger"
"github.com/int128/kubelogin/di"
)
// Run the integration tests.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the kuneconfig.
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var passwordConfig authserver.PasswordConfig
server := authserver.Start(t, func(url string) http.Handler {
passwordConfig = authserver.PasswordConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Username: "USER",
Password: "PASS",
}
return authserver.NewPasswordHandler(t, passwordConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: passwordConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, nil, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: passwordConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACert", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACertData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDTokenKeyPair: keys.JWSKeyPair,
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, codeConfig.Issuer)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: codeConfig.Issuer,
IDToken: idToken,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, nil, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
})
})
}
func newIDToken(t *testing.T, issuer string) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
}
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func runCmd(t *testing.T, ctx context.Context, br *browserRequest, args ...string) {
t.Helper()
cmd := di.NewCmdWith(logger.New(t), br)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
type browserRequest struct {
t *testing.T
urlCh chan<- string
wg *sync.WaitGroup
}
func (r *browserRequest) ShowLocalServerURL(url string) {
defer close(r.urlCh)
r.t.Logf("Open %s for authentication", url)
r.urlCh <- url
}
func (r *browserRequest) wait() {
r.wg.Wait()
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
t.Helper()
urlCh := make(chan string)
var wg sync.WaitGroup
go func() {
defer wg.Done()
select {
case url := <-urlCh:
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
case err := <-ctx.Done():
t.Errorf("context done while waiting for URL prompt: %s", err)
}
}()
wg.Add(1)
return &browserRequest{t, urlCh, &wg}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

View File

@@ -12,46 +12,35 @@ import (
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/auth"
"github.com/int128/kubelogin/usecases/login"
)
var usecasesSet = wire.NewSet(
login.Login{},
login.Exec{},
wire.Bind((*usecases.Login)(nil), (*login.Login)(nil)),
wire.Bind((*usecases.LoginAndExec)(nil), (*login.Exec)(nil)),
)
var adaptorsSet = wire.NewSet(
cmd.Cmd{},
kubeconfig.Kubeconfig{},
oidc.Factory{},
env.Env{},
wire.Bind((*adaptors.Cmd)(nil), (*cmd.Cmd)(nil)),
wire.Bind((*adaptors.Kubeconfig)(nil), (*kubeconfig.Kubeconfig)(nil)),
wire.Bind((*adaptors.OIDC)(nil), (*oidc.Factory)(nil)),
wire.Bind((*adaptors.Env)(nil), (*env.Env)(nil)),
)
var extraSet = wire.NewSet(
login.ShowLocalServerURL{},
wire.Bind((*usecases.LoginShowLocalServerURL)(nil), (*login.ShowLocalServerURL)(nil)),
logger.New,
)
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() adaptors.Cmd {
wire.Build(
usecasesSet,
adaptorsSet,
extraSet,
auth.Set,
auth.ExtraSet,
login.Set,
cmd.Set,
env.Set,
kubeconfig.Set,
oidc.Set,
logger.Set,
)
return nil
}
// NewCmdWith returns an instance of adaptors.Cmd with given dependencies.
// This is only for e2e tests.
func NewCmdWith(adaptors.Logger, usecases.LoginShowLocalServerURL) adaptors.Cmd {
wire.Build(
usecasesSet,
adaptorsSet,
auth.Set,
login.Set,
cmd.Set,
env.Set,
kubeconfig.Set,
oidc.Set,
)
return nil
}

View File

@@ -6,7 +6,6 @@
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"github.com/int128/kubelogin/adaptors/env"
@@ -14,34 +13,38 @@ import (
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/auth"
"github.com/int128/kubelogin/usecases/login"
)
// Injectors from di.go:
func NewCmd() adaptors.Cmd {
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
adaptorsLogger := logger.New()
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
showLocalServerURL := &login.ShowLocalServerURL{
showLocalServerURL := &auth.ShowLocalServerURL{
Logger: adaptorsLogger,
}
loginLogin := &login.Login{
Kubeconfig: kubeconfigKubeconfig,
authentication := &auth.Authentication{
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
}
exec := &login.Exec{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Env: envEnv,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
@@ -52,24 +55,27 @@ func NewCmd() adaptors.Cmd {
}
func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL) adaptors.Cmd {
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
loginLogin := &login.Login{
Kubeconfig: kubeconfigKubeconfig,
authentication := &auth.Authentication{
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
}
exec := &login.Exec{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Env: envEnv,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
@@ -78,11 +84,3 @@ func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases
}
return cmdCmd
}
// di.go:
var usecasesSet = wire.NewSet(login.Login{}, login.Exec{}, wire.Bind((*usecases.Login)(nil), (*login.Login)(nil)), wire.Bind((*usecases.LoginAndExec)(nil), (*login.Exec)(nil)))
var adaptorsSet = wire.NewSet(cmd.Cmd{}, kubeconfig.Kubeconfig{}, oidc.Factory{}, env.Env{}, wire.Bind((*adaptors.Cmd)(nil), (*cmd.Cmd)(nil)), wire.Bind((*adaptors.Kubeconfig)(nil), (*kubeconfig.Kubeconfig)(nil)), wire.Bind((*adaptors.OIDC)(nil), (*oidc.Factory)(nil)), wire.Bind((*adaptors.Env)(nil), (*env.Env)(nil)))
var extraSet = wire.NewSet(login.ShowLocalServerURL{}, wire.Bind((*usecases.LoginShowLocalServerURL)(nil), (*login.ShowLocalServerURL)(nil)), logger.New)

BIN
docs/authn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

16
docs/authn.seqdiag Normal file
View File

@@ -0,0 +1,16 @@
seqdiag {
User -> kubelogin [label = "execute"];
kubelogin -> Browser [label = "open"];
Browser -> Provider [label = "authentication request"];
Browser <-- Provider [label = "redirect"];
User -> Browser [label = "enter credentials"];
Browser -> Provider [label = "credentials"];
Browser <-- Provider [label = "authentication response"];
User <-- Browser [label = "success"];
kubelogin <-- Browser [label = "close"];
kubelogin -> Provider [label = "token request"];
kubelogin <-- Provider [label = "token response"];
kubelogin -> kubeconfig [label = "write token"];
kubelogin <-- kubeconfig;
User <-- kubelogin;
}

403
e2e_test/cmd_test.go Normal file
View File

@@ -0,0 +1,403 @@
package e2e_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"sync"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/di"
"github.com/int128/kubelogin/e2e_test/idp"
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
"github.com/int128/kubelogin/e2e_test/keys"
"github.com/int128/kubelogin/e2e_test/kubeconfig"
"github.com/int128/kubelogin/e2e_test/localserver"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/usecases"
)
// Run the integration tests.
//
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for the local server.
// 4. Verify the kuneconfig.
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", time.Hour)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "profile groups openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("CACert", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("CACertData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", time.Hour)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", time.Hour)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
}
func newIDToken(t *testing.T, issuer, nonce string, expiration time.Duration) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(expiration).Unix(),
}
claims.Nonce = nonce
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
var nonce string
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
service.EXPECT().Exchange("YOUR_AUTH_CODE").
DoAndReturn(func(string) (*idp.TokenResponse, error) {
*idToken = newIDToken(t, serverURL, nonce, time.Hour)
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
})
}
func runCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, args ...string) {
t.Helper()
cmd := di.NewCmdWith(logger.New(t), s)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
type nopBrowserRequest struct {
t *testing.T
}
func (r *nopBrowserRequest) ShowLocalServerURL(url string) {
r.t.Errorf("ShowLocalServerURL must not be called")
}
type browserRequest struct {
t *testing.T
urlCh chan<- string
wg *sync.WaitGroup
}
func (r *browserRequest) ShowLocalServerURL(url string) {
defer close(r.urlCh)
r.t.Logf("Open %s for authentication", url)
r.urlCh <- url
}
func (r *browserRequest) wait() {
r.wg.Wait()
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
t.Helper()
urlCh := make(chan string)
var wg sync.WaitGroup
go func() {
defer wg.Done()
select {
case url := <-urlCh:
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
case err := <-ctx.Done():
t.Errorf("context done while waiting for URL prompt: %s", err)
}
}()
wg.Add(1)
return &browserRequest{t, urlCh, &wg}
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

142
e2e_test/idp/handler.go Normal file
View File

@@ -0,0 +1,142 @@
// Package idp provides a test double of the identity provider of OpenID Connect.
package idp
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"golang.org/x/xerrors"
)
func NewHandler(t *testing.T, service Service) *Handler {
return &Handler{t, service}
}
// Handler provides a HTTP handler for the identity provider of OpenID Connect.
// You need to implement the Service interface.
// Note that this skips some security checks and is only for testing.
type Handler struct {
t *testing.T
service Service
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
wr := &responseWriterRecorder{w, 200}
err := h.serveHTTP(wr, r)
if err == nil {
h.t.Logf("%d %s %s", wr.statusCode, r.Method, r.RequestURI)
return
}
if errResp := new(ErrorResponse); xerrors.As(err, &errResp) {
h.t.Logf("400 %s %s: %s", r.Method, r.RequestURI, err)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(400)
e := json.NewEncoder(w)
if err := e.Encode(errResp); err != nil {
h.t.Errorf("idp/handler: could not write the response: %s", err)
}
return
}
h.t.Logf("500 %s %s: %s", r.Method, r.RequestURI, err)
http.Error(w, err.Error(), 500)
}
type responseWriterRecorder struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterRecorder) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
}
func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
discoveryResponse := h.service.Discovery()
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(discoveryResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/certs":
certificatesResponse := h.service.GetCertificates()
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(certificatesResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/auth":
// 3.1.2.1. Authentication Request
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
q := r.URL.Query()
redirectURI, scope, state, nonce := q.Get("redirect_uri"), q.Get("scope"), q.Get("state"), q.Get("nonce")
code, err := h.service.AuthenticateCode(scope, nonce)
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
to := fmt.Sprintf("%s?state=%s&code=%s", redirectURI, state, code)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/token":
if err := r.ParseForm(); err != nil {
return xerrors.Errorf("could not parse the form: %w", err)
}
grantType := r.Form.Get("grant_type")
switch grantType {
case "authorization_code":
// 3.1.3.1. Token Request
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
code := r.Form.Get("code")
tokenResponse, err := h.service.Exchange(code)
if err != nil {
return xerrors.Errorf("token request error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case "password":
// 4.3. Resource Owner Password Credentials Grant
// https://tools.ietf.org/html/rfc6749#section-4.3
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
tokenResponse, err := h.service.AuthenticatePassword(username, password, scope)
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case "refresh_token":
// 12.1. Refresh Request
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
refreshToken := r.Form.Get("refresh_token")
tokenResponse, err := h.service.Refresh(refreshToken)
if err != nil {
return xerrors.Errorf("token refresh error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
default:
// 5.2. Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2
return &ErrorResponse{
Code: "invalid_grant",
Description: fmt.Sprintf("unknown grant_type %s", grantType),
}
}
default:
http.NotFound(w, r)
}
return nil
}

View File

@@ -0,0 +1,110 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/e2e_test/idp (interfaces: Service)
// Package mock_idp is a generated GoMock package.
package mock_idp
import (
gomock "github.com/golang/mock/gomock"
idp "github.com/int128/kubelogin/e2e_test/idp"
reflect "reflect"
)
// MockService is a mock of Service interface
type MockService struct {
ctrl *gomock.Controller
recorder *MockServiceMockRecorder
}
// MockServiceMockRecorder is the mock recorder for MockService
type MockServiceMockRecorder struct {
mock *MockService
}
// NewMockService creates a new mock instance
func NewMockService(ctrl *gomock.Controller) *MockService {
mock := &MockService{ctrl: ctrl}
mock.recorder = &MockServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockService) EXPECT() *MockServiceMockRecorder {
return m.recorder
}
// AuthenticateCode mocks base method
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateCode indicates an expected call of AuthenticateCode
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
}
// AuthenticatePassword mocks base method
func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticatePassword indicates an expected call of AuthenticatePassword
func (mr *MockServiceMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockService)(nil).AuthenticatePassword), arg0, arg1, arg2)
}
// Discovery mocks base method
func (m *MockService) Discovery() *idp.DiscoveryResponse {
ret := m.ctrl.Call(m, "Discovery")
ret0, _ := ret[0].(*idp.DiscoveryResponse)
return ret0
}
// Discovery indicates an expected call of Discovery
func (mr *MockServiceMockRecorder) Discovery() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockService)(nil).Discovery))
}
// Exchange mocks base method
func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
ret := m.ctrl.Call(m, "Exchange", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exchange indicates an expected call of Exchange
func (mr *MockServiceMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockService)(nil).Exchange), arg0)
}
// GetCertificates mocks base method
func (m *MockService) GetCertificates() *idp.CertificatesResponse {
ret := m.ctrl.Call(m, "GetCertificates")
ret0, _ := ret[0].(*idp.CertificatesResponse)
return ret0
}
// GetCertificates indicates an expected call of GetCertificates
func (mr *MockServiceMockRecorder) GetCertificates() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockService)(nil).GetCertificates))
}
// Refresh mocks base method
func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
ret := m.ctrl.Call(m, "Refresh", arg0)
ret0, _ := ret[0].(*idp.TokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockServiceMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockService)(nil).Refresh), arg0)
}

120
e2e_test/idp/service.go Normal file
View File

@@ -0,0 +1,120 @@
package idp
//go:generate mockgen -destination mock_idp/mock_service.go github.com/int128/kubelogin/e2e_test/idp Service
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
)
// Service provides discovery and authentication methods.
// If an implemented method returns an ErrorResponse,
// the handler will respond 400 and corresponding json of the ErrorResponse.
// Otherwise, the handler will respond 500 and fail the current test.
type Service interface {
Discovery() *DiscoveryResponse
GetCertificates() *CertificatesResponse
AuthenticateCode(scope, nonce string) (code string, err error)
Exchange(code string) (*TokenResponse, error)
AuthenticatePassword(username, password, scope string) (*TokenResponse, error)
Refresh(refreshToken string) (*TokenResponse, error)
}
type DiscoveryResponse struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
JwksURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ScopesSupported []string `json:"scopes_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
// NewDiscoveryResponse returns a DiscoveryResponse for the local server.
// This is based on https://accounts.google.com/.well-known/openid-configuration.
func NewDiscoveryResponse(issuer string) *DiscoveryResponse {
return &DiscoveryResponse{
Issuer: issuer,
AuthorizationEndpoint: issuer + "/auth",
TokenEndpoint: issuer + "/token",
JwksURI: issuer + "/certs",
UserinfoEndpoint: issuer + "/userinfo",
RevocationEndpoint: issuer + "/revoke",
ResponseTypesSupported: []string{"code id_token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
ScopesSupported: []string{"openid", "email", "profile"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
CodeChallengeMethodsSupported: []string{"plain", "S256"},
ClaimsSupported: []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
}
}
type CertificatesResponse struct {
Keys []*CertificatesResponseKey `json:"keys"`
}
type CertificatesResponseKey struct {
Kty string `json:"kty"`
Alg string `json:"alg"`
Use string `json:"use"`
Kid string `json:"kid"`
N string `json:"n"`
E string `json:"e"`
}
// NewCertificatesResponse returns a CertificatesResponse using the key pair.
// This is used for verifying a signature of ID token.
func NewCertificatesResponse(idTokenKeyPair *rsa.PrivateKey) *CertificatesResponse {
return &CertificatesResponse{
Keys: []*CertificatesResponseKey{
{
Kty: "RSA",
Alg: "RS256",
Use: "sig",
Kid: "dummy",
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
N: base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
},
},
}
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
IDToken string `json:"id_token"`
}
// NewTokenResponse returns a TokenResponse.
func NewTokenResponse(idToken, refreshToken string) *TokenResponse {
return &TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
IDToken: idToken,
RefreshToken: refreshToken,
}
}
// ErrorResponse represents an error response described in the following section:
// 5.2 Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2
type ErrorResponse struct {
Code string `json:"error"`
Description string `json:"error_description"`
}
func (err *ErrorResponse) Error() string {
return fmt.Sprintf("%s(%s)", err.Code, err.Description)
}

View File

@@ -8,7 +8,7 @@ import (
"encoding/pem"
"io/ioutil"
"github.com/pkg/errors"
"golang.org/x/xerrors"
)
// TLSCACert is path to the CA certificate.
@@ -54,18 +54,18 @@ func init() {
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
b, err := ioutil.ReadFile(name)
if err != nil {
return nil, errors.Wrapf(err, "could not read JWSKey")
return nil, xerrors.Errorf("could not read JWSKey: %w", err)
}
block, rest := pem.Decode(b)
if block == nil {
return nil, errors.New("could not decode PEM")
return nil, xerrors.New("could not decode PEM")
}
if len(rest) > 0 {
return nil, errors.New("PEM should contain single key but multiple keys")
return nil, xerrors.New("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, errors.Wrapf(err, "could not parse the key")
return nil, xerrors.Errorf("could not parse the key: %w", err)
}
return k, nil
}

View File

@@ -16,6 +16,7 @@ type Values struct {
IDPCertificateAuthority string
IDPCertificateAuthorityData string
IDToken string
RefreshToken string
}
// Create creates a kubeconfig file and returns path to it.

View File

@@ -30,5 +30,8 @@ users:
#{{ end }}
#{{ if .IDToken }}
id-token: {{ .IDToken }}
#{{ end }}
#{{ if .RefreshToken }}
refresh-token: {{ .RefreshToken }}
#{{ end }}
name: oidc

View File

@@ -1,8 +1,7 @@
// Package authserver provides an authentication server which supports
// Authorization Code Grant and Resource Owner Password Credentials Grant.
// Package localserver provides a http server running on localhost.
// This is only for testing.
//
package authserver
package localserver
import (
"context"
@@ -29,12 +28,12 @@ func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
}
// Start starts an authentication server.
func Start(t *testing.T, h func(url string) http.Handler) Shutdowner {
func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
s := &http.Server{
Handler: h(url),
Handler: h,
}
go func() {
err := s.Serve(l)
@@ -42,16 +41,16 @@ func Start(t *testing.T, h func(url string) http.Handler) Shutdowner {
t.Error(err)
}
}()
return &shutdowner{l, s}
return url, &shutdowner{l, s}
}
// Start starts an authentication server with TLS.
func StartTLS(t *testing.T, cert string, key string, h func(url string) http.Handler) Shutdowner {
func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
s := &http.Server{
Handler: h(url),
Handler: h,
}
go func() {
err := s.ServeTLS(l, cert, key)
@@ -59,7 +58,7 @@ func StartTLS(t *testing.T, cert string, key string, h func(url string) http.Han
t.Error(err)
}
}()
return &shutdowner{l, s}
return url, &shutdowner{l, s}
}
func newLocalhostListener(t *testing.T) (net.Listener, string) {

View File

@@ -5,7 +5,8 @@ import (
)
func New(t testingLogger) *logger.Logger {
return logger.FromStdLogger(&bridge{t})
b := &bridge{t}
return logger.NewWith(b, b)
}
type testingLogger interface {
@@ -19,3 +20,8 @@ type bridge struct {
func (b *bridge) Printf(format string, v ...interface{}) {
b.t.Logf(format, v...)
}
func (b *bridge) Output(calldepth int, s string) error {
b.t.Logf("%s", s)
return nil
}

8
go.mod
View File

@@ -7,20 +7,20 @@ require (
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.3.1
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/google/wire v0.2.2
github.com/google/wire v0.3.0
github.com/imdario/mergo v0.3.7 // indirect
github.com/int128/oauth2cli v1.4.0
github.com/int128/oauth2cli v1.4.1
github.com/json-iterator/go v1.1.6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v0.0.4
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
gopkg.in/yaml.v2 v2.2.2

27
go.sum
View File

@@ -12,6 +12,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
@@ -25,22 +26,25 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/wire v0.2.2 h1:fSIRzE/K12IaNgV6X0173X/oLrTwHKRiMcFZhiDrN3s=
github.com/google/wire v0.2.2/go.mod h1:7FHVg6mFpFQrjeUZrm+BaD50N5jnDKm50uVPTpyYOmU=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60=
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/int128/oauth2cli v1.4.0 h1:Xt4uk2lIb9Mf9Xyd5o43Hf9iV5izb2jYK3zRX/cPgh0=
github.com/int128/oauth2cli v1.4.0/go.mod h1:81pWOyFVt1TRyZ7lZDtZuAGOE/S/jEpb1mpocRopI6U=
github.com/int128/oauth2cli v1.4.1 h1:IsaYMafEDS1jyArxYdmksw+nMsNxiYCQzdkPj3QF9BY=
github.com/int128/oauth2cli v1.4.1/go.mod h1:CMJjyUSgKiobye1M/9byFACOjtB2LRo2mo7boklEKlI=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@@ -53,8 +57,6 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
@@ -64,8 +66,8 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE=
github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
@@ -87,9 +89,8 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 h1:HdqqaWmYAUI7/dmByKKEw+yxDksGSo+9GjkUc9Zp34E=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -98,9 +99,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
@@ -108,6 +109,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -6,15 +6,16 @@ type ContextName string
// UserName represents name of a user.
type UserName string
// Auth represents the authentication provider,
// AuthProvider represents the authentication provider,
// i.e. context, user and auth-provider in a kubeconfig.
type Auth struct {
type AuthProvider struct {
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // Context name (optional)
OIDCConfig OIDCConfig
}
// OIDCConfig represents a configuration of an OIDC provider.
type OIDCConfig struct {
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id

139
usecases/auth/auth.go Normal file
View File

@@ -0,0 +1,139 @@
package auth
import (
"context"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"golang.org/x/xerrors"
)
// Set provides the use-case of Authentication.
var Set = wire.NewSet(
wire.Struct(new(Authentication), "*"),
wire.Bind(new(usecases.Authentication), new(*Authentication)),
)
// ExtraSet is a set of interaction components for e2e testing.
var ExtraSet = wire.NewSet(
wire.Struct(new(ShowLocalServerURL), "*"),
wire.Bind(new(usecases.LoginShowLocalServerURL), new(*ShowLocalServerURL)),
)
const passwordPrompt = "Password: "
// Authentication provides the internal use-case of authentication.
//
// If the IDToken is not set, it performs the authentication flow.
// If the IDToken is valid, it does nothing.
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
// If the RefreshToken has expired, it performs the authentication flow.
//
// The authentication flow is determined as:
//
// If the Username is not set, it performs the authorization code flow.
// Otherwise, it performs the resource owner password credentials flow.
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
client, err := u.OIDC.New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return nil, xerrors.Errorf("could not create an OIDC client: %w", err)
}
if in.CurrentAuthProvider.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Verifying the token in the kubeconfig")
out, err := client.Verify(ctx, adaptors.OIDCVerifyIn{IDToken: in.CurrentAuthProvider.OIDCConfig.IDToken})
if err != nil {
return nil, xerrors.Errorf("invalid ID token in the kubeconfig, you need to remove it manually: %w", err)
}
if out.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
u.Logger.Debugf(1, "You already have a valid token in the kubeconfig")
return &usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: in.CurrentAuthProvider.OIDCConfig.IDToken,
RefreshToken: in.CurrentAuthProvider.OIDCConfig.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "You have an expired token at %s", out.IDTokenExpiry)
}
if in.CurrentAuthProvider.OIDCConfig.RefreshToken != "" {
u.Logger.Debugf(1, "Refreshing the token")
out, err := client.Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: in.CurrentAuthProvider.OIDCConfig.RefreshToken,
})
if err == nil {
return &usecases.AuthenticationOut{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "Could not refresh the token: %s", err)
}
if in.Username == "" {
u.Logger.Debugf(1, "Performing the authentication code flow")
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &usecases.AuthenticationOut{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "Performing the resource owner password credentials flow")
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Username: in.Username,
Password: in.Password,
})
if err != nil {
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
}
return &usecases.AuthenticationOut{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
// ShowLocalServerURL just shows the URL of local server to console.
type ShowLocalServerURL struct {
Logger adaptors.Logger
}
func (s *ShowLocalServerURL) ShowLocalServerURL(url string) {
s.Logger.Printf("Open %s for authentication", url)
}

382
usecases/auth/auth_test.go Normal file
View File

@@ -0,0 +1,382 @@
package auth
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"golang.org/x/xerrors"
)
func TestAuthentication_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
pastTime := time.Now().Add(-time.Hour) //TODO: inject time service
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("AuthorizationCodeFlow", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
ListenPort: []int{10000},
SkipOpenBrowser: true,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: []int{10000},
SkipOpenBrowser: true,
}).
Return(&adaptors.OIDCAuthenticateOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/UsePassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Username: "USER",
Password: "PASS",
}).
Return(&adaptors.OIDCAuthenticateOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
Username: "USER",
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Username: "USER",
Password: "PASS",
}).
Return(&adaptors.OIDCAuthenticateOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
}).
Return(mockOIDCClient, nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Authentication{
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
Username: "USER",
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
}
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
}).
Return(mock_adaptors.NewMockOIDCClient(ctrl), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := Authentication{
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "VALID_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "VALID_REFRESH_TOKEN",
},
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "VALID_REFRESH_TOKEN",
}).
Return(&adaptors.OIDCAuthenticateOut{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
ListenPort: []int{10000},
CurrentAuthProvider: &kubeconfig.AuthProvider{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "EXPIRED_ID_TOKEN",
RefreshToken: "EXPIRED_REFRESH_TOKEN",
},
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "EXPIRED_REFRESH_TOKEN",
}).
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: []int{10000},
}).
Return(&adaptors.OIDCAuthenticateOut{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
}

View File

@@ -2,16 +2,18 @@ package usecases
import (
"context"
"time"
"github.com/int128/kubelogin/models/kubeconfig"
)
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec,Authentication
type Login interface {
Do(ctx context.Context, in LoginIn) error
}
// LoginIn represents an input DTO of the Login use-case.
type LoginIn struct {
KubeconfigFilename string // Default to the environment variable or global config as kubectl
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
@@ -34,6 +36,7 @@ type LoginAndExec interface {
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
}
// LoginAndExecInIn represents an input DTO of the LoginAndExec use-case.
type LoginAndExecIn struct {
LoginIn LoginIn
Executable string
@@ -43,3 +46,27 @@ type LoginAndExecIn struct {
type LoginAndExecOut struct {
ExitCode int
}
type Authentication interface {
Do(ctx context.Context, in AuthenticationIn) (*AuthenticationOut, error)
}
// AuthenticationIn represents an input DTO of the Authentication use-case.
type AuthenticationIn struct {
CurrentAuthProvider *kubeconfig.AuthProvider
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
}
// AuthenticationIn represents an output DTO of the Authentication use-case.
type AuthenticationOut struct {
AlreadyHasValidIDToken bool
IDTokenExpiry time.Time
IDTokenClaims map[string]string
IDToken string
RefreshToken string
}

View File

@@ -5,100 +5,73 @@ import (
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"golang.org/x/xerrors"
)
// Exec provide the use case of wrapping the kubectl command.
// Exec provide the use case of transparently executing kubectl.
//
// If the current auth provider is not oidc, just run kubectl.
// If the kubeconfig has a valid token, just run kubectl.
// Otherwise, update the kubeconfig and run kubectl.
//
type Exec struct {
Kubeconfig adaptors.Kubeconfig
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
Authentication usecases.Authentication
Kubeconfig adaptors.Kubeconfig
Env adaptors.Env
Logger adaptors.Logger
}
func (u *Exec) Do(ctx context.Context, in usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
if err := u.doInternal(ctx, in.LoginIn); err != nil {
return nil, errors.WithStack(err)
if err := u.login(ctx, in.LoginIn); err != nil {
return nil, xerrors.Errorf("could not log in to the provider: %w", err)
}
u.Logger.Debugf(1, "Executing the command %s %s", in.Executable, in.Args)
exitCode, err := u.Env.Exec(ctx, in.Executable, in.Args)
if err != nil {
return nil, errors.Wrapf(err, "could not execute kubectl")
return nil, xerrors.Errorf("could not execute kubectl: %w", err)
}
u.Logger.Debugf(1, "The command exited with status %d", exitCode)
return &usecases.LoginAndExecOut{ExitCode: exitCode}, nil
}
func (u *Exec) doInternal(ctx context.Context, in usecases.LoginIn) error {
func (u *Exec) login(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
auth, err := u.Kubeconfig.GetCurrentAuth(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
authProvider, err := u.Kubeconfig.GetCurrentAuthProvider(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Debugf(1, "The current authentication provider is not oidc: %s", err)
return nil
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
u.Logger.Debugf(1, "A token will be written to %s", auth.LocationOfOrigin)
u.Logger.Debugf(1, "Using the authentication provider of the user %s", authProvider.UserName)
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
client, err := u.OIDC.New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
CurrentAuthProvider: authProvider,
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not create an OIDC client")
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
}
if out.AlreadyHasValidIDToken {
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
return nil
}
if auth.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := client.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig})
if err == nil {
u.Logger.Debugf(1, "You already have a valid token until %s", token.Expiry)
dumpIDToken(u.Logger, token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
authProvider.OIDCConfig.IDToken = out.IDToken
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
var tokenSet *adaptors.OIDCAuthenticateOut
if in.Username != "" {
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return errors.Wrapf(err, "could not read a password")
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: in.Username,
Password: in.Password,
})
if err != nil {
return errors.Wrapf(err, "error while the resource owner password credentials grant flow")
}
tokenSet = out
} else {
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return errors.Wrapf(err, "error while the authorization code grant flow")
}
tokenSet = out
}
u.Logger.Printf("You got a valid token until %s", tokenSet.VerifiedIDToken.Expiry)
dumpIDToken(u.Logger, tokenSet.VerifiedIDToken)
auth.OIDCConfig.IDToken = tokenSet.IDToken
auth.OIDCConfig.RefreshToken = tokenSet.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", auth.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuth(auth); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
}
return nil
}

View File

@@ -3,60 +3,153 @@ package login
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"github.com/int128/kubelogin/usecases/mock_usecases"
"golang.org/x/xerrors"
)
func TestExec_Do(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
},
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: []int{10000},
}), nil)
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
})
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(123, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
CurrentAuthProvider: currentAuthProvider,
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(&usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.LoginAndExecOut{
ExitCode: 123,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
}
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
Return(&usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Exec{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: []int{10000},
},
Executable: "kubectl",
Args: []string{"foo", "bar"},
})
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
if out.ExitCode != 0 {
t.Errorf("ExitCode wants 0 but %d", out.ExitCode)
want := &usecases.LoginAndExecOut{
ExitCode: 0,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
@@ -64,35 +157,127 @@ func TestExec_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
LoginIn: usecases.LoginIn{},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, errors.New("no oidc config"))
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, xerrors.New("no oidc config"))
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
u := Exec{
Kubeconfig: mockKubeconfig,
OIDC: mock_adaptors.NewMockOIDC(ctrl),
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: []int{10000},
},
Executable: "kubectl",
Args: []string{"foo", "bar"},
})
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
if out.ExitCode != 0 {
t.Errorf("ExitCode wants 0 but %d", out.ExitCode)
want := &usecases.LoginAndExecOut{
ExitCode: 0,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AuthenticationError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
Return(nil, xerrors.New("authentication error"))
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mock_adaptors.NewMockEnv(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
t.Run("WriteError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}).
Return(xerrors.New("I/O error"))
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
Return(&usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mock_adaptors.NewMockEnv(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
}

View File

@@ -3,10 +3,18 @@ package login
import (
"context"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"golang.org/x/xerrors"
)
// Set provides the use-cases of logging in.
var Set = wire.NewSet(
wire.Struct(new(Login), "*"),
wire.Struct(new(Exec), "*"),
wire.Bind(new(usecases.Login), new(*Login)),
wire.Bind(new(usecases.LoginAndExec), new(*Exec)),
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
@@ -16,107 +24,55 @@ const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubec
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
const passwordPrompt = "Password: "
// Login provides the use case of login to the provider.
// Login provides the use case of explicit login.
//
// If the current auth provider is not oidc, show the error.
// If the kubeconfig has a valid token, do nothing.
// Otherwise, update the kubeconfig.
//
type Login struct {
Kubeconfig adaptors.Kubeconfig
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
Authentication usecases.Authentication
Kubeconfig adaptors.Kubeconfig
Logger adaptors.Logger
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
auth, err := u.Kubeconfig.GetCurrentAuth(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
authProvider, err := u.Kubeconfig.GetCurrentAuthProvider(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage)
return errors.Wrapf(err, "could not find the current authentication provider")
return xerrors.Errorf("could not find the current authentication provider: %w", err)
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
u.Logger.Debugf(1, "A token will be written to %s", auth.LocationOfOrigin)
u.Logger.Debugf(1, "Using the authentication provider of the user %s", authProvider.UserName)
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
client, err := u.OIDC.New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
CurrentAuthProvider: authProvider,
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not create an OIDC client")
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
}
if out.AlreadyHasValidIDToken {
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
return nil
}
if auth.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := client.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig})
if err == nil {
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
dumpIDToken(u.Logger, token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
var tokenSet *adaptors.OIDCAuthenticateOut
if in.Username != "" {
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return errors.Wrapf(err, "could not read a password")
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: in.Username,
Password: in.Password,
})
if err != nil {
return errors.Wrapf(err, "error while the resource owner password credentials grant flow")
}
tokenSet = out
} else {
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return errors.Wrapf(err, "error while the authorization code grant flow")
}
tokenSet = out
}
u.Logger.Printf("You got a valid token until %s", tokenSet.VerifiedIDToken.Expiry)
dumpIDToken(u.Logger, tokenSet.VerifiedIDToken)
auth.OIDCConfig.IDToken = tokenSet.IDToken
auth.OIDCConfig.RefreshToken = tokenSet.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", auth.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuth(auth); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
authProvider.OIDCConfig.IDToken = out.IDToken
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
}
return nil
}
func dumpIDToken(logger adaptors.Logger, token *oidc.IDToken) {
var claims map[string]interface{}
if err := token.Claims(&claims); err != nil {
logger.Debugf(1, "Error while inspection of the ID token: %s", err)
}
for k, v := range claims {
logger.Debugf(1, "The ID token has the claim: %s=%v", k, v)
}
}
// ShowLocalServerURL just shows the URL of local server to console.
type ShowLocalServerURL struct {
Logger adaptors.Logger
}
func (s *ShowLocalServerURL) ShowLocalServerURL(url string) {
s.Logger.Printf("Open %s for authentication", url)
}

View File

@@ -5,260 +5,221 @@ import (
"testing"
"time"
"github.com/coreos/go-oidc"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"github.com/int128/kubelogin/usecases/mock_usecases"
"golang.org/x/xerrors"
)
func newMockCodeOIDC(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) *mock_adaptors.MockOIDCClient {
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, in).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDCClient
}
func newMockPasswordOIDC(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) *mock_adaptors.MockOIDCClient {
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, in).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDCClient
}
func newAuth(idToken, refreshToken string) *kubeconfig.Auth {
return &kubeconfig.Auth{
LocationOfOrigin: "theLocation",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: idToken,
RefreshToken: refreshToken,
},
}
}
func TestLogin_Do(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: []int{10000},
}), nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(newMockPasswordOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: "USER",
Password: "PASS",
}), nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
in := usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}); err != nil {
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
})
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
CurrentAuthProvider: currentAuthProvider,
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(&usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Login{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("AskPassword", func(t *testing.T) {
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
in := usecases.LoginIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockPasswordOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: "USER",
Password: "PASS",
}), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
Return(&usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Env: mockEnv,
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
Username: "USER",
}); err != nil {
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("AskPasswordError", func(t *testing.T) {
t.Run("NoOIDCConfig", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
in := usecases.LoginIn{}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mock_adaptors.NewMockOIDCClient(ctrl), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", errors.New("error"))
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, xerrors.New("no oidc config"))
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Env: mockEnv,
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
Username: "USER",
}); err == nil {
t.Errorf("err wants an error but nil")
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
}
})
t.Run("KubeconfigHasValidToken", func(t *testing.T) {
t.Run("AuthenticationError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("VALID_ID_TOKEN", "N/A")
in := usecases.LoginIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig}).
Return(&oidc.IDToken{Expiry: time.Now()}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mockOIDCClient, nil)
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
Return(nil, xerrors.New("authentication error"))
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{}); err != nil {
t.Errorf("Do returned error: %+v", err)
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
}
})
t.Run("KubeconfigHasExpiredToken", func(t *testing.T) {
t.Run("WriteError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("EXPIRED_ID_TOKEN", "EXPIRED_REFRESH_TOKEN")
in := usecases.LoginIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDCClient := newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{Config: auth.OIDCConfig})
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig}).
Return(nil, errors.New("token expired"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mockOIDCClient, nil)
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}).
Return(xerrors.New("I/O error"))
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
Return(&usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{}); err != nil {
t.Errorf("Do returned error: %+v", err)
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
}
})
}

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec)
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec,Authentication)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
@@ -81,3 +81,39 @@ func (m *MockLoginAndExec) Do(arg0 context.Context, arg1 usecases.LoginAndExecIn
func (mr *MockLoginAndExecMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLoginAndExec)(nil).Do), arg0, arg1)
}
// MockAuthentication is a mock of Authentication interface
type MockAuthentication struct {
ctrl *gomock.Controller
recorder *MockAuthenticationMockRecorder
}
// MockAuthenticationMockRecorder is the mock recorder for MockAuthentication
type MockAuthenticationMockRecorder struct {
mock *MockAuthentication
}
// NewMockAuthentication creates a new mock instance
func NewMockAuthentication(ctrl *gomock.Controller) *MockAuthentication {
mock := &MockAuthentication{ctrl: ctrl}
mock.recorder = &MockAuthenticationMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthentication) EXPECT() *MockAuthenticationMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockAuthentication) Do(arg0 context.Context, arg1 usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*usecases.AuthenticationOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockAuthenticationMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockAuthentication)(nil).Do), arg0, arg1)
}