dex/server/cross_client_test.go
Bobby Rullo 75473b4cba refresh tokens: grant claims based on scopes
Before,  this logic was only in the OIDCServer.CodeToken() method; now it has been
pulled out so that other paths, like OIDCServer.RefreshToken() can use
it.

The net affect, is that now refresh tokens can be used to get
cross-client authenticated ID Tokens.
2016-06-14 14:14:36 -07:00

333 lines
7.8 KiB
Go

package server
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"strings"
"testing"
"github.com/coreos/go-oidc/oidc"
"github.com/kylelemons/godebug/pretty"
"github.com/coreos/dex/client"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/scope"
)
func makeCrossClientTestFixtures() (*testFixtures, error) {
xClients := []client.LoadableClient{}
for _, cliData := range []struct {
id string
trustedPeers []string
}{
{
id: "client_a",
}, {
id: "client_b",
trustedPeers: []string{"client_a"},
}, {
id: "client_c",
trustedPeers: []string{"client_a", "client_b"},
},
} {
u := url.URL{
Scheme: "https://",
Path: cliData.id,
Host: cliData.id,
}
xClients = append(xClients, client.LoadableClient{
Client: client.Client{
Credentials: oidc.ClientCredentials{
ID: cliData.id,
Secret: base64.URLEncoding.EncodeToString(
[]byte(cliData.id + "_secret")),
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{u},
},
},
TrustedPeers: cliData.trustedPeers,
})
}
xClients = append(xClients, testClients...)
f, err := makeTestFixturesWithOptions(testFixtureOptions{
clients: xClients,
})
if err != nil {
return nil, fmt.Errorf("couldn't make test fixtures: %v", err)
}
return f, nil
}
func TestServerCrossClientAuthAllowed(t *testing.T) {
f, err := makeCrossClientTestFixtures()
if err != nil {
t.Fatalf("couldn't make test fixtures: %v", err)
}
tests := []struct {
reqClient string
authClient string
wantAuthorized bool
wantErr bool
}{
{
reqClient: "client_b",
authClient: "client_a",
wantAuthorized: false,
wantErr: false,
},
{
reqClient: "client_a",
authClient: "client_b",
wantAuthorized: true,
wantErr: false,
},
{
reqClient: "client_a",
authClient: "client_c",
wantAuthorized: true,
wantErr: false,
},
{
reqClient: "client_c",
authClient: "client_b",
wantAuthorized: false,
wantErr: false,
},
{
reqClient: "client_c",
authClient: "nope",
wantErr: false,
},
}
for i, tt := range tests {
got, err := f.srv.CrossClientAuthAllowed(tt.reqClient, tt.authClient)
if tt.wantErr {
if err == nil {
t.Errorf("case %d: want non-nil err", i)
}
continue
}
if err != nil {
t.Errorf("case %d: unexpected err %v: ", i, err)
}
if got != tt.wantAuthorized {
t.Errorf("case %d: want=%v, got=%v", i, tt.wantAuthorized, got)
}
}
}
func TestHandleAuthCrossClient(t *testing.T) {
f, err := makeCrossClientTestFixtures()
if err != nil {
t.Fatalf("couldn't make test fixtures: %v", err)
}
tests := []struct {
scopes []string
clientID string
wantCode int
}{
{
scopes: []string{scope.ScopeGoogleCrossClient + "client_a"},
clientID: "client_b",
wantCode: http.StatusBadRequest,
},
{
scopes: []string{scope.ScopeGoogleCrossClient + "client_b"},
clientID: "client_a",
wantCode: http.StatusFound,
},
{
scopes: []string{scope.ScopeGoogleCrossClient + "client_b"},
clientID: "client_a",
wantCode: http.StatusFound,
},
{
scopes: []string{scope.ScopeGoogleCrossClient + "client_c"},
clientID: "client_a",
wantCode: http.StatusFound,
},
{
// Two clients that client_a is authorized to mint tokens for.
scopes: []string{
scope.ScopeGoogleCrossClient + "client_c",
scope.ScopeGoogleCrossClient + "client_b",
},
clientID: "client_a",
wantCode: http.StatusFound,
},
{
// Two clients that client_a is authorized to mint tokens for.
scopes: []string{
scope.ScopeGoogleCrossClient + "client_c",
scope.ScopeGoogleCrossClient + "client_a",
},
clientID: "client_b",
wantCode: http.StatusBadRequest,
},
}
idpcs := []connector.Connector{
&fakeConnector{loginURL: "http://fake.example.com"},
}
for i, tt := range tests {
hdlr := handleAuthFunc(f.srv, idpcs, nil, true)
w := httptest.NewRecorder()
query := url.Values{
"response_type": []string{"code"},
"client_id": []string{tt.clientID},
"connector_id": []string{"fake"},
"scope": []string{strings.Join(append([]string{"openid"}, tt.scopes...), " ")},
}
u := fmt.Sprintf("http://server.example.com?%s", query.Encode())
req, err := http.NewRequest("GET", u, nil)
if err != nil {
t.Errorf("case %d: unable to form HTTP request: %v", i, err)
continue
}
hdlr.ServeHTTP(w, req)
if tt.wantCode != w.Code {
t.Errorf("case %d: HTTP code mismatch: want=%d got=%d", i, tt.wantCode, w.Code)
continue
}
}
}
func TestServerCodeTokenCrossClient(t *testing.T) {
f, err := makeCrossClientTestFixtures()
if err != nil {
t.Fatalf("Error creating test fixtures: %v", err)
}
sm := f.sessionManager
tests := []struct {
clientID string
offline bool
refreshToken string
crossClients []string
wantErr bool
wantAUD []string
wantAZP string
}{
// First test the non-cross-client cases, make sure they're undisturbed:
{
// No 'offline_access' in scope, should get empty refresh token.
clientID: testClientID,
refreshToken: "",
wantAUD: []string{testClientID},
},
{
// Have 'offline_access' in scope, should get non-empty refresh token.
clientID: testClientID,
offline: true,
refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))),
wantAUD: []string{testClientID},
},
// Now test cross-client cases:
{
clientID: "client_a",
crossClients: []string{"client_b"},
wantAUD: []string{"client_b"},
wantAZP: "client_a",
},
{
clientID: "client_a",
crossClients: []string{"client_b", "client_a"},
wantAUD: []string{"client_a", "client_b"},
wantAZP: "client_a",
},
}
for i, tt := range tests {
scopes := []string{"openid"}
if tt.offline {
scopes = append(scopes, "offline_access")
}
for _, client := range tt.crossClients {
scopes = append(scopes, scope.ScopeGoogleCrossClient+client)
}
sessionID, err := sm.NewSession("bogus_idpc", tt.clientID, "bogus", url.URL{}, "", false, scopes)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
_, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{})
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
_, err = sm.AttachUser(sessionID, "ID-1")
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
key, err := sm.NewSessionKey(sessionID)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
jwt, token, err := f.srv.CodeToken(f.clientCreds[tt.clientID], key)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
if jwt == nil {
t.Fatalf("case %d: expect non-nil jwt", i)
}
if token != tt.refreshToken {
t.Errorf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token)
}
claims, err := jwt.Claims()
if err != nil {
t.Fatalf("case %d: unexpected error getting claims: %v", i, err)
}
var gotAUD []string
if len(tt.wantAUD) < 2 {
aud, _, err := claims.StringClaim("aud")
if err != nil {
t.Fatalf("case %d: unexpected error getting 'aud': %q: raw: %v", i, err, claims["aud"])
}
gotAUD = []string{aud}
} else {
gotAUD, _, err = claims.StringsClaim("aud")
if err != nil {
t.Fatalf("case %d: unexpected error getting 'aud': %v", i, err)
}
}
sort.Strings(gotAUD)
if diff := pretty.Compare(tt.wantAUD, gotAUD); diff != "" {
t.Fatalf("case %d: pretty.Compare(tt.wantAUD, gotAUD): %v", i, diff)
}
gotAZP, _, err := claims.StringClaim("azp")
if err != nil {
if err != nil {
t.Fatalf("case %d: unexpected error getting 'aud': %v", i, err)
}
}
if gotAZP != tt.wantAZP {
t.Errorf("case %d: wantAZP=%v, gotAZP=%v", i, tt.wantAZP, gotAZP)
}
}
}