diff --git a/admin/api.go b/admin/api.go index 1e782d2b..bb805803 100644 --- a/admin/api.go +++ b/admin/api.go @@ -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. - creds, err := a.clientManager.New(cli) + creds, err := a.clientManager.New(cli, &clientmanager.ClientOptions{ + TrustedPeers: req.Client.TrustedPeers, + }) + if err != nil { return adminschema.ClientCreateResponse{}, mapError(err) } diff --git a/client/client.go b/client/client.go index b7abb93a..9a862ef6 100644 --- a/client/client.go +++ b/client/client.go @@ -15,6 +15,7 @@ import ( ) var ( + ErrorInvalidClientID = errors.New("not a valid client ID") ErrorInvalidRedirectURL = errors.New("not a valid redirect url for the given client") ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many") 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) 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. diff --git a/client/manager/manager.go b/client/manager/manager.go index 28335936..5ee0790b 100644 --- a/client/manager/manager.go +++ b/client/manager/manager.go @@ -21,6 +21,10 @@ const ( maxSecretLength = 72 ) +type ClientOptions struct { + TrustedPeers []string +} + type SecretGenerator func() ([]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() if err != nil { return nil, err @@ -83,6 +87,13 @@ func (m *ClientManager) New(cli client.Client) (*oidc.ClientCredentials, error) 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() if err != nil { return nil, err diff --git a/client/manager/manager_test.go b/client/manager/manager_test.go index 2ab2083c..b00a4d22 100644 --- a/client/manager/manager_test.go +++ b/client/manager/manager_test.go @@ -132,7 +132,7 @@ func TestAuthenticate(t *testing.T) { cli := client.Client{ Metadata: cm, } - cc, err := f.mgr.New(cli) + cc, err := f.mgr.New(cli, nil) if err != nil { t.Fatalf(err.Error()) } diff --git a/cmd/dexctl/driver_db.go b/cmd/dexctl/driver_db.go index 19bfc9f1..b0471e83 100644 --- a/cmd/dexctl/driver_db.go +++ b/cmd/dexctl/driver_db.go @@ -34,7 +34,7 @@ func (d *dbDriver) NewClient(meta oidc.ClientMetadata) (*oidc.ClientCredentials, cli := client.Client{ Metadata: meta, } - return d.ciManager.New(cli) + return d.ciManager.New(cli, nil) } func (d *dbDriver) ConnectorConfigs() ([]connector.ConnectorConfig, error) { diff --git a/db/client.go b/db/client.go index edbd87c0..895e9e79 100644 --- a/db/client.go +++ b/db/client.go @@ -16,7 +16,8 @@ import ( ) const ( - clientTableName = "client_identity" + clientTableName = "client_identity" + trustedPeerTableName = "trusted_peers" // postgres error codes pgErrorCodeUniqueViolation = "23505" // unique_violation @@ -29,6 +30,13 @@ func init() { autoinc: false, 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) { @@ -58,6 +66,11 @@ type clientModel struct { 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) { ci := client.Client{ Credentials: oidc.ClientCredentials{ @@ -254,3 +267,63 @@ func (r *clientRepo) update(tx repo.Transaction, cli client.Client) error { _, err = ex.Update(cm) 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 +} diff --git a/db/migrate.go b/db/migrate.go index b6ebaefb..8a6e8488 100644 --- a/db/migrate.go +++ b/db/migrate.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/go-gorp/gorp" - "github.com/rubenv/sql-migrate" + migrate "github.com/rubenv/sql-migrate" "github.com/coreos/dex/db/migrations" ) diff --git a/db/migrate_sqlite3.go b/db/migrate_sqlite3.go index af00fcf8..6e2142a4 100644 --- a/db/migrate_sqlite3.go +++ b/db/migrate_sqlite3.go @@ -70,4 +70,10 @@ CREATE TABLE session_key ( expires_at bigint, stale integer ); + +CREATE TABLE trusted_peers ( + client_id text NOT NULL, + trusted_client_id text NOT NULL +); + ` diff --git a/db/migrations/0012_add_cross_client_authorizers.sql b/db/migrations/0012_add_cross_client_authorizers.sql new file mode 100644 index 00000000..6939f4c0 --- /dev/null +++ b/db/migrations/0012_add_cross_client_authorizers.sql @@ -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")) ; diff --git a/db/migrations/assets.go b/db/migrations/assets.go index 6b3821e7..e0d995b4 100644 --- a/db/migrations/assets.go +++ b/db/migrations/assets.go @@ -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", }, }, + { + 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", + }, + }, }, } diff --git a/db/user.go b/db/user.go index 00991668..6aecdd63 100644 --- a/db/user.go +++ b/db/user.go @@ -248,7 +248,7 @@ func (r *userRepo) GetRemoteIdentities(tx repo.Transaction, userID string) ([]us if err != sql.ErrNoRows { return nil, err } - return nil, err + return nil, nil } if len(rims) == 0 { return nil, nil diff --git a/functional/db_test.go b/functional/db_test.go index c8322afd..cc78d8a0 100644 --- a/functional/db_test.go +++ b/functional/db_test.go @@ -316,7 +316,7 @@ func TestDBClientRepoAuthenticate(t *testing.T) { cli := client.Client{ Metadata: cm, } - cc, err := m.New(cli) + cc, err := m.New(cli, nil) if err != nil { t.Fatalf(err.Error()) } diff --git a/integration/admin_api_test.go b/integration/admin_api_test.go index ada83054..ef51b06a 100644 --- a/integration/admin_api_test.go +++ b/integration/admin_api_test.go @@ -86,7 +86,9 @@ func makeAdminAPITestFixtures() *adminAPITestFixtures { var cliCount int 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) clientIDGenerator := func(hostport string) (string, error) { @@ -379,6 +381,7 @@ func TestCreateClient(t *testing.T) { } return u } + addIDAndSecret := func(cli adminschema.Client) *adminschema.Client { cli.Id = "client_auth.example.com" cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0")) @@ -404,16 +407,20 @@ func TestCreateClient(t *testing.T) { adminMultiRedirect := adminClientGood adminMultiRedirect.RedirectURIs = []string{"https://auth.example.com/", "https://auth2.example.com/"} - clientMultiRedirect := clientGoodAdmin + clientMultiRedirect := clientGood clientMultiRedirect.Metadata.RedirectURIs = append( clientMultiRedirect.Metadata.RedirectURIs, *mustParseURL("https://auth2.example.com/")) + adminClientWithPeers := adminClientGood + adminClientWithPeers.TrustedPeers = []string{"test_client_0"} + tests := []struct { - req adminschema.ClientCreateRequest - want adminschema.ClientCreateResponse - wantClient client.Client - wantError int + req adminschema.ClientCreateRequest + want adminschema.ClientCreateResponse + wantClient client.Client + wantError int + wantTrustedPeers []string }{ { req: adminschema.ClientCreateRequest{}, @@ -462,13 +469,35 @@ func TestCreateClient(t *testing.T) { }, 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 { - if i != 3 { - continue - } 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() if tt.wantError != 0 { diff --git a/integration/user_api_test.go b/integration/user_api_test.go index 3b381c0e..9daf3143 100644 --- a/integration/user_api_test.go +++ b/integration/user_api_test.go @@ -618,7 +618,7 @@ func TestRefreshTokenEndpoints(t *testing.T) { t.Errorf("case %d: expected client ids did not match actual: %s", i, diff) } 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) } } diff --git a/schema/adminschema/README.md b/schema/adminschema/README.md index 4b1b42df..a3f0650b 100644 --- a/schema/adminschema/README.md +++ b/schema/adminschema/README.md @@ -34,7 +34,10 @@ __Version:__ v1 redirectURIs: [ string ], - secret: string // The client secret. Ignored in client create requests. + secret: string // The client secret. Ignored in client create requests., + trustedPeers: [ + string + ] } ``` diff --git a/schema/adminschema/v1-gen.go b/schema/adminschema/v1-gen.go index 472aa28e..2f897d09 100644 --- a/schema/adminschema/v1-gen.go +++ b/schema/adminschema/v1-gen.go @@ -148,6 +148,10 @@ type Client struct { // Secret: The client secret. Ignored in client create requests. 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 { diff --git a/schema/adminschema/v1-json.go b/schema/adminschema/v1-json.go index f57ef741..b600e08b 100644 --- a/schema/adminschema/v1-json.go +++ b/schema/adminschema/v1-json.go @@ -84,6 +84,13 @@ const DiscoveryJSON = `{ "clientURI": { "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 ) ." + }, + "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 = `{ } } } + ` \ No newline at end of file diff --git a/schema/adminschema/v1.json b/schema/adminschema/v1.json index 72f11249..6e868461 100644 --- a/schema/adminschema/v1.json +++ b/schema/adminschema/v1.json @@ -78,6 +78,13 @@ "clientURI": { "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 ) ." + }, + "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 @@ } } } + diff --git a/schema/workerschema/README.md b/schema/workerschema/README.md index 0e396d66..0218864a 100644 --- a/schema/workerschema/README.md +++ b/schema/workerschema/README.md @@ -199,7 +199,7 @@ A client with associated public metadata. > __Description__ -> List all clients that hold refresh tokens for the authenticated user. +> List all clients that hold refresh tokens for the specified user. > __Parameters__ @@ -221,19 +221,19 @@ A client with associated public metadata. > __Summary__ -> Revoke Clients +> Revoke RefreshClient > __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__ > |Name|Located in|Description|Required|Type| |:-----|:-----|:-----|:-----|:-----| -| userid | path | | Yes | string | | clientid | path | | Yes | string | +| userid | path | | Yes | string | > __Responses__ @@ -310,8 +310,8 @@ A client with associated public metadata. > |Name|Located in|Description|Required|Type| |:-----|:-----|:-----|:-----|:-----| -| maxResults | query | | No | integer | | nextPageToken | query | | No | string | +| maxResults | query | | No | integer | > __Responses__ diff --git a/schema/workerschema/v1-gen.go b/schema/workerschema/v1-gen.go index 9b71f95f..4a2fbd0c 100644 --- a/schema/workerschema/v1-gen.go +++ b/schema/workerschema/v1-gen.go @@ -334,82 +334,7 @@ func (c *ClientsListCall) Do() (*ClientPage, error) { } -// method id "dex.Client.Revoke": - -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": +// method id "dex.RefreshClient.List": type RefreshClientListCall struct { s *Service @@ -417,7 +342,7 @@ type RefreshClientListCall struct { 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. func (r *RefreshClientService) List(userid string) *RefreshClientListCall { c := &RefreshClientListCall{s: r.s, opt_: make(map[string]interface{})} @@ -461,9 +386,9 @@ func (c *RefreshClientListCall) Do() (*RefreshClientList, error) { } 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", - // "id": "dex.Client.List", + // "id": "dex.RefreshClient.List", // "parameterOrder": [ // "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": type UsersCreateCall struct { diff --git a/schema/workerschema/v1-json.go b/schema/workerschema/v1-json.go index 77707848..5160e89d 100644 --- a/schema/workerschema/v1-json.go +++ b/schema/workerschema/v1-json.go @@ -272,28 +272,6 @@ const DiscoveryJSON = `{ "response": { "$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": { "methods": { "List": { - "id": "dex.Client.List", - "description": "List all clients that hold refresh tokens for the authenticated user.", + "id": "dex.RefreshClient.List", + "description": "List all clients that hold refresh tokens for the specified user.", "httpMethod": "GET", "path": "account/{userid}/refresh", "parameters": { @@ -415,6 +393,28 @@ const DiscoveryJSON = `{ "response": { "$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" + } + } } } } diff --git a/schema/workerschema/v1.json b/schema/workerschema/v1.json index 76be8e57..76ca8bf5 100644 --- a/schema/workerschema/v1.json +++ b/schema/workerschema/v1.json @@ -266,28 +266,6 @@ "response": { "$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": { "methods": { "List": { - "id": "dex.Client.List", - "description": "List all clients that hold refresh tokens for the authenticated user.", + "id": "dex.RefreshClient.List", + "description": "List all clients that hold refresh tokens for the specified user.", "httpMethod": "GET", "path": "account/{userid}/refresh", "parameters": { @@ -409,6 +387,28 @@ "response": { "$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" + } + } } } } diff --git a/scope/scope.go b/scope/scope.go new file mode 100644 index 00000000..50fd266f --- /dev/null +++ b/scope/scope.go @@ -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 +} diff --git a/server/auth_middleware_test.go b/server/auth_middleware_test.go index 568f0564..0be50af7 100644 --- a/server/auth_middleware_test.go +++ b/server/auth_middleware_test.go @@ -37,7 +37,7 @@ func TestClientToken(t *testing.T) { cli := client.Client{ Metadata: clientMetadata, } - creds, err := clientManager.New(cli) + creds, err := clientManager.New(cli, nil) if err != nil { t.Fatalf("Failed to create client: %v", err) } diff --git a/server/client_registration.go b/server/client_registration.go index 0de3490a..6a95760a 100644 --- a/server/client_registration.go +++ b/server/client_registration.go @@ -42,7 +42,7 @@ func (s *Server) handleClientRegistrationRequest(r *http.Request) (*oidc.ClientR cli := client.Client{ Metadata: clientMetadata, } - creds, err := s.ClientManager.New(cli) + creds, err := s.ClientManager.New(cli, nil) if err != nil { log.Errorf("Failed to create new client identity: %v", err) return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata") diff --git a/server/client_resource.go b/server/client_resource.go index b00cbee9..b4fad488 100644 --- a/server/client_resource.go +++ b/server/client_resource.go @@ -87,7 +87,7 @@ func (c *clientResource) create(w http.ResponseWriter, r *http.Request) { writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidClientMetadata, err.Error())) return } - creds, err := c.manager.New(ci) + creds, err := c.manager.New(ci, nil) if err != nil { log.Errorf("Failed creating client: %v", err) diff --git a/server/cross_client_test.go b/server/cross_client_test.go new file mode 100644 index 00000000..4d1d4120 --- /dev/null +++ b/server/cross_client_test.go @@ -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) + } + } +} diff --git a/server/http.go b/server/http.go index a9d2bc46..542442ad 100644 --- a/server/http.go +++ b/server/http.go @@ -21,6 +21,7 @@ import ( "github.com/coreos/dex/connector" phttp "github.com/coreos/dex/pkg/http" "github.com/coreos/dex/pkg/log" + "github.com/coreos/dex/scope" ) const ( @@ -341,30 +342,9 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T } // Check scopes. - var scopes []string - foundOpenIDScope := false - for _, scope := range acr.Scope { - 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) + if scopeErr := validateScopes(srv, acr.ClientID, acr.Scope); scopeErr != nil { + log.Error(scopeErr) + writeAuthError(w, scopeErr, acr.State) 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 { return func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { diff --git a/server/http_test.go b/server/http_test.go index 0dda1847..180993d3 100644 --- a/server/http_test.go +++ b/server/http_test.go @@ -18,6 +18,7 @@ import ( "github.com/coreos/dex/client" "github.com/coreos/dex/connector" + "github.com/coreos/dex/scope" "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oauth2" "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() if err != nil { t.Fatalf("could not run test fixtures: %v", err) diff --git a/server/server.go b/server/server.go index 2b794862..8c7bd361 100644 --- a/server/server.go +++ b/server/server.go @@ -45,13 +45,19 @@ type OIDCServer interface { ClientMetadata(string) (*oidc.ClientMetadata, error) NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []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(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error) + ClientCredsToken(creds oidc.ClientCredentials) (*jose.JWT, error) + // RefreshToken takes a previously generated refresh token and returns a new ID token // if the token is valid. RefreshToken(creds oidc.ClientCredentials, token string) (*jose.JWT, error) + KillSession(string) error + + CrossClientAuthAllowed(requestingClientID, authorizingClientID string) (bool, error) } 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()) 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) if err != nil { 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 } +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 { noop := func() error { return nil } diff --git a/server/server_test.go b/server/server_test.go index f0eb30ce..1650d37f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -9,16 +9,17 @@ import ( "testing" "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/key" "github.com/coreos/go-oidc/oauth2" "github.com/coreos/go-oidc/oidc" "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{ @@ -266,6 +267,12 @@ func TestServerLoginDisabledUser(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 { scope []string refreshToken string @@ -277,21 +284,14 @@ func TestServerCodeToken(t *testing.T) { }, // Have 'offline_access' in scope, should get non-empty refresh token. { - // NOTE(ericchiang): This test assumes that the database ID of the first - // refresh token will be "1". + // NOTE(ericchiang): This test assumes that the database ID of the + // first refresh token will be "1". scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, } 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) if err != nil { 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) } - jwt, token, err := f.srv.CodeToken( - oidc.ClientCredentials{ - ID: testClientID, - Secret: clientTestSecret, - }, key) + jwt, token, err := f.srv.CodeToken(oidc.ClientCredentials{ + ID: testClientID, + Secret: clientTestSecret}, key) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } diff --git a/server/testutil.go b/server/testutil.go index be794a44..2a4ce254 100644 --- a/server/testutil.go +++ b/server/testutil.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/dex/connector" "github.com/coreos/dex/db" "github.com/coreos/dex/email" + "github.com/coreos/dex/refresh/refreshtest" sessionmanager "github.com/coreos/dex/session/manager" "github.com/coreos/dex/user" useremail "github.com/coreos/dex/user/email" @@ -83,6 +84,11 @@ var ( } testPrivKey, _ = key.GeneratePrivateKey() + + testClientCreds = oidc.ClientCredentials{ + ID: testClientID, + Secret: base64.URLEncoding.EncodeToString([]byte("secret")), + } ) type testFixtures struct { @@ -93,6 +99,7 @@ type testFixtures struct { redirectURL url.URL clientRepo client.ClientRepo clientManager *clientmanager.ClientManager + clientCreds map[string]oidc.ClientCredentials } 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.GenerateCode = sequentialGenerateCodeFunc() + refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() + emailer, err := email.NewTemplatizedEmailerFromGlobs( emailTemplatesLocation+"/*.txt", emailTemplatesLocation+"/*.html", @@ -210,6 +219,7 @@ func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, err UserManager: userManager, ClientManager: clientManager, KeyManager: km, + RefreshTokenRepo: refreshTokenRepo, } err = setTemplates(srv, tpl) @@ -243,5 +253,8 @@ func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, err emailer: emailer, clientRepo: clientRepo, clientManager: clientManager, + clientCreds: map[string]oidc.ClientCredentials{ + testClientID: testClientCreds, + }, }, nil } diff --git a/session/session.go b/session/session.go index ab35cfd4..050d60a8 100644 --- a/session/session.go +++ b/session/session.go @@ -6,6 +6,8 @@ import ( "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oidc" + + "github.com/coreos/dex/scope" ) const ( @@ -46,11 +48,13 @@ type Session struct { // Regsiter indicates that this session is a registration flow. 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 - // Scope is the 'scope' field in the authentication request. Example scopes are 'openid', 'email', 'offline', etc. - Scope []string + // Scope is the 'scope' field in the authentication request. Example scopes + // are 'openid', 'email', 'offline', etc. + Scope scope.Scopes } // Claims returns a new set of Claims for the current session.