*: add tests for the RefreshConnector

This commit is contained in:
Eric Chiang 2016-11-21 12:16:36 -08:00
parent 952e0f81f5
commit 55e97d90a6
2 changed files with 102 additions and 23 deletions

View file

@ -15,20 +15,32 @@ import (
// NewCallbackConnector returns a mock connector which requires no user interaction. It always returns // NewCallbackConnector returns a mock connector which requires no user interaction. It always returns
// the same (fake) identity. // the same (fake) identity.
func NewCallbackConnector() connector.Connector { 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 ( var (
_ connector.CallbackConnector = callbackConnector{} _ connector.CallbackConnector = &Callback{}
_ connector.PasswordConnector = passwordConnector{} _ 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 } // LoginURL returns the URL to redirect the user to login with.
func (m *Callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
func (m callbackConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
u, err := url.Parse(callbackURL) u, err := url.Parse(callbackURL)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) 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") var connectorData = []byte("foobar")
func (m callbackConnector) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) { // HandleCallback parses the request and returns the user's identity
var groups []string func (m *Callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) {
if s.Groups { return m.Identity, nil
groups = []string{"authors"} }
}
return connector.Identity{ // Refresh updates the identity during a refresh token request.
UserID: "0-385-28089-0", func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
Username: "Kilgore Trout", return m.Identity, nil
Email: "kilgore@kilgore.trout",
EmailVerified: true,
Groups: groups,
ConnectorData: connectorData,
}, nil
} }
// CallbackConfig holds the configuration parameters for a connector which requires no interaction. // CallbackConfig holds the configuration parameters for a connector which requires no interaction.

View file

@ -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) { func TestOAuth2CodeFlow(t *testing.T) {
clientID := "testclient" clientID := "testclient"
clientSecret := "testclientsecret" clientSecret := "testclientsecret"
requestedScopes := []string{oidc.ScopeOpenID, "email", "offline_access"} requestedScopes := []string{oidc.ScopeOpenID, "email", "profile", "groups", "offline_access"}
t0 := time.Now() t0 := time.Now()
@ -149,8 +152,14 @@ func TestOAuth2CodeFlow(t *testing.T) {
// so tests can compute the expected "expires_in" field. // so tests can compute the expected "expires_in" field.
idTokensValidFor := time.Second * 30 idTokensValidFor := time.Second * 30
// Connector used by the tests.
var conn *mock.Callback
tests := []struct { tests := []struct {
name string 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 handleToken func(context.Context, *oidc.Provider, *oauth2.Config, *oauth2.Token) error
}{ }{
{ {
@ -265,7 +274,8 @@ func TestOAuth2CodeFlow(t *testing.T) {
}, },
}, },
{ {
name: "refresh with unauthorized scopes", name: "refresh with unauthorized scopes",
scopes: []string{"openid", "email"},
handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error { handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error {
v := url.Values{} v := url.Values{}
v.Add("client_id", clientID) v.Add("client_id", clientID)
@ -273,7 +283,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
v.Add("grant_type", "refresh_token") v.Add("grant_type", "refresh_token")
v.Add("refresh_token", token.RefreshToken) v.Add("refresh_token", token.RefreshToken)
// Request a scope that wasn't requestd initially. // 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) resp, err := http.PostForm(p.TokenURL, v)
if err != nil { if err != nil {
return err return err
@ -289,6 +299,57 @@ func TestOAuth2CodeFlow(t *testing.T) {
return nil 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 { for _, tc := range tests {
@ -300,6 +361,15 @@ func TestOAuth2CodeFlow(t *testing.T) {
c.Issuer = c.Issuer + "/non-root-path" c.Issuer = c.Issuer + "/non-root-path"
c.Now = now c.Now = now
c.IDTokensValidFor = idTokensValidFor 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() defer httpServer.Close()
@ -375,6 +445,9 @@ func TestOAuth2CodeFlow(t *testing.T) {
Scopes: requestedScopes, Scopes: requestedScopes,
RedirectURL: redirectURL, RedirectURL: redirectURL,
} }
if len(tc.scopes) != 0 {
oauth2Config.Scopes = tc.scopes
}
resp, err := http.Get(oauth2Server.URL + "/login") resp, err := http.Get(oauth2Server.URL + "/login")
if err != nil { if err != nil {