diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 0ad9998d..d6f56f38 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -10,15 +10,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" - "go.pinniped.dev/internal/oidcclient/login" + "go.pinniped.dev/internal/oidcclient" ) //nolint: gochecknoinits func init() { - loginCmd.AddCommand(oidcLoginCommand(login.Run)) + loginCmd.AddCommand(oidcLoginCommand(oidcclient.Login)) } -func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...login.Option) (*login.Token, error)) *cobra.Command { +func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oidcclient.Option) (*oidcclient.Token, error)) *cobra.Command { var ( cmd = cobra.Command{ Args: cobra.NoArgs, @@ -40,18 +40,18 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...log mustMarkRequired(&cmd, "issuer", "client-id") cmd.RunE = func(cmd *cobra.Command, args []string) error { - opts := []login.Option{ - login.WithContext(cmd.Context()), - login.WithScopes(scopes), + opts := []oidcclient.Option{ + oidcclient.WithContext(cmd.Context()), + oidcclient.WithScopes(scopes), } if listenPort != 0 { - opts = append(opts, login.WithListenPort(listenPort)) + opts = append(opts, oidcclient.WithListenPort(listenPort)) } // --skip-browser replaces the default "browser open" function with one that prints to stderr. if skipBrowser { - opts = append(opts, login.WithBrowserOpen(func(url string) error { + opts = append(opts, oidcclient.WithBrowserOpen(func(url string) error { cmd.PrintErr("Please log in: ", url, "\n") return nil })) @@ -69,8 +69,8 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...log APIVersion: "client.authentication.k8s.io/v1beta1", }, Status: &clientauthenticationv1beta1.ExecCredentialStatus{ - ExpirationTimestamp: &metav1.Time{Time: tok.IDTokenExpiry}, - Token: tok.IDToken, + ExpirationTimestamp: &tok.IDToken.Expiry, + Token: tok.IDToken.Token, }, }) } diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index b2bd9348..5f4cfd5b 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -9,9 +9,10 @@ import ( "time" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidcclient/login" + "go.pinniped.dev/internal/oidcclient" ) func TestLoginOIDCCommand(t *testing.T) { @@ -87,13 +88,18 @@ func TestLoginOIDCCommand(t *testing.T) { var ( gotIssuer string gotClientID string - gotOptions []login.Option + gotOptions []oidcclient.Option ) - cmd := oidcLoginCommand(func(issuer string, clientID string, opts ...login.Option) (*login.Token, error) { + cmd := oidcLoginCommand(func(issuer string, clientID string, opts ...oidcclient.Option) (*oidcclient.Token, error) { gotIssuer = issuer gotClientID = clientID gotOptions = opts - return &login.Token{IDToken: "test-id-token", IDTokenExpiry: time1}, nil + return &oidcclient.Token{ + IDToken: &oidcclient.IDToken{ + Token: "test-id-token", + Expiry: metav1.NewTime(time1), + }, + }, nil }) require.NotNil(t, cmd) diff --git a/internal/oidcclient/login/login.go b/internal/oidcclient/login.go similarity index 92% rename from internal/oidcclient/login/login.go rename to internal/oidcclient/login.go index 9f7df3da..2de838df 100644 --- a/internal/oidcclient/login/login.go +++ b/internal/oidcclient/login.go @@ -1,8 +1,8 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package login implements a CLI OIDC login flow. -package login +// Package oidcclient implements a CLI OIDC login flow. +package oidcclient import ( "context" @@ -15,6 +15,7 @@ import ( "github.com/coreos/go-oidc" "github.com/pkg/browser" "golang.org/x/oauth2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" @@ -55,13 +56,7 @@ type callbackResult struct { err error } -type Token struct { - *oauth2.Token - IDToken string `json:"id_token"` - IDTokenExpiry time.Time `json:"id_token_expiry"` -} - -// Option is an optional configuration for Run(). +// Option is an optional configuration for Login(). type Option func(*handlerState) error // WithContext specifies a specific context.Context under which to perform the login. If this option is not specified, @@ -105,8 +100,8 @@ func WithBrowserOpen(openURL func(url string) error) Option { } } -// Run an OAuth2/OIDC authorization code login using a localhost listener. -func Run(issuer string, clientID string, opts ...Option) (*Token, error) { +// Login performs an OAuth2/OIDC authorization code login using a localhost listener. +func Login(issuer string, clientID string, opts ...Option) (*Token, error) { h := handlerState{ issuer: issuer, clientID: clientID, @@ -262,9 +257,18 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req } h.callbacks <- callbackResult{token: &Token{ - Token: oauth2Tok, - IDToken: idTok, - IDTokenExpiry: validated.Expiry, + AccessToken: &AccessToken{ + Token: oauth2Tok.AccessToken, + Type: oauth2Tok.TokenType, + Expiry: metav1.NewTime(oauth2Tok.Expiry), + }, + RefreshToken: &RefreshToken{ + Token: oauth2Tok.RefreshToken, + }, + IDToken: &IDToken{ + Token: idTok, + Expiry: metav1.NewTime(validated.Expiry), + }, }} _, _ = w.Write([]byte("you have been logged in and may now close this tab")) return nil diff --git a/internal/oidcclient/login/login_test.go b/internal/oidcclient/login_test.go similarity index 96% rename from internal/oidcclient/login/login_test.go rename to internal/oidcclient/login_test.go index 4050ebf2..e01b4435 100644 --- a/internal/oidcclient/login/login_test.go +++ b/internal/oidcclient/login_test.go @@ -1,7 +1,7 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package login +package oidcclient import ( "context" @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/mocks/mockkeyset" @@ -26,18 +27,21 @@ import ( "go.pinniped.dev/internal/oidcclient/state" ) -func TestRun(t *testing.T) { +func TestLogin(t *testing.T) { time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC) testToken := Token{ - Token: &oauth2.Token{ - AccessToken: "test-access-token", - RefreshToken: "test-refresh-token", - Expiry: time1.Add(1 * time.Minute), + AccessToken: &AccessToken{ + Token: "test-access-token", + Expiry: metav1.NewTime(time1.Add(1 * time.Minute)), + }, + RefreshToken: &RefreshToken{ + Token: "test-refresh-token", + }, + IDToken: &IDToken{ + Token: "test-id-token", + Expiry: metav1.NewTime(time1.Add(2 * time.Minute)), }, - IDToken: "test-id-token", - IDTokenExpiry: time1.Add(2 * time.Minute), } - _ = testToken // Start a test server that returns 500 errors errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -223,7 +227,7 @@ func TestRun(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - tok, err := Run(tt.issuer, tt.clientID, + tok, err := Login(tt.issuer, tt.clientID, WithContext(context.Background()), WithListenPort(0), WithScopes([]string{"test-scope"}), @@ -393,7 +397,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { } require.NoError(t, result.err) require.NotNil(t, result.token) - require.Equal(t, result.token.IDToken, tt.returnIDTok) + require.Equal(t, result.token.IDToken.Token, tt.returnIDTok) } }) } diff --git a/internal/oidcclient/types.go b/internal/oidcclient/types.go new file mode 100644 index 00000000..a4a0f419 --- /dev/null +++ b/internal/oidcclient/types.go @@ -0,0 +1,49 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidcclient + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AccessToken is an OAuth2 access token. +type AccessToken struct { + // Token is the token that authorizes and authenticates the requests. + Token string `json:"token"` + + // Type is the type of token. + Type string `json:"type,omitempty"` + + // Expiry is the optional expiration time of the access token. + Expiry metav1.Time `json:"expiryTimestamp,omitempty"` +} + +// RefreshToken is an OAuth2 refresh token. +type RefreshToken struct { + // Token is a token that's used by the application (as opposed to the user) to refresh the access token if it expires. + Token string `json:"token"` +} + +// IDToken is an OpenID Connect ID token. +type IDToken struct { + // Token is an OpenID Connect ID token. + Token string `json:"token"` + + // Expiry is the optional expiration time of the ID token. + Expiry metav1.Time `json:"expiryTimestamp,omitempty"` +} + +// Token contains the elements of an OIDC session. +type Token struct { + // AccessToken is the token that authorizes and authenticates the requests. + AccessToken *AccessToken `json:"access,omitempty"` + + // RefreshToken is a token that's used by the application + // (as opposed to the user) to refresh the access token + // if it expires. + RefreshToken *RefreshToken `json:"refresh,omitempty"` + + // IDToken is an OpenID Connect ID token. + IDToken *IDToken `json:"id,omitempty"` +}