forked from mystiq/dex
*: 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
|
// 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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue