Merge pull request #426 from bobbyrullo/cross_client_2
Cross client work
This commit is contained in:
commit
a9d854e144
33 changed files with 936 additions and 201 deletions
|
@ -141,7 +141,10 @@ func (a *AdminAPI) CreateClient(req adminschema.ClientCreateRequest) (adminschem
|
||||||
}
|
}
|
||||||
|
|
||||||
// metadata is guaranteed to have at least one redirect_uri by earlier validation.
|
// metadata is guaranteed to have at least one redirect_uri by earlier validation.
|
||||||
creds, err := a.clientManager.New(cli)
|
creds, err := a.clientManager.New(cli, &clientmanager.ClientOptions{
|
||||||
|
TrustedPeers: req.Client.TrustedPeers,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return adminschema.ClientCreateResponse{}, mapError(err)
|
return adminschema.ClientCreateResponse{}, mapError(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ErrorInvalidClientID = errors.New("not a valid client ID")
|
||||||
ErrorInvalidRedirectURL = errors.New("not a valid redirect url for the given client")
|
ErrorInvalidRedirectURL = errors.New("not a valid redirect url for the given client")
|
||||||
ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many")
|
ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many")
|
||||||
ErrorNoValidRedirectURLs = errors.New("no valid redirect URLs for this client.")
|
ErrorNoValidRedirectURLs = errors.New("no valid redirect URLs for this client.")
|
||||||
|
@ -60,6 +61,12 @@ type ClientRepo interface {
|
||||||
New(tx repo.Transaction, client Client) (*oidc.ClientCredentials, error)
|
New(tx repo.Transaction, client Client) (*oidc.ClientCredentials, error)
|
||||||
|
|
||||||
Update(tx repo.Transaction, client Client) error
|
Update(tx repo.Transaction, client Client) error
|
||||||
|
|
||||||
|
// GetTrustedPeers returns the list of clients authorized to mint ID token for the given client.
|
||||||
|
GetTrustedPeers(tx repo.Transaction, clientID string) ([]string, error)
|
||||||
|
|
||||||
|
// SetTrustedPeers sets the list of clients authorized to mint ID token for the given client.
|
||||||
|
SetTrustedPeers(tx repo.Transaction, clientID string, clientIDs []string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidRedirectURL returns the passed in URL if it is present in the redirectURLs list, and returns an error otherwise.
|
// ValidRedirectURL returns the passed in URL if it is present in the redirectURLs list, and returns an error otherwise.
|
||||||
|
|
|
@ -21,6 +21,10 @@ const (
|
||||||
maxSecretLength = 72
|
maxSecretLength = 72
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ClientOptions struct {
|
||||||
|
TrustedPeers []string
|
||||||
|
}
|
||||||
|
|
||||||
type SecretGenerator func() ([]byte, error)
|
type SecretGenerator func() ([]byte, error)
|
||||||
|
|
||||||
func DefaultSecretGenerator() ([]byte, error) {
|
func DefaultSecretGenerator() ([]byte, error) {
|
||||||
|
@ -63,7 +67,7 @@ func NewClientManager(clientRepo client.ClientRepo, txnFactory repo.TransactionF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ClientManager) New(cli client.Client) (*oidc.ClientCredentials, error) {
|
func (m *ClientManager) New(cli client.Client, options *ClientOptions) (*oidc.ClientCredentials, error) {
|
||||||
tx, err := m.begin()
|
tx, err := m.begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -83,6 +87,13 @@ func (m *ClientManager) New(cli client.Client) (*oidc.ClientCredentials, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options != nil && len(options.TrustedPeers) > 0 {
|
||||||
|
err = m.clientRepo.SetTrustedPeers(tx, creds.ID, options.TrustedPeers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -132,7 +132,7 @@ func TestAuthenticate(t *testing.T) {
|
||||||
cli := client.Client{
|
cli := client.Client{
|
||||||
Metadata: cm,
|
Metadata: cm,
|
||||||
}
|
}
|
||||||
cc, err := f.mgr.New(cli)
|
cc, err := f.mgr.New(cli, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ func (d *dbDriver) NewClient(meta oidc.ClientMetadata) (*oidc.ClientCredentials,
|
||||||
cli := client.Client{
|
cli := client.Client{
|
||||||
Metadata: meta,
|
Metadata: meta,
|
||||||
}
|
}
|
||||||
return d.ciManager.New(cli)
|
return d.ciManager.New(cli, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbDriver) ConnectorConfigs() ([]connector.ConnectorConfig, error) {
|
func (d *dbDriver) ConnectorConfigs() ([]connector.ConnectorConfig, error) {
|
||||||
|
|
75
db/client.go
75
db/client.go
|
@ -16,7 +16,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
clientTableName = "client_identity"
|
clientTableName = "client_identity"
|
||||||
|
trustedPeerTableName = "trusted_peers"
|
||||||
|
|
||||||
// postgres error codes
|
// postgres error codes
|
||||||
pgErrorCodeUniqueViolation = "23505" // unique_violation
|
pgErrorCodeUniqueViolation = "23505" // unique_violation
|
||||||
|
@ -29,6 +30,13 @@ func init() {
|
||||||
autoinc: false,
|
autoinc: false,
|
||||||
pkey: []string{"id"},
|
pkey: []string{"id"},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
register(table{
|
||||||
|
name: trustedPeerTableName,
|
||||||
|
model: trustedPeerModel{},
|
||||||
|
autoinc: false,
|
||||||
|
pkey: []string{"client_id", "trusted_client_id"},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClientModel(cli client.Client) (*clientModel, error) {
|
func newClientModel(cli client.Client) (*clientModel, error) {
|
||||||
|
@ -58,6 +66,11 @@ type clientModel struct {
|
||||||
DexAdmin bool `db:"dex_admin"`
|
DexAdmin bool `db:"dex_admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type trustedPeerModel struct {
|
||||||
|
ClientID string `db:"client_id"`
|
||||||
|
TrustedClientID string `db:"trusted_client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
func (m *clientModel) Client() (*client.Client, error) {
|
func (m *clientModel) Client() (*client.Client, error) {
|
||||||
ci := client.Client{
|
ci := client.Client{
|
||||||
Credentials: oidc.ClientCredentials{
|
Credentials: oidc.ClientCredentials{
|
||||||
|
@ -254,3 +267,63 @@ func (r *clientRepo) update(tx repo.Transaction, cli client.Client) error {
|
||||||
_, err = ex.Update(cm)
|
_, err = ex.Update(cm)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *clientRepo) GetTrustedPeers(tx repo.Transaction, clientID string) ([]string, error) {
|
||||||
|
ex := r.executor(tx)
|
||||||
|
if clientID == "" {
|
||||||
|
return nil, client.ErrorInvalidClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
qt := r.quote(trustedPeerTableName)
|
||||||
|
var ids []string
|
||||||
|
_, err := ex.Select(&ids, fmt.Sprintf("SELECT trusted_client_id from %s where client_id = $1", qt), clientID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *clientRepo) SetTrustedPeers(tx repo.Transaction, clientID string, clientIDs []string) error {
|
||||||
|
ex := r.executor(tx)
|
||||||
|
qt := r.quote(trustedPeerTableName)
|
||||||
|
|
||||||
|
// First delete all existing rows
|
||||||
|
_, err := ex.Exec(fmt.Sprintf("DELETE from %s where client_id = $1", qt), clientID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the client exists.
|
||||||
|
_, err = r.get(tx, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that all the clients are valid
|
||||||
|
for _, curID := range clientIDs {
|
||||||
|
_, err := r.get(tx, curID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the clients
|
||||||
|
rows := []interface{}{}
|
||||||
|
for _, curID := range clientIDs {
|
||||||
|
rows = append(rows, &trustedPeerModel{
|
||||||
|
ClientID: clientID,
|
||||||
|
TrustedClientID: curID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = ex.Insert(rows...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-gorp/gorp"
|
"github.com/go-gorp/gorp"
|
||||||
"github.com/rubenv/sql-migrate"
|
migrate "github.com/rubenv/sql-migrate"
|
||||||
|
|
||||||
"github.com/coreos/dex/db/migrations"
|
"github.com/coreos/dex/db/migrations"
|
||||||
)
|
)
|
||||||
|
|
|
@ -70,4 +70,10 @@ CREATE TABLE session_key (
|
||||||
expires_at bigint,
|
expires_at bigint,
|
||||||
stale integer
|
stale integer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE trusted_peers (
|
||||||
|
client_id text NOT NULL,
|
||||||
|
trusted_client_id text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
5
db/migrations/0012_add_cross_client_authorizers.sql
Normal file
5
db/migrations/0012_add_cross_client_authorizers.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-- +migrate Up
|
||||||
|
CREATE TABLE IF NOT EXISTS "trusted_peers" (
|
||||||
|
"client_id" text not null,
|
||||||
|
"trusted_client_id" text not null,
|
||||||
|
primary key ("client_id", "trusted_client_id")) ;
|
|
@ -72,5 +72,11 @@ var PostgresMigrations migrate.MigrationSource = &migrate.MemoryMigrationSource{
|
||||||
"-- +migrate Up\n\n-- This migration is a fix for a bug that allowed duplicate emails if they used different cases (see #338).\n-- When migrating, dex will not take the liberty of deleting rows for duplicate cases. Instead it will\n-- raise an exception and call for an admin to remove duplicates manually.\n\nCREATE OR REPLACE FUNCTION raise_exp() RETURNS VOID AS $$\nBEGIN\n RAISE EXCEPTION 'Found duplicate emails when using case insensitive comparision, cannot perform migration.';\nEND;\n$$ LANGUAGE plpgsql;\n\nSELECT LOWER(email),\n COUNT(email),\n CASE\n WHEN COUNT(email) > 1 THEN raise_exp()\n ELSE NULL\n END\nFROM authd_user\nGROUP BY LOWER(email);\n\nUPDATE authd_user SET email = LOWER(email);\n",
|
"-- +migrate Up\n\n-- This migration is a fix for a bug that allowed duplicate emails if they used different cases (see #338).\n-- When migrating, dex will not take the liberty of deleting rows for duplicate cases. Instead it will\n-- raise an exception and call for an admin to remove duplicates manually.\n\nCREATE OR REPLACE FUNCTION raise_exp() RETURNS VOID AS $$\nBEGIN\n RAISE EXCEPTION 'Found duplicate emails when using case insensitive comparision, cannot perform migration.';\nEND;\n$$ LANGUAGE plpgsql;\n\nSELECT LOWER(email),\n COUNT(email),\n CASE\n WHEN COUNT(email) > 1 THEN raise_exp()\n ELSE NULL\n END\nFROM authd_user\nGROUP BY LOWER(email);\n\nUPDATE authd_user SET email = LOWER(email);\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "0012_add_cross_client_authorizers.sql",
|
||||||
|
Up: []string{
|
||||||
|
"-- +migrate Up\nCREATE TABLE IF NOT EXISTS \"trusted_peers\" (\n \"client_id\" text not null,\n \"trusted_client_id\" text not null,\n primary key (\"client_id\", \"trusted_client_id\")) ;\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,7 +248,7 @@ func (r *userRepo) GetRemoteIdentities(tx repo.Transaction, userID string) ([]us
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(rims) == 0 {
|
if len(rims) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -316,7 +316,7 @@ func TestDBClientRepoAuthenticate(t *testing.T) {
|
||||||
cli := client.Client{
|
cli := client.Client{
|
||||||
Metadata: cm,
|
Metadata: cm,
|
||||||
}
|
}
|
||||||
cc, err := m.New(cli)
|
cc, err := m.New(cli, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,9 @@ func makeAdminAPITestFixtures() *adminAPITestFixtures {
|
||||||
|
|
||||||
var cliCount int
|
var cliCount int
|
||||||
secGen := func() ([]byte, error) {
|
secGen := func() ([]byte, error) {
|
||||||
return []byte(fmt.Sprintf("client_%v", cliCount)), nil
|
id := []byte(fmt.Sprintf("client_%v", cliCount))
|
||||||
|
cliCount++
|
||||||
|
return id, nil
|
||||||
}
|
}
|
||||||
cr := db.NewClientRepo(dbMap)
|
cr := db.NewClientRepo(dbMap)
|
||||||
clientIDGenerator := func(hostport string) (string, error) {
|
clientIDGenerator := func(hostport string) (string, error) {
|
||||||
|
@ -379,6 +381,7 @@ func TestCreateClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
addIDAndSecret := func(cli adminschema.Client) *adminschema.Client {
|
addIDAndSecret := func(cli adminschema.Client) *adminschema.Client {
|
||||||
cli.Id = "client_auth.example.com"
|
cli.Id = "client_auth.example.com"
|
||||||
cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
|
cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
|
||||||
|
@ -404,16 +407,20 @@ func TestCreateClient(t *testing.T) {
|
||||||
|
|
||||||
adminMultiRedirect := adminClientGood
|
adminMultiRedirect := adminClientGood
|
||||||
adminMultiRedirect.RedirectURIs = []string{"https://auth.example.com/", "https://auth2.example.com/"}
|
adminMultiRedirect.RedirectURIs = []string{"https://auth.example.com/", "https://auth2.example.com/"}
|
||||||
clientMultiRedirect := clientGoodAdmin
|
clientMultiRedirect := clientGood
|
||||||
clientMultiRedirect.Metadata.RedirectURIs = append(
|
clientMultiRedirect.Metadata.RedirectURIs = append(
|
||||||
clientMultiRedirect.Metadata.RedirectURIs,
|
clientMultiRedirect.Metadata.RedirectURIs,
|
||||||
*mustParseURL("https://auth2.example.com/"))
|
*mustParseURL("https://auth2.example.com/"))
|
||||||
|
|
||||||
|
adminClientWithPeers := adminClientGood
|
||||||
|
adminClientWithPeers.TrustedPeers = []string{"test_client_0"}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
req adminschema.ClientCreateRequest
|
req adminschema.ClientCreateRequest
|
||||||
want adminschema.ClientCreateResponse
|
want adminschema.ClientCreateResponse
|
||||||
wantClient client.Client
|
wantClient client.Client
|
||||||
wantError int
|
wantError int
|
||||||
|
wantTrustedPeers []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
req: adminschema.ClientCreateRequest{},
|
req: adminschema.ClientCreateRequest{},
|
||||||
|
@ -462,13 +469,35 @@ func TestCreateClient(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantClient: clientMultiRedirect,
|
wantClient: clientMultiRedirect,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
req: adminschema.ClientCreateRequest{
|
||||||
|
Client: &adminClientWithPeers,
|
||||||
|
},
|
||||||
|
want: adminschema.ClientCreateResponse{
|
||||||
|
Client: addIDAndSecret(adminClientWithPeers),
|
||||||
|
},
|
||||||
|
wantClient: clientGood,
|
||||||
|
wantTrustedPeers: []string{"test_client_0"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
if i != 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f := makeAdminAPITestFixtures()
|
f := makeAdminAPITestFixtures()
|
||||||
|
for j, r := range []string{"https://client0.example.com",
|
||||||
|
"https://client1.example.com"} {
|
||||||
|
_, err := f.cr.New(nil, client.Client{
|
||||||
|
Credentials: oidc.ClientCredentials{
|
||||||
|
ID: fmt.Sprintf("test_client_%d", j),
|
||||||
|
},
|
||||||
|
Metadata: oidc.ClientMetadata{
|
||||||
|
RedirectURIs: []url.URL{*mustParseURL(r)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("case %d, client %d: unexpected error creating client: %v", i, j, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := f.adClient.Client.Create(&tt.req).Do()
|
resp, err := f.adClient.Client.Create(&tt.req).Do()
|
||||||
if tt.wantError != 0 {
|
if tt.wantError != 0 {
|
||||||
|
|
|
@ -618,7 +618,7 @@ func TestRefreshTokenEndpoints(t *testing.T) {
|
||||||
t.Errorf("case %d: expected client ids did not match actual: %s", i, diff)
|
t.Errorf("case %d: expected client ids did not match actual: %s", i, diff)
|
||||||
}
|
}
|
||||||
for _, clientID := range ids {
|
for _, clientID := range ids {
|
||||||
if err := f.client.Clients.Revoke(tt.userID, clientID).Do(); err != nil {
|
if err := f.client.RefreshClient.Revoke(tt.userID, clientID).Do(); err != nil {
|
||||||
t.Errorf("case %d: failed to revoke client: %v", i, err)
|
t.Errorf("case %d: failed to revoke client: %v", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,10 @@ __Version:__ v1
|
||||||
redirectURIs: [
|
redirectURIs: [
|
||||||
string
|
string
|
||||||
],
|
],
|
||||||
secret: string // The client secret. Ignored in client create requests.
|
secret: string // The client secret. Ignored in client create requests.,
|
||||||
|
trustedPeers: [
|
||||||
|
string
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -148,6 +148,10 @@ type Client struct {
|
||||||
|
|
||||||
// Secret: The client secret. Ignored in client create requests.
|
// Secret: The client secret. Ignored in client create requests.
|
||||||
Secret string `json:"secret,omitempty"`
|
Secret string `json:"secret,omitempty"`
|
||||||
|
|
||||||
|
// TrustedPeers: Array of ClientIDs of clients that are allowed to mint
|
||||||
|
// ID tokens for the client being created.
|
||||||
|
TrustedPeers []string `json:"trustedPeers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientCreateRequest struct {
|
type ClientCreateRequest struct {
|
||||||
|
|
|
@ -84,6 +84,13 @@ const DiscoveryJSON = `{
|
||||||
"clientURI": {
|
"clientURI": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "OPTIONAL. URL of the home page of the Client. The value of this field MUST point to a valid Web page. If present, the server SHOULD display this URL to the End-User in a followable fashion. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ."
|
"description": "OPTIONAL. URL of the home page of the Client. The value of this field MUST point to a valid Web page. If present, the server SHOULD display this URL to the End-User in a followable fashion. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ."
|
||||||
|
},
|
||||||
|
"trustedPeers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of ClientIDs of clients that are allowed to mint ID tokens for the client being created."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -228,4 +235,5 @@ const DiscoveryJSON = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
`
|
`
|
|
@ -78,6 +78,13 @@
|
||||||
"clientURI": {
|
"clientURI": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "OPTIONAL. URL of the home page of the Client. The value of this field MUST point to a valid Web page. If present, the server SHOULD display this URL to the End-User in a followable fashion. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ."
|
"description": "OPTIONAL. URL of the home page of the Client. The value of this field MUST point to a valid Web page. If present, the server SHOULD display this URL to the End-User in a followable fashion. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ."
|
||||||
|
},
|
||||||
|
"trustedPeers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of ClientIDs of clients that are allowed to mint ID tokens for the client being created."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -222,3 +229,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,7 +199,7 @@ A client with associated public metadata.
|
||||||
|
|
||||||
> __Description__
|
> __Description__
|
||||||
|
|
||||||
> List all clients that hold refresh tokens for the authenticated user.
|
> List all clients that hold refresh tokens for the specified user.
|
||||||
|
|
||||||
|
|
||||||
> __Parameters__
|
> __Parameters__
|
||||||
|
@ -221,19 +221,19 @@ A client with associated public metadata.
|
||||||
|
|
||||||
> __Summary__
|
> __Summary__
|
||||||
|
|
||||||
> Revoke Clients
|
> Revoke RefreshClient
|
||||||
|
|
||||||
> __Description__
|
> __Description__
|
||||||
|
|
||||||
> Revoke all refresh tokens issues to the client for the authenticated user.
|
> Revoke all refresh tokens issues to the client for the specified user.
|
||||||
|
|
||||||
|
|
||||||
> __Parameters__
|
> __Parameters__
|
||||||
|
|
||||||
> |Name|Located in|Description|Required|Type|
|
> |Name|Located in|Description|Required|Type|
|
||||||
|:-----|:-----|:-----|:-----|:-----|
|
|:-----|:-----|:-----|:-----|:-----|
|
||||||
| userid | path | | Yes | string |
|
|
||||||
| clientid | path | | Yes | string |
|
| clientid | path | | Yes | string |
|
||||||
|
| userid | path | | Yes | string |
|
||||||
|
|
||||||
|
|
||||||
> __Responses__
|
> __Responses__
|
||||||
|
@ -310,8 +310,8 @@ A client with associated public metadata.
|
||||||
|
|
||||||
> |Name|Located in|Description|Required|Type|
|
> |Name|Located in|Description|Required|Type|
|
||||||
|:-----|:-----|:-----|:-----|:-----|
|
|:-----|:-----|:-----|:-----|:-----|
|
||||||
| maxResults | query | | No | integer |
|
|
||||||
| nextPageToken | query | | No | string |
|
| nextPageToken | query | | No | string |
|
||||||
|
| maxResults | query | | No | integer |
|
||||||
|
|
||||||
|
|
||||||
> __Responses__
|
> __Responses__
|
||||||
|
|
|
@ -334,82 +334,7 @@ func (c *ClientsListCall) Do() (*ClientPage, error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// method id "dex.Client.Revoke":
|
// method id "dex.RefreshClient.List":
|
||||||
|
|
||||||
type ClientsRevokeCall struct {
|
|
||||||
s *Service
|
|
||||||
userid string
|
|
||||||
clientid string
|
|
||||||
opt_ map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revoke: Revoke all refresh tokens issues to the client for the
|
|
||||||
// authenticated user.
|
|
||||||
func (r *ClientsService) Revoke(userid string, clientid string) *ClientsRevokeCall {
|
|
||||||
c := &ClientsRevokeCall{s: r.s, opt_: make(map[string]interface{})}
|
|
||||||
c.userid = userid
|
|
||||||
c.clientid = clientid
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields allows partial responses to be retrieved.
|
|
||||||
// See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
|
|
||||||
// for more information.
|
|
||||||
func (c *ClientsRevokeCall) Fields(s ...googleapi.Field) *ClientsRevokeCall {
|
|
||||||
c.opt_["fields"] = googleapi.CombineFields(s)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientsRevokeCall) Do() error {
|
|
||||||
var body io.Reader = nil
|
|
||||||
params := make(url.Values)
|
|
||||||
params.Set("alt", "json")
|
|
||||||
if v, ok := c.opt_["fields"]; ok {
|
|
||||||
params.Set("fields", fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
urls := googleapi.ResolveRelative(c.s.BasePath, "account/{userid}/refresh/{clientid}")
|
|
||||||
urls += "?" + params.Encode()
|
|
||||||
req, _ := http.NewRequest("DELETE", urls, body)
|
|
||||||
googleapi.Expand(req.URL, map[string]string{
|
|
||||||
"userid": c.userid,
|
|
||||||
"clientid": c.clientid,
|
|
||||||
})
|
|
||||||
req.Header.Set("User-Agent", "google-api-go-client/0.5")
|
|
||||||
res, err := c.s.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer googleapi.CloseBody(res)
|
|
||||||
if err := googleapi.CheckResponse(res); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
// {
|
|
||||||
// "description": "Revoke all refresh tokens issues to the client for the authenticated user.",
|
|
||||||
// "httpMethod": "DELETE",
|
|
||||||
// "id": "dex.Client.Revoke",
|
|
||||||
// "parameterOrder": [
|
|
||||||
// "userid",
|
|
||||||
// "clientid"
|
|
||||||
// ],
|
|
||||||
// "parameters": {
|
|
||||||
// "clientid": {
|
|
||||||
// "location": "path",
|
|
||||||
// "required": true,
|
|
||||||
// "type": "string"
|
|
||||||
// },
|
|
||||||
// "userid": {
|
|
||||||
// "location": "path",
|
|
||||||
// "required": true,
|
|
||||||
// "type": "string"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "path": "account/{userid}/refresh/{clientid}"
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// method id "dex.Client.List":
|
|
||||||
|
|
||||||
type RefreshClientListCall struct {
|
type RefreshClientListCall struct {
|
||||||
s *Service
|
s *Service
|
||||||
|
@ -417,7 +342,7 @@ type RefreshClientListCall struct {
|
||||||
opt_ map[string]interface{}
|
opt_ map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List: List all clients that hold refresh tokens for the authenticated
|
// List: List all clients that hold refresh tokens for the specified
|
||||||
// user.
|
// user.
|
||||||
func (r *RefreshClientService) List(userid string) *RefreshClientListCall {
|
func (r *RefreshClientService) List(userid string) *RefreshClientListCall {
|
||||||
c := &RefreshClientListCall{s: r.s, opt_: make(map[string]interface{})}
|
c := &RefreshClientListCall{s: r.s, opt_: make(map[string]interface{})}
|
||||||
|
@ -461,9 +386,9 @@ func (c *RefreshClientListCall) Do() (*RefreshClientList, error) {
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
// {
|
// {
|
||||||
// "description": "List all clients that hold refresh tokens for the authenticated user.",
|
// "description": "List all clients that hold refresh tokens for the specified user.",
|
||||||
// "httpMethod": "GET",
|
// "httpMethod": "GET",
|
||||||
// "id": "dex.Client.List",
|
// "id": "dex.RefreshClient.List",
|
||||||
// "parameterOrder": [
|
// "parameterOrder": [
|
||||||
// "userid"
|
// "userid"
|
||||||
// ],
|
// ],
|
||||||
|
@ -482,6 +407,81 @@ func (c *RefreshClientListCall) Do() (*RefreshClientList, error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// method id "dex.RefreshClient.Revoke":
|
||||||
|
|
||||||
|
type RefreshClientRevokeCall struct {
|
||||||
|
s *Service
|
||||||
|
userid string
|
||||||
|
clientid string
|
||||||
|
opt_ map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke: Revoke all refresh tokens issues to the client for the
|
||||||
|
// specified user.
|
||||||
|
func (r *RefreshClientService) Revoke(userid string, clientid string) *RefreshClientRevokeCall {
|
||||||
|
c := &RefreshClientRevokeCall{s: r.s, opt_: make(map[string]interface{})}
|
||||||
|
c.userid = userid
|
||||||
|
c.clientid = clientid
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields allows partial responses to be retrieved.
|
||||||
|
// See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
|
||||||
|
// for more information.
|
||||||
|
func (c *RefreshClientRevokeCall) Fields(s ...googleapi.Field) *RefreshClientRevokeCall {
|
||||||
|
c.opt_["fields"] = googleapi.CombineFields(s)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RefreshClientRevokeCall) Do() error {
|
||||||
|
var body io.Reader = nil
|
||||||
|
params := make(url.Values)
|
||||||
|
params.Set("alt", "json")
|
||||||
|
if v, ok := c.opt_["fields"]; ok {
|
||||||
|
params.Set("fields", fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
urls := googleapi.ResolveRelative(c.s.BasePath, "account/{userid}/refresh/{clientid}")
|
||||||
|
urls += "?" + params.Encode()
|
||||||
|
req, _ := http.NewRequest("DELETE", urls, body)
|
||||||
|
googleapi.Expand(req.URL, map[string]string{
|
||||||
|
"userid": c.userid,
|
||||||
|
"clientid": c.clientid,
|
||||||
|
})
|
||||||
|
req.Header.Set("User-Agent", "google-api-go-client/0.5")
|
||||||
|
res, err := c.s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer googleapi.CloseBody(res)
|
||||||
|
if err := googleapi.CheckResponse(res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
// {
|
||||||
|
// "description": "Revoke all refresh tokens issues to the client for the specified user.",
|
||||||
|
// "httpMethod": "DELETE",
|
||||||
|
// "id": "dex.RefreshClient.Revoke",
|
||||||
|
// "parameterOrder": [
|
||||||
|
// "userid",
|
||||||
|
// "clientid"
|
||||||
|
// ],
|
||||||
|
// "parameters": {
|
||||||
|
// "clientid": {
|
||||||
|
// "location": "path",
|
||||||
|
// "required": true,
|
||||||
|
// "type": "string"
|
||||||
|
// },
|
||||||
|
// "userid": {
|
||||||
|
// "location": "path",
|
||||||
|
// "required": true,
|
||||||
|
// "type": "string"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "path": "account/{userid}/refresh/{clientid}"
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// method id "dex.User.Create":
|
// method id "dex.User.Create":
|
||||||
|
|
||||||
type UsersCreateCall struct {
|
type UsersCreateCall struct {
|
||||||
|
|
|
@ -272,28 +272,6 @@ const DiscoveryJSON = `{
|
||||||
"response": {
|
"response": {
|
||||||
"$ref": "ClientWithSecret"
|
"$ref": "ClientWithSecret"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Revoke": {
|
|
||||||
"id": "dex.Client.Revoke",
|
|
||||||
"description": "Revoke all refresh tokens issues to the client for the authenticated user.",
|
|
||||||
"httpMethod": "DELETE",
|
|
||||||
"path": "account/{userid}/refresh/{clientid}",
|
|
||||||
"parameterOrder": [
|
|
||||||
"userid",
|
|
||||||
"clientid"
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"clientid": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true,
|
|
||||||
"location": "path"
|
|
||||||
},
|
|
||||||
"userid": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true,
|
|
||||||
"location": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -398,8 +376,8 @@ const DiscoveryJSON = `{
|
||||||
"RefreshClient": {
|
"RefreshClient": {
|
||||||
"methods": {
|
"methods": {
|
||||||
"List": {
|
"List": {
|
||||||
"id": "dex.Client.List",
|
"id": "dex.RefreshClient.List",
|
||||||
"description": "List all clients that hold refresh tokens for the authenticated user.",
|
"description": "List all clients that hold refresh tokens for the specified user.",
|
||||||
"httpMethod": "GET",
|
"httpMethod": "GET",
|
||||||
"path": "account/{userid}/refresh",
|
"path": "account/{userid}/refresh",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
@ -415,6 +393,28 @@ const DiscoveryJSON = `{
|
||||||
"response": {
|
"response": {
|
||||||
"$ref": "RefreshClientList"
|
"$ref": "RefreshClientList"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Revoke": {
|
||||||
|
"id": "dex.RefreshClient.Revoke",
|
||||||
|
"description": "Revoke all refresh tokens issues to the client for the specified user.",
|
||||||
|
"httpMethod": "DELETE",
|
||||||
|
"path": "account/{userid}/refresh/{clientid}",
|
||||||
|
"parameterOrder": [
|
||||||
|
"userid",
|
||||||
|
"clientid"
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"clientid": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"location": "path"
|
||||||
|
},
|
||||||
|
"userid": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"location": "path"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,28 +266,6 @@
|
||||||
"response": {
|
"response": {
|
||||||
"$ref": "ClientWithSecret"
|
"$ref": "ClientWithSecret"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Revoke": {
|
|
||||||
"id": "dex.Client.Revoke",
|
|
||||||
"description": "Revoke all refresh tokens issues to the client for the authenticated user.",
|
|
||||||
"httpMethod": "DELETE",
|
|
||||||
"path": "account/{userid}/refresh/{clientid}",
|
|
||||||
"parameterOrder": [
|
|
||||||
"userid",
|
|
||||||
"clientid"
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"clientid": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true,
|
|
||||||
"location": "path"
|
|
||||||
},
|
|
||||||
"userid": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true,
|
|
||||||
"location": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -392,8 +370,8 @@
|
||||||
"RefreshClient": {
|
"RefreshClient": {
|
||||||
"methods": {
|
"methods": {
|
||||||
"List": {
|
"List": {
|
||||||
"id": "dex.Client.List",
|
"id": "dex.RefreshClient.List",
|
||||||
"description": "List all clients that hold refresh tokens for the authenticated user.",
|
"description": "List all clients that hold refresh tokens for the specified user.",
|
||||||
"httpMethod": "GET",
|
"httpMethod": "GET",
|
||||||
"path": "account/{userid}/refresh",
|
"path": "account/{userid}/refresh",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
@ -409,6 +387,28 @@
|
||||||
"response": {
|
"response": {
|
||||||
"$ref": "RefreshClientList"
|
"$ref": "RefreshClientList"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Revoke": {
|
||||||
|
"id": "dex.RefreshClient.Revoke",
|
||||||
|
"description": "Revoke all refresh tokens issues to the client for the specified user.",
|
||||||
|
"httpMethod": "DELETE",
|
||||||
|
"path": "account/{userid}/refresh/{clientid}",
|
||||||
|
"parameterOrder": [
|
||||||
|
"userid",
|
||||||
|
"clientid"
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"clientid": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"location": "path"
|
||||||
|
},
|
||||||
|
"userid": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"location": "path"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
scope/scope.go
Normal file
34
scope/scope.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package scope
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Scope prefix which indicates initiation of a cross-client authentication flow.
|
||||||
|
// See https://developers.google.com/identity/protocols/CrossClientAuth
|
||||||
|
ScopeGoogleCrossClient = "audience:server:client_id:"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scopes []string
|
||||||
|
|
||||||
|
func (s Scopes) OfflineAccess() bool {
|
||||||
|
return s.HasScope("offline_access")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Scopes) HasScope(scope string) bool {
|
||||||
|
for _, curScope := range s {
|
||||||
|
if curScope == scope {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Scopes) CrossClientIDs() []string {
|
||||||
|
clients := []string{}
|
||||||
|
for _, scope := range s {
|
||||||
|
if strings.HasPrefix(scope, ScopeGoogleCrossClient) {
|
||||||
|
clients = append(clients, scope[len(ScopeGoogleCrossClient):])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clients
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ func TestClientToken(t *testing.T) {
|
||||||
cli := client.Client{
|
cli := client.Client{
|
||||||
Metadata: clientMetadata,
|
Metadata: clientMetadata,
|
||||||
}
|
}
|
||||||
creds, err := clientManager.New(cli)
|
creds, err := clientManager.New(cli, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create client: %v", err)
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (s *Server) handleClientRegistrationRequest(r *http.Request) (*oidc.ClientR
|
||||||
cli := client.Client{
|
cli := client.Client{
|
||||||
Metadata: clientMetadata,
|
Metadata: clientMetadata,
|
||||||
}
|
}
|
||||||
creds, err := s.ClientManager.New(cli)
|
creds, err := s.ClientManager.New(cli, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to create new client identity: %v", err)
|
log.Errorf("Failed to create new client identity: %v", err)
|
||||||
return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata")
|
return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata")
|
||||||
|
|
|
@ -87,7 +87,7 @@ func (c *clientResource) create(w http.ResponseWriter, r *http.Request) {
|
||||||
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidClientMetadata, err.Error()))
|
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidClientMetadata, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
creds, err := c.manager.New(ci)
|
creds, err := c.manager.New(ci, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed creating client: %v", err)
|
log.Errorf("Failed creating client: %v", err)
|
||||||
|
|
330
server/cross_client_test.go
Normal file
330
server/cross_client_test.go
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
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"
|
||||||
|
clientmanager "github.com/coreos/dex/client/manager"
|
||||||
|
"github.com/coreos/dex/connector"
|
||||||
|
"github.com/coreos/dex/scope"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeCrossClientTestFixtures() (*testFixtures, error) {
|
||||||
|
f, err := makeTestFixtures()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't make test fixtures: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cliData := range []struct {
|
||||||
|
id string
|
||||||
|
authorized []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
id: "client_a",
|
||||||
|
}, {
|
||||||
|
id: "client_b",
|
||||||
|
authorized: []string{"client_a"},
|
||||||
|
}, {
|
||||||
|
id: "client_c",
|
||||||
|
authorized: []string{"client_a", "client_b"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "https://",
|
||||||
|
Path: cliData.id,
|
||||||
|
Host: cliData.id,
|
||||||
|
}
|
||||||
|
cliCreds, err := f.clientManager.New(client.Client{
|
||||||
|
Credentials: oidc.ClientCredentials{
|
||||||
|
ID: cliData.id,
|
||||||
|
},
|
||||||
|
Metadata: oidc.ClientMetadata{
|
||||||
|
RedirectURIs: []url.URL{u},
|
||||||
|
},
|
||||||
|
}, &clientmanager.ClientOptions{
|
||||||
|
TrustedPeers: cliData.authorized,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unexpected error creating clients: %v", err)
|
||||||
|
}
|
||||||
|
f.clientCreds[cliData.id] = *cliCreds
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/coreos/dex/connector"
|
"github.com/coreos/dex/connector"
|
||||||
phttp "github.com/coreos/dex/pkg/http"
|
phttp "github.com/coreos/dex/pkg/http"
|
||||||
"github.com/coreos/dex/pkg/log"
|
"github.com/coreos/dex/pkg/log"
|
||||||
|
"github.com/coreos/dex/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -341,30 +342,9 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check scopes.
|
// Check scopes.
|
||||||
var scopes []string
|
if scopeErr := validateScopes(srv, acr.ClientID, acr.Scope); scopeErr != nil {
|
||||||
foundOpenIDScope := false
|
log.Error(scopeErr)
|
||||||
for _, scope := range acr.Scope {
|
writeAuthError(w, scopeErr, acr.State)
|
||||||
switch scope {
|
|
||||||
case "openid":
|
|
||||||
foundOpenIDScope = true
|
|
||||||
scopes = append(scopes, scope)
|
|
||||||
case "offline_access":
|
|
||||||
// According to the spec, for offline_access scope, the client must
|
|
||||||
// use a response_type value that would result in an Authorization Code.
|
|
||||||
// Currently oauth2.ResponseTypeCode is the only supported response type,
|
|
||||||
// and it's been checked above, so we don't need to check it again here.
|
|
||||||
//
|
|
||||||
// TODO(yifan): Verify that 'consent' should be in 'prompt'.
|
|
||||||
scopes = append(scopes, scope)
|
|
||||||
default:
|
|
||||||
// Pass all other scopes.
|
|
||||||
scopes = append(scopes, scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundOpenIDScope {
|
|
||||||
log.Errorf("Invalid auth request: missing 'openid' in 'scope'")
|
|
||||||
writeAuthError(w, oauth2.NewError(oauth2.ErrorInvalidRequest), acr.State)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,6 +390,67 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateScopes(srv OIDCServer, clientID string, scopes []string) error {
|
||||||
|
foundOpenIDScope := false
|
||||||
|
for i, curScope := range scopes {
|
||||||
|
if i > 0 && curScope == scopes[i-1] {
|
||||||
|
err := oauth2.NewError(oauth2.ErrorInvalidRequest)
|
||||||
|
err.Description = fmt.Sprintf(
|
||||||
|
"Duplicate scopes are not allowed: %q",
|
||||||
|
curScope)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(curScope, scope.ScopeGoogleCrossClient):
|
||||||
|
otherClient := curScope[len(scope.ScopeGoogleCrossClient):]
|
||||||
|
var allowed bool
|
||||||
|
var err error
|
||||||
|
if otherClient == clientID {
|
||||||
|
allowed = true
|
||||||
|
} else {
|
||||||
|
allowed, err = srv.CrossClientAuthAllowed(clientID, otherClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
err := oauth2.NewError(oauth2.ErrorInvalidRequest)
|
||||||
|
err.Description = fmt.Sprintf(
|
||||||
|
"%q is not authorized to perform cross-client requests for %q",
|
||||||
|
clientID, otherClient)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case curScope == "openid":
|
||||||
|
foundOpenIDScope = true
|
||||||
|
case curScope == "profile":
|
||||||
|
case curScope == "email":
|
||||||
|
case curScope == "offline_access":
|
||||||
|
// According to the spec, for offline_access scope, the client must
|
||||||
|
// use a response_type value that would result in an Authorization
|
||||||
|
// Code. Currently oauth2.ResponseTypeCode is the only supported
|
||||||
|
// response type, and it's been checked above, so we don't need to
|
||||||
|
// check it again here.
|
||||||
|
//
|
||||||
|
// TODO(yifan): Verify that 'consent' should be in 'prompt'.
|
||||||
|
default:
|
||||||
|
// Reject all other scopes.
|
||||||
|
err := oauth2.NewError(oauth2.ErrorInvalidRequest)
|
||||||
|
err.Description = fmt.Sprintf("%q is not a recognized scope", curScope)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundOpenIDScope {
|
||||||
|
log.Errorf("Invalid auth request: missing 'openid' in 'scope'")
|
||||||
|
err := oauth2.NewError(oauth2.ErrorInvalidRequest)
|
||||||
|
err.Description = "Invalid auth request: missing 'openid' in 'scope'"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleTokenFunc(srv OIDCServer) http.HandlerFunc {
|
func handleTokenFunc(srv OIDCServer) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
|
|
||||||
"github.com/coreos/dex/client"
|
"github.com/coreos/dex/client"
|
||||||
"github.com/coreos/dex/connector"
|
"github.com/coreos/dex/connector"
|
||||||
|
"github.com/coreos/dex/scope"
|
||||||
"github.com/coreos/go-oidc/jose"
|
"github.com/coreos/go-oidc/jose"
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
"github.com/coreos/go-oidc/oauth2"
|
||||||
"github.com/coreos/go-oidc/oidc"
|
"github.com/coreos/go-oidc/oidc"
|
||||||
|
@ -308,8 +309,110 @@ func TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleTokenFunc(t *testing.T) {
|
func TestValidateScopes(t *testing.T) {
|
||||||
|
f, err := makeCrossClientTestFixtures()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't make test fixtures: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
clientID string
|
||||||
|
scopes []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// ERR: no openid scope
|
||||||
|
clientID: "XXX",
|
||||||
|
scopes: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OK: minimum scopes
|
||||||
|
clientID: "XXX",
|
||||||
|
scopes: []string{"openid"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OK: offline_access
|
||||||
|
clientID: "XXX",
|
||||||
|
scopes: []string{"openid", "offline_access"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// ERR: unknown scope
|
||||||
|
clientID: "XXX",
|
||||||
|
scopes: []string{"openid", "wat"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// ERR: invalid cross client auth
|
||||||
|
clientID: "XXX",
|
||||||
|
scopes: []string{"openid", scope.ScopeGoogleCrossClient + "client_a"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OK: valid cross client auth (though perverse - a client
|
||||||
|
// requesting cross-client auth for itself)
|
||||||
|
clientID: "client_a",
|
||||||
|
scopes: []string{"openid", scope.ScopeGoogleCrossClient + "client_a"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
// OK: valid cross client auth
|
||||||
|
clientID: "client_a",
|
||||||
|
scopes: []string{"openid", scope.ScopeGoogleCrossClient + "client_b"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
// ERR: valid cross client auth...but duplicated scope.
|
||||||
|
clientID: "client_a",
|
||||||
|
scopes: []string{"openid",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_b",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_b",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OK: valid cross client auth with >1 clients including itself
|
||||||
|
clientID: "client_a",
|
||||||
|
scopes: []string{
|
||||||
|
"openid",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_a",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_b",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_c",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// ERR: valid cross client auth with >1 clients including itself...but no openid!
|
||||||
|
clientID: "client_a",
|
||||||
|
scopes: []string{
|
||||||
|
scope.ScopeGoogleCrossClient + "client_a",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_b",
|
||||||
|
scope.ScopeGoogleCrossClient + "client_c",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
err := validateScopes(f.srv, tt.clientID, tt.scopes)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleTokenFunc(t *testing.T) {
|
||||||
fx, err := makeTestFixtures()
|
fx, err := makeTestFixtures()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not run test fixtures: %v", err)
|
t.Fatalf("could not run test fixtures: %v", err)
|
||||||
|
|
|
@ -45,13 +45,19 @@ type OIDCServer interface {
|
||||||
ClientMetadata(string) (*oidc.ClientMetadata, error)
|
ClientMetadata(string) (*oidc.ClientMetadata, error)
|
||||||
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error)
|
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error)
|
||||||
Login(oidc.Identity, string) (string, error)
|
Login(oidc.Identity, string) (string, error)
|
||||||
|
|
||||||
// CodeToken exchanges a code for an ID token and a refresh token string on success.
|
// CodeToken exchanges a code for an ID token and a refresh token string on success.
|
||||||
CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error)
|
CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error)
|
||||||
|
|
||||||
ClientCredsToken(creds oidc.ClientCredentials) (*jose.JWT, error)
|
ClientCredsToken(creds oidc.ClientCredentials) (*jose.JWT, error)
|
||||||
|
|
||||||
// RefreshToken takes a previously generated refresh token and returns a new ID token
|
// RefreshToken takes a previously generated refresh token and returns a new ID token
|
||||||
// if the token is valid.
|
// if the token is valid.
|
||||||
RefreshToken(creds oidc.ClientCredentials, token string) (*jose.JWT, error)
|
RefreshToken(creds oidc.ClientCredentials, token string) (*jose.JWT, error)
|
||||||
|
|
||||||
KillSession(string) error
|
KillSession(string) error
|
||||||
|
|
||||||
|
CrossClientAuthAllowed(requestingClientID, authorizingClientID string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTVerifierFactory func(clientID string) oidc.JWTVerifier
|
type JWTVerifierFactory func(clientID string) oidc.JWTVerifier
|
||||||
|
@ -438,6 +444,36 @@ func (s *Server) CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jo
|
||||||
claims := ses.Claims(s.IssuerURL.String())
|
claims := ses.Claims(s.IssuerURL.String())
|
||||||
user.AddToClaims(claims)
|
user.AddToClaims(claims)
|
||||||
|
|
||||||
|
crossClientIDs := ses.Scope.CrossClientIDs()
|
||||||
|
if len(crossClientIDs) > 0 {
|
||||||
|
var aud []string
|
||||||
|
for _, id := range crossClientIDs {
|
||||||
|
if ses.ClientID == id {
|
||||||
|
aud = append(aud, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allowed, err := s.CrossClientAuthAllowed(ses.ClientID, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to check cross client auth. reqClientID %v; authClient:ID %v; err: %v", ses.ClientID, id, err)
|
||||||
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
err := oauth2.NewError(oauth2.ErrorInvalidRequest)
|
||||||
|
err.Description = fmt.Sprintf(
|
||||||
|
"%q is not authorized to perform cross-client requests for %q",
|
||||||
|
ses.ClientID, id)
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
aud = append(aud, id)
|
||||||
|
}
|
||||||
|
if len(aud) == 1 {
|
||||||
|
claims.Add("aud", aud[0])
|
||||||
|
} else {
|
||||||
|
claims.Add("aud", aud)
|
||||||
|
}
|
||||||
|
claims.Add("azp", ses.ClientID)
|
||||||
|
}
|
||||||
|
|
||||||
jwt, err := jose.NewSignedJWT(claims, signer)
|
jwt, err := jose.NewSignedJWT(claims, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to generate ID token: %v", err)
|
log.Errorf("Failed to generate ID token: %v", err)
|
||||||
|
@ -521,6 +557,19 @@ func (s *Server) RefreshToken(creds oidc.ClientCredentials, token string) (*jose
|
||||||
return jwt, nil
|
return jwt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) CrossClientAuthAllowed(requestingClientID, authorizingClientID string) (bool, error) {
|
||||||
|
alloweds, err := s.ClientRepo.GetTrustedPeers(nil, authorizingClientID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, allowed := range alloweds {
|
||||||
|
if requestingClientID == allowed {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) JWTVerifierFactory() JWTVerifierFactory {
|
func (s *Server) JWTVerifierFactory() JWTVerifierFactory {
|
||||||
noop := func() error { return nil }
|
noop := func() error { return nil }
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/dex/client"
|
|
||||||
"github.com/coreos/dex/db"
|
|
||||||
"github.com/coreos/dex/refresh/refreshtest"
|
|
||||||
"github.com/coreos/dex/session/manager"
|
|
||||||
"github.com/coreos/dex/user"
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
"github.com/coreos/go-oidc/jose"
|
||||||
"github.com/coreos/go-oidc/key"
|
"github.com/coreos/go-oidc/key"
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
"github.com/coreos/go-oidc/oauth2"
|
||||||
"github.com/coreos/go-oidc/oidc"
|
"github.com/coreos/go-oidc/oidc"
|
||||||
"github.com/kylelemons/godebug/pretty"
|
"github.com/kylelemons/godebug/pretty"
|
||||||
|
|
||||||
|
"github.com/coreos/dex/client"
|
||||||
|
"github.com/coreos/dex/db"
|
||||||
|
"github.com/coreos/dex/refresh/refreshtest"
|
||||||
|
"github.com/coreos/dex/session/manager"
|
||||||
|
"github.com/coreos/dex/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validRedirURL = url.URL{
|
var validRedirURL = url.URL{
|
||||||
|
@ -266,6 +267,12 @@ func TestServerLoginDisabledUser(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerCodeToken(t *testing.T) {
|
func TestServerCodeToken(t *testing.T) {
|
||||||
|
f, err := makeTestFixtures()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error creating test fixtures: %v", err)
|
||||||
|
}
|
||||||
|
sm := f.sessionManager
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
scope []string
|
scope []string
|
||||||
refreshToken string
|
refreshToken string
|
||||||
|
@ -277,21 +284,14 @@ func TestServerCodeToken(t *testing.T) {
|
||||||
},
|
},
|
||||||
// Have 'offline_access' in scope, should get non-empty refresh token.
|
// Have 'offline_access' in scope, should get non-empty refresh token.
|
||||||
{
|
{
|
||||||
// NOTE(ericchiang): This test assumes that the database ID of the first
|
// NOTE(ericchiang): This test assumes that the database ID of the
|
||||||
// refresh token will be "1".
|
// first refresh token will be "1".
|
||||||
scope: []string{"openid", "offline_access"},
|
scope: []string{"openid", "offline_access"},
|
||||||
refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))),
|
refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
f, err := makeTestFixtures()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error making test fixtures: %v", err)
|
|
||||||
}
|
|
||||||
f.srv.RefreshTokenRepo = refreshtest.NewTestRefreshTokenRepo()
|
|
||||||
|
|
||||||
sm := f.sessionManager
|
|
||||||
sessionID, err := sm.NewSession("bogus_idpc", testClientID, "bogus", url.URL{}, "", false, tt.scope)
|
sessionID, err := sm.NewSession("bogus_idpc", testClientID, "bogus", url.URL{}, "", false, tt.scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("case %d: unexpected error: %v", i, err)
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
|
@ -311,11 +311,9 @@ func TestServerCodeToken(t *testing.T) {
|
||||||
t.Fatalf("case %d: unexpected error: %v", i, err)
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, token, err := f.srv.CodeToken(
|
jwt, token, err := f.srv.CodeToken(oidc.ClientCredentials{
|
||||||
oidc.ClientCredentials{
|
ID: testClientID,
|
||||||
ID: testClientID,
|
Secret: clientTestSecret}, key)
|
||||||
Secret: clientTestSecret,
|
|
||||||
}, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("case %d: unexpected error: %v", i, err)
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/coreos/dex/connector"
|
"github.com/coreos/dex/connector"
|
||||||
"github.com/coreos/dex/db"
|
"github.com/coreos/dex/db"
|
||||||
"github.com/coreos/dex/email"
|
"github.com/coreos/dex/email"
|
||||||
|
"github.com/coreos/dex/refresh/refreshtest"
|
||||||
sessionmanager "github.com/coreos/dex/session/manager"
|
sessionmanager "github.com/coreos/dex/session/manager"
|
||||||
"github.com/coreos/dex/user"
|
"github.com/coreos/dex/user"
|
||||||
useremail "github.com/coreos/dex/user/email"
|
useremail "github.com/coreos/dex/user/email"
|
||||||
|
@ -83,6 +84,11 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
testPrivKey, _ = key.GeneratePrivateKey()
|
testPrivKey, _ = key.GeneratePrivateKey()
|
||||||
|
|
||||||
|
testClientCreds = oidc.ClientCredentials{
|
||||||
|
ID: testClientID,
|
||||||
|
Secret: base64.URLEncoding.EncodeToString([]byte("secret")),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type testFixtures struct {
|
type testFixtures struct {
|
||||||
|
@ -93,6 +99,7 @@ type testFixtures struct {
|
||||||
redirectURL url.URL
|
redirectURL url.URL
|
||||||
clientRepo client.ClientRepo
|
clientRepo client.ClientRepo
|
||||||
clientManager *clientmanager.ClientManager
|
clientManager *clientmanager.ClientManager
|
||||||
|
clientCreds map[string]oidc.ClientCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
type testFixtureOptions struct {
|
type testFixtureOptions struct {
|
||||||
|
@ -150,6 +157,8 @@ func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, err
|
||||||
sessionManager := sessionmanager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB()))
|
sessionManager := sessionmanager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB()))
|
||||||
sessionManager.GenerateCode = sequentialGenerateCodeFunc()
|
sessionManager.GenerateCode = sequentialGenerateCodeFunc()
|
||||||
|
|
||||||
|
refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo()
|
||||||
|
|
||||||
emailer, err := email.NewTemplatizedEmailerFromGlobs(
|
emailer, err := email.NewTemplatizedEmailerFromGlobs(
|
||||||
emailTemplatesLocation+"/*.txt",
|
emailTemplatesLocation+"/*.txt",
|
||||||
emailTemplatesLocation+"/*.html",
|
emailTemplatesLocation+"/*.html",
|
||||||
|
@ -210,6 +219,7 @@ func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, err
|
||||||
UserManager: userManager,
|
UserManager: userManager,
|
||||||
ClientManager: clientManager,
|
ClientManager: clientManager,
|
||||||
KeyManager: km,
|
KeyManager: km,
|
||||||
|
RefreshTokenRepo: refreshTokenRepo,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setTemplates(srv, tpl)
|
err = setTemplates(srv, tpl)
|
||||||
|
@ -243,5 +253,8 @@ func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, err
|
||||||
emailer: emailer,
|
emailer: emailer,
|
||||||
clientRepo: clientRepo,
|
clientRepo: clientRepo,
|
||||||
clientManager: clientManager,
|
clientManager: clientManager,
|
||||||
|
clientCreds: map[string]oidc.ClientCredentials{
|
||||||
|
testClientID: testClientCreds,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
"github.com/coreos/go-oidc/jose"
|
||||||
"github.com/coreos/go-oidc/oidc"
|
"github.com/coreos/go-oidc/oidc"
|
||||||
|
|
||||||
|
"github.com/coreos/dex/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -46,11 +48,13 @@ type Session struct {
|
||||||
// Regsiter indicates that this session is a registration flow.
|
// Regsiter indicates that this session is a registration flow.
|
||||||
Register bool
|
Register bool
|
||||||
|
|
||||||
// Nonce is optionally provided in the initial authorization request, and propogated in such cases to the generated claims.
|
// Nonce is optionally provided in the initial authorization request, and
|
||||||
|
// propogated in such cases to the generated claims.
|
||||||
Nonce string
|
Nonce string
|
||||||
|
|
||||||
// Scope is the 'scope' field in the authentication request. Example scopes are 'openid', 'email', 'offline', etc.
|
// Scope is the 'scope' field in the authentication request. Example scopes
|
||||||
Scope []string
|
// are 'openid', 'email', 'offline', etc.
|
||||||
|
Scope scope.Scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claims returns a new set of Claims for the current session.
|
// Claims returns a new set of Claims for the current session.
|
||||||
|
|
Reference in a new issue