*: add tests for the RefreshConnector
This commit is contained in:
parent
952e0f81f5
commit
55e97d90a6
2 changed files with 102 additions and 23 deletions
|
@ -15,20 +15,32 @@ import (
|
|||
// NewCallbackConnector returns a mock connector which requires no user interaction. It always returns
|
||||
// the same (fake) identity.
|
||||
func NewCallbackConnector() connector.Connector {
|
||||
return callbackConnector{}
|
||||
return &Callback{
|
||||
Identity: connector.Identity{
|
||||
UserID: "0-385-28089-0",
|
||||
Username: "Kilgore Trout",
|
||||
Email: "kilgore@kilgore.trout",
|
||||
EmailVerified: true,
|
||||
Groups: []string{"authors"},
|
||||
ConnectorData: connectorData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ connector.CallbackConnector = callbackConnector{}
|
||||
_ connector.CallbackConnector = &Callback{}
|
||||
|
||||
_ connector.PasswordConnector = passwordConnector{}
|
||||
)
|
||||
|
||||
type callbackConnector struct{}
|
||||
// Callback is a connector that requires no user interaction and always returns the same identity.
|
||||
type Callback struct {
|
||||
// The returned identity.
|
||||
Identity connector.Identity
|
||||
}
|
||||
|
||||
func (m callbackConnector) Close() error { return nil }
|
||||
|
||||
func (m callbackConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
|
||||
// LoginURL returns the URL to redirect the user to login with.
|
||||
func (m *Callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
|
||||
u, err := url.Parse(callbackURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err)
|
||||
|
@ -41,20 +53,14 @@ func (m callbackConnector) LoginURL(s connector.Scopes, callbackURL, state strin
|
|||
|
||||
var connectorData = []byte("foobar")
|
||||
|
||||
func (m callbackConnector) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) {
|
||||
var groups []string
|
||||
if s.Groups {
|
||||
groups = []string{"authors"}
|
||||
}
|
||||
// HandleCallback parses the request and returns the user's identity
|
||||
func (m *Callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) {
|
||||
return m.Identity, nil
|
||||
}
|
||||
|
||||
return connector.Identity{
|
||||
UserID: "0-385-28089-0",
|
||||
Username: "Kilgore Trout",
|
||||
Email: "kilgore@kilgore.trout",
|
||||
EmailVerified: true,
|
||||
Groups: groups,
|
||||
ConnectorData: connectorData,
|
||||
}, nil
|
||||
// Refresh updates the identity during a refresh token request.
|
||||
func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
|
||||
return m.Identity, nil
|
||||
}
|
||||
|
||||
// CallbackConfig holds the configuration parameters for a connector which requires no interaction.
|
||||
|
|
|
@ -132,10 +132,13 @@ func TestDiscovery(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestOAuth2CodeFlow runs integration tests against a test server. The tests stand up a server
|
||||
// which requires no interaction to login, logs in through a test client, then passes the client
|
||||
// and returned token to the test.
|
||||
func TestOAuth2CodeFlow(t *testing.T) {
|
||||
clientID := "testclient"
|
||||
clientSecret := "testclientsecret"
|
||||
requestedScopes := []string{oidc.ScopeOpenID, "email", "offline_access"}
|
||||
requestedScopes := []string{oidc.ScopeOpenID, "email", "profile", "groups", "offline_access"}
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
|
@ -149,8 +152,14 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||
// so tests can compute the expected "expires_in" field.
|
||||
idTokensValidFor := time.Second * 30
|
||||
|
||||
// Connector used by the tests.
|
||||
var conn *mock.Callback
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
// If specified these set of scopes will be used during the test case.
|
||||
scopes []string
|
||||
// handleToken provides the OAuth2 token response for the integration test.
|
||||
handleToken func(context.Context, *oidc.Provider, *oauth2.Config, *oauth2.Token) error
|
||||
}{
|
||||
{
|
||||
|
@ -266,6 +275,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "refresh with unauthorized scopes",
|
||||
scopes: []string{"openid", "email"},
|
||||
handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error {
|
||||
v := url.Values{}
|
||||
v.Add("client_id", clientID)
|
||||
|
@ -273,7 +283,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||
v.Add("grant_type", "refresh_token")
|
||||
v.Add("refresh_token", token.RefreshToken)
|
||||
// Request a scope that wasn't requestd initially.
|
||||
v.Add("scope", strings.Join(append(requestedScopes, "profile"), " "))
|
||||
v.Add("scope", "oidc email profile")
|
||||
resp, err := http.PostForm(p.TokenURL, v)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -289,6 +299,57 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// This test ensures that the connector.RefreshConnector interface is being
|
||||
// used when clients request a refresh token.
|
||||
name: "refresh with identity changes",
|
||||
handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error {
|
||||
// have to use time.Now because the OAuth2 package uses it.
|
||||
token.Expiry = time.Now().Add(time.Second * -10)
|
||||
if token.Valid() {
|
||||
return errors.New("token shouldn't be valid")
|
||||
}
|
||||
|
||||
ident := connector.Identity{
|
||||
UserID: "fooid",
|
||||
Username: "foo",
|
||||
Email: "foo@bar.com",
|
||||
EmailVerified: true,
|
||||
Groups: []string{"foo", "bar"},
|
||||
}
|
||||
conn.Identity = ident
|
||||
|
||||
type claims struct {
|
||||
Username string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
want := claims{ident.Username, ident.Email, ident.EmailVerified, ident.Groups}
|
||||
|
||||
newToken, err := config.TokenSource(ctx, token).Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to refresh token: %v", err)
|
||||
}
|
||||
rawIDToken, ok := newToken.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("no id_token in refreshed token")
|
||||
}
|
||||
idToken, err := p.NewVerifier(ctx).Verify(rawIDToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify id token: %v", err)
|
||||
}
|
||||
var got claims
|
||||
if err := idToken.Claims(&got); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal claims: %v", err)
|
||||
}
|
||||
|
||||
if diff := pretty.Compare(want, got); diff != "" {
|
||||
return fmt.Errorf("got identity != want identity: %s", diff)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
@ -300,6 +361,15 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||
c.Issuer = c.Issuer + "/non-root-path"
|
||||
c.Now = now
|
||||
c.IDTokensValidFor = idTokensValidFor
|
||||
// Create a new mock callback connector for each test case.
|
||||
conn = mock.NewCallbackConnector().(*mock.Callback)
|
||||
c.Connectors = []Connector{
|
||||
{
|
||||
ID: "mock",
|
||||
DisplayName: "mock",
|
||||
Connector: conn,
|
||||
},
|
||||
}
|
||||
})
|
||||
defer httpServer.Close()
|
||||
|
||||
|
@ -375,6 +445,9 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||
Scopes: requestedScopes,
|
||||
RedirectURL: redirectURL,
|
||||
}
|
||||
if len(tc.scopes) != 0 {
|
||||
oauth2Config.Scopes = tc.scopes
|
||||
}
|
||||
|
||||
resp, err := http.Get(oauth2Server.URL + "/login")
|
||||
if err != nil {
|
||||
|
|
Reference in a new issue