Add --grant-type option and username prompt for ROPC (#178)

This commit is contained in:
Hidetake Iwata
2019-10-31 00:36:40 +09:00
committed by GitHub
parent 5a71247214
commit 0c582e97ad
10 changed files with 327 additions and 206 deletions

View File

@@ -85,13 +85,14 @@ Flags:
--oidc-client-id string Client ID of the provider (mandatory)
--oidc-client-secret string Client secret of the provider
--oidc-extra-scope strings Scopes to request to the provider
--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
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
--grant-type string The authorization grant type to use. One of (auto|authcode|password) (default "auto")
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--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
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
-h, --help help for get-token
Global Flags:
@@ -155,21 +156,39 @@ You can change the ports by the option:
#### Resource owner password credentials grant flow
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.
Kubelogin performs the resource owner password credentials grant flow
when `--grant-type=password` or `--username` is set.
You can pass the username and password:
Note that most OIDC providers do not support this flow.
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
You can set the username and password.
```yaml
- --username USERNAME
- --password PASSWORD
```
If the password is not set, kubelogin will show the prompt.
If the password is not set, kubelogin will show the prompt for the password.
```yaml
- --username USERNAME
```
```
% kubelogin --username USER
% kubectl get pods
Password:
```
If the username is not set, kubelogin will show the prompt for the username and password.
```yaml
- --grant-type=password
```
```
% kubectl get pods
Username: foo
Password:
```

View File

@@ -102,12 +102,13 @@ Flags:
--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
--grant-type string The authorization grant type to use. One of (auto|authcode|password) (default "auto")
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--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
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)

View File

@@ -18,100 +18,96 @@ func TestCmd_Run(t *testing.T) {
const version = "HEAD"
t.Run("root", func(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
tests := map[string]struct {
args []string
in standalone.Input
}{
"Defaults": {
args: []string{executable},
in: standalone.Input{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
},
})
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("AuthCodeOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
})
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
},
},
"GrantType=password": {
args: []string{executable,
"--grant-type", "password",
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
})
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
},
"GrantType=auto": {
args: []string{executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
@@ -131,85 +127,44 @@ func TestCmd_Run(t *testing.T) {
})
t.Run("get-token", func(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
tests := map[string]struct {
args []string
in credentialplugin.Input
}{
"Defaults": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
},
})
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
"FullOptions": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("AuthCodeOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
})
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, credentialplugin.Input{
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
@@ -217,41 +172,78 @@ func TestCmd_Run(t *testing.T) {
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
},
},
},
"GrantType=password": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "password",
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
})
cmd := Cmd{
Root: &Root{
},
},
"GrantType=auto": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-port", "10080",
"--listen-port", "20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)

View File

@@ -57,7 +57,10 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
return nil
},
RunE: func(*cobra.Command, []string) error {
authCodeOption, ropcOption := o.authenticationOptions.toUseCaseOptions()
authCodeOption, ropcOption, err := o.authenticationOptions.toUseCaseOptions()
if err != nil {
return xerrors.Errorf("error: %w", err)
}
in := credentialplugin.Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,

View File

@@ -43,6 +43,7 @@ func (o *rootOptions) register(f *pflag.FlagSet) {
}
type authenticationOptions struct {
GrantType string
ListenPort []int
SkipOpenBrowser bool
Username string
@@ -50,26 +51,27 @@ type authenticationOptions struct {
}
func (o *authenticationOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", "The authorization grant type to use. One of (auto|authcode|password)")
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
func (o *authenticationOptions) toUseCaseOptions() (authCodeOption *authentication.AuthCodeOption, ropcOption *authentication.ROPCOption) {
switch {
case o.Username != "":
ropcOption = &authentication.ROPCOption{
Username: o.Username,
Password: o.Password,
}
default:
authCodeOption = &authentication.AuthCodeOption{
func (o *authenticationOptions) toUseCaseOptions() (*authentication.AuthCodeOption, *authentication.ROPCOption, error) {
if o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == "") {
return &authentication.AuthCodeOption{
BindAddress: translateListenPortToBindAddress(o.ListenPort),
SkipOpenBrowser: o.SkipOpenBrowser,
}
}, nil, nil
}
return
if o.GrantType == "password" || (o.GrantType == "auto" && o.Username != "") {
return nil, &authentication.ROPCOption{
Username: o.Username,
Password: o.Password,
}, nil
}
return nil, nil, xerrors.Errorf("grant-type must be one of (auto|authcode|password)")
}
type Root struct {
@@ -85,7 +87,10 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
Long: longDescription,
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
authCodeOption, ropcOption := o.authenticationOptions.toUseCaseOptions()
authCodeOption, ropcOption, err := o.authenticationOptions.toUseCaseOptions()
if err != nil {
return xerrors.Errorf("invalid option: %w", err)
}
in := standalone.Input{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),

View File

@@ -42,7 +42,10 @@ func (cmd *Setup) New(ctx context.Context) *cobra.Command {
Short: "Show the setup instruction",
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
authCodeOption, ropcOption := o.authenticationOptions.toUseCaseOptions()
authCodeOption, ropcOption, err := o.authenticationOptions.toUseCaseOptions()
if err != nil {
return xerrors.Errorf("error: %w", err)
}
in := setup.Stage2Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,

View File

@@ -1,8 +1,11 @@
// Package env provides environment dependent facilities.
package env
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"github.com/google/wire"
@@ -27,6 +30,7 @@ var Set = wire.NewSet(
)
type Interface interface {
ReadString(prompt string) (string, error)
ReadPassword(prompt string) (string, error)
OpenBrowser(url string) error
}
@@ -34,6 +38,20 @@ type Interface interface {
// Env provides environment specific facilities.
type Env struct{}
// ReadString reads a string from the stdin.
func (*Env) ReadString(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("could not write the prompt: %w", err)
}
r := bufio.NewReader(os.Stdin)
s, err := r.ReadString('\n')
if err != nil {
return "", xerrors.Errorf("could not read from stdin: %w", err)
}
s = strings.TrimRight(s, "\r\n")
return s, nil
}
// ReadPassword reads a password from the stdin without echo back.
func (*Env) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
@@ -41,7 +59,7 @@ func (*Env) ReadPassword(prompt string) (string, error) {
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", xerrors.Errorf("could not read: %w", err)
return "", xerrors.Errorf("could not read from stdin: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", xerrors.Errorf("could not write a new line: %w", err)

View File

@@ -34,6 +34,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
// OpenBrowser mocks base method
func (m *MockInterface) OpenBrowser(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
ret0, _ := ret[0].(error)
return ret0
@@ -41,11 +42,13 @@ func (m *MockInterface) OpenBrowser(arg0 string) error {
// OpenBrowser indicates an expected call of OpenBrowser
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
}
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
@@ -54,5 +57,21 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}
// ReadString mocks base method
func (m *MockInterface) ReadString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadString", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadString indicates an expected call of ReadString
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)
}

View File

@@ -67,6 +67,7 @@ type Output struct {
RefreshToken string
}
const usernamePrompt = "Username: "
const passwordPrompt = "Password: "
// Authentication provides the internal use-case of authentication.
@@ -200,6 +201,13 @@ func (u *Authentication) doAuthCodeFlow(ctx context.Context, in *AuthCodeOption,
func (u *Authentication) doPasswordCredentialsFlow(ctx context.Context, in *ROPCOption, client oidcclient.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
if in.Username == "" {
var err error
in.Username, err = u.Env.ReadString(usernamePrompt)
if err != nil {
return nil, xerrors.Errorf("could not get the username: %w", err)
}
}
if in.Password == "" {
var err error
in.Password, err = u.Env.ReadPassword(passwordPrompt)

View File

@@ -141,6 +141,59 @@ func TestAuthentication_Do(t *testing.T) {
}
})
t.Run("ResourceOwnerPasswordCredentialsFlow/AskUsernameAndPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
ROPCOption: &ROPCOption{},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(gomock.Any(), "USER", "PASS").
Return(&oidcclient.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClientFactory := mock_oidcclient.NewMockFactoryInterface(ctrl)
mockOIDCClientFactory.EXPECT().
New(ctx, oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(mockOIDCClient, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadString(usernamePrompt).Return("USER", nil)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Authentication{
OIDCClientFactory: mockOIDCClientFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenSubject: "YOUR_SUBJECT",
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()