diff --git a/admin/api.go b/admin/api.go index 2329c3ba..d65c71f9 100644 --- a/admin/api.go +++ b/admin/api.go @@ -4,6 +4,11 @@ package admin import ( "net/http" + "github.com/coreos/go-oidc/oidc" + "github.com/go-gorp/gorp" + + "github.com/coreos/dex/client" + "github.com/coreos/dex/db" "github.com/coreos/dex/schema/adminschema" "github.com/coreos/dex/user" "github.com/coreos/dex/user/manager" @@ -11,22 +16,25 @@ import ( // AdminAPI provides the logic necessary to implement the Admin API. type AdminAPI struct { - userManager *manager.UserManager - userRepo user.UserRepo - passwordInfoRepo user.PasswordInfoRepo - localConnectorID string + userManager *manager.UserManager + userRepo user.UserRepo + passwordInfoRepo user.PasswordInfoRepo + clientIdentityRepo client.ClientIdentityRepo + localConnectorID string } -func NewAdminAPI(userManager *manager.UserManager, userRepo user.UserRepo, pwiRepo user.PasswordInfoRepo, localConnectorID string) *AdminAPI { +// TODO(ericchiang): Swap the DbMap for a storage interface. See #278 + +func NewAdminAPI(dbMap *gorp.DbMap, userManager *manager.UserManager, localConnectorID string) *AdminAPI { if localConnectorID == "" { panic("must specify non-blank localConnectorID") } - return &AdminAPI{ - userManager: userManager, - userRepo: userRepo, - passwordInfoRepo: pwiRepo, - localConnectorID: localConnectorID, + userManager: userManager, + userRepo: db.NewUserRepo(dbMap), + passwordInfoRepo: db.NewPasswordInfoRepo(dbMap), + clientIdentityRepo: db.NewClientIdentityRepo(dbMap), + localConnectorID: localConnectorID, } } @@ -108,6 +116,27 @@ func (a *AdminAPI) GetState() (adminschema.State, error) { return state, nil } +type ClientRegistrationRequest struct { + IsAdmin bool `json:"isAdmin"` + Client oidc.ClientMetadata `json:"client"` +} + +func (a *AdminAPI) CreateClient(req ClientRegistrationRequest) (oidc.ClientRegistrationResponse, error) { + if err := req.Client.Valid(); err != nil { + return oidc.ClientRegistrationResponse{}, mapError(err) + } + // metadata is guarenteed to have at least one redirect_uri by earlier validation. + id, err := oidc.GenClientID(req.Client.RedirectURIs[0].Host) + if err != nil { + return oidc.ClientRegistrationResponse{}, mapError(err) + } + c, err := a.clientIdentityRepo.New(id, req.Client, req.IsAdmin) + if err != nil { + return oidc.ClientRegistrationResponse{}, mapError(err) + } + return oidc.ClientRegistrationResponse{ClientID: c.ID, ClientSecret: c.Secret, ClientMetadata: req.Client}, nil +} + func mapError(e error) error { if mapped, ok := errorMap[e]; ok { return mapped(e) diff --git a/admin/api_test.go b/admin/api_test.go index 4409e2b7..165d8ff5 100644 --- a/admin/api_test.go +++ b/admin/api_test.go @@ -69,7 +69,7 @@ func makeTestFixtures() *testFixtures { }() f.mgr = manager.NewUserManager(f.ur, f.pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) - f.adAPI = NewAdminAPI(f.mgr, f.ur, f.pwr, "local") + f.adAPI = NewAdminAPI(dbMap, f.mgr, "local") return f } diff --git a/client/client.go b/client/client.go index e8d8b194..20531603 100644 --- a/client/client.go +++ b/client/client.go @@ -33,7 +33,7 @@ type ClientIdentityRepo interface { // New registers a ClientIdentity with the repo for the given metadata. // An unused ID must be provided. A corresponding secret will be returned // in a ClientCredentials struct along with the provided ID. - New(id string, meta oidc.ClientMetadata) (*oidc.ClientCredentials, error) + New(id string, meta oidc.ClientMetadata, admin bool) (*oidc.ClientCredentials, error) SetDexAdmin(clientID string, isAdmin bool) error diff --git a/cmd/dex-overlord/main.go b/cmd/dex-overlord/main.go index 1d3eb5de..8dad8aa0 100644 --- a/cmd/dex-overlord/main.go +++ b/cmd/dex-overlord/main.go @@ -113,13 +113,13 @@ func main() { time.Sleep(sleep) } } - userRepo := db.NewUserRepo(dbc) pwiRepo := db.NewPasswordInfoRepo(dbc) connCfgRepo := db.NewConnectorConfigRepo(dbc) userManager := manager.NewUserManager(userRepo, pwiRepo, connCfgRepo, db.TransactionFactory(dbc), manager.ManagerOptions{}) - adminAPI := admin.NewAdminAPI(userManager, userRepo, pwiRepo, *localConnectorID) + + adminAPI := admin.NewAdminAPI(dbc, userManager, *localConnectorID) kRepo, err := db.NewPrivateKeySetRepo(dbc, *useOldFormat, keySecrets.BytesSlice()...) if err != nil { log.Fatalf(err.Error()) diff --git a/cmd/dexctl/driver_db.go b/cmd/dexctl/driver_db.go index 7f61092a..fe918aa1 100644 --- a/cmd/dexctl/driver_db.go +++ b/cmd/dexctl/driver_db.go @@ -36,7 +36,7 @@ func (d *dbDriver) NewClient(meta oidc.ClientMetadata) (*oidc.ClientCredentials, return nil, err } - return d.ciRepo.New(clientID, meta) + return d.ciRepo.New(clientID, meta, false) } func (d *dbDriver) ConnectorConfigs() ([]connector.ConnectorConfig, error) { diff --git a/db/client.go b/db/client.go index 6754368a..62187034 100644 --- a/db/client.go +++ b/db/client.go @@ -234,7 +234,7 @@ func isAlreadyExistsErr(err error) bool { return false } -func (r *clientIdentityRepo) New(id string, meta oidc.ClientMetadata) (*oidc.ClientCredentials, error) { +func (r *clientIdentityRepo) New(id string, meta oidc.ClientMetadata, admin bool) (*oidc.ClientCredentials, error) { secret, err := pcrypto.RandBytes(maxSecretLength) if err != nil { return nil, err @@ -244,6 +244,7 @@ func (r *clientIdentityRepo) New(id string, meta oidc.ClientMetadata) (*oidc.Cli if err != nil { return nil, err } + cim.DexAdmin = admin if err := r.executor(nil).Insert(cim); err != nil { if isAlreadyExistsErr(err) { diff --git a/functional/db_test.go b/functional/db_test.go index 29a7ae7c..97efdfe5 100644 --- a/functional/db_test.go +++ b/functional/db_test.go @@ -191,7 +191,7 @@ func TestDBClientIdentityRepoMetadata(t *testing.T) { }, } - _, err := r.New("foo", cm) + _, err := r.New("foo", cm, false) if err != nil { t.Fatalf(err.Error()) } @@ -227,7 +227,7 @@ func TestDBClientIdentityRepoNewDuplicate(t *testing.T) { }, } - if _, err := r.New("foo", meta1); err != nil { + if _, err := r.New("foo", meta1, false); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -237,7 +237,7 @@ func TestDBClientIdentityRepoNewDuplicate(t *testing.T) { }, } - if _, err := r.New("foo", meta2); err == nil { + if _, err := r.New("foo", meta2, false); err == nil { t.Fatalf("expected non-nil error") } } @@ -251,7 +251,7 @@ func TestDBClientIdentityRepoAuthenticate(t *testing.T) { }, } - cc, err := r.New("baz", cm) + cc, err := r.New("baz", cm, false) if err != nil { t.Fatalf(err.Error()) } @@ -299,7 +299,7 @@ func TestDBClientIdentityAll(t *testing.T) { }, } - _, err := r.New("foo", cm) + _, err := r.New("foo", cm, false) if err != nil { t.Fatalf(err.Error()) } @@ -322,7 +322,7 @@ func TestDBClientIdentityAll(t *testing.T) { url.URL{Scheme: "http", Host: "foo.com", Path: "/cb"}, }, } - _, err = r.New("bar", cm) + _, err = r.New("bar", cm, false) if err != nil { t.Fatalf(err.Error()) } diff --git a/integration/admin_api_test.go b/integration/admin_api_test.go index 337eccec..861e9788 100644 --- a/integration/admin_api_test.go +++ b/integration/admin_api_test.go @@ -1,8 +1,10 @@ package integration import ( + "errors" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/kylelemons/godebug/pretty" @@ -12,6 +14,7 @@ import ( "github.com/coreos/dex/schema/adminschema" "github.com/coreos/dex/server" "github.com/coreos/dex/user" + "github.com/coreos/go-oidc/oidc" ) const ( @@ -74,10 +77,10 @@ func (a *adminAPITransport) RoundTrip(r *http.Request) (*http.Response, error) { func makeAdminAPITestFixtures() *adminAPITestFixtures { f := &adminAPITestFixtures{} - ur, pwr, um := makeUserObjects(adminUsers, adminPasswords) + dbMap, ur, pwr, um := makeUserObjects(adminUsers, adminPasswords) f.ur = ur f.pwr = pwr - f.adAPI = admin.NewAdminAPI(um, f.ur, f.pwr, "local") + f.adAPI = admin.NewAdminAPI(dbMap, um, "local") f.adSrv = server.NewAdminServer(f.adAPI, nil, adminAPITestSecret) f.hSrv = httptest.NewServer(f.adSrv.HTTPHandler()) f.hc = &http.Client{ @@ -252,6 +255,52 @@ func TestCreateAdmin(t *testing.T) { } } +func TestCreateClient(t *testing.T) { + tests := []struct { + client oidc.ClientMetadata + wantError bool + }{ + { + client: oidc.ClientMetadata{}, + wantError: true, + }, + { + client: oidc.ClientMetadata{ + RedirectURIs: []url.URL{ + {Scheme: "https", Host: "auth.example.com", Path: "/"}, + }, + }, + }, + } + + for i, tt := range tests { + err := func() error { + f := makeAdminAPITestFixtures() + req := &adminschema.ClientCreateRequestClient{} + for _, redirectURI := range tt.client.RedirectURIs { + req.Redirect_uris = append(req.Redirect_uris, redirectURI.String()) + } + resp, err := f.adClient.Client.Create(&adminschema.ClientCreateRequest{Client: req}).Do() + if err != nil { + if tt.wantError { + return nil + } + return err + } + if resp.Client_id == "" { + return errors.New("no client id returned") + } + if resp.Client_secret == "" { + return errors.New("no client secret returned") + } + return nil + }() + if err != nil { + t.Errorf("case %d: %v", i, err) + } + } +} + func TestGetState(t *testing.T) { tests := []struct { addUsers []user.User diff --git a/integration/common_test.go b/integration/common_test.go index f7fe429b..aeb71391 100644 --- a/integration/common_test.go +++ b/integration/common_test.go @@ -9,6 +9,7 @@ import ( "net/url" "github.com/coreos/go-oidc/key" + "github.com/go-gorp/gorp" "github.com/jonboulle/clockwork" "github.com/coreos/dex/connector" @@ -45,7 +46,9 @@ func (t *tokenHandlerTransport) RoundTrip(r *http.Request) (*http.Response, erro return &resp, nil } -func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.PasswordInfo) (user.UserRepo, user.PasswordInfoRepo, *manager.UserManager) { +// TODO(ericchiang): Replace DbMap with storage interface. See #278 + +func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.PasswordInfo) (*gorp.DbMap, user.UserRepo, user.PasswordInfoRepo, *manager.UserManager) { dbMap := db.NewMemDB() ur := func() user.UserRepo { repo, err := db.NewUserRepoFromUsers(dbMap, users) @@ -73,5 +76,5 @@ func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.Pas um := manager.NewUserManager(ur, pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) um.Clock = clock - return ur, pwr, um + return dbMap, ur, pwr, um } diff --git a/integration/user_api_test.go b/integration/user_api_test.go index 687d32f5..f2f4758f 100644 --- a/integration/user_api_test.go +++ b/integration/user_api_test.go @@ -99,7 +99,7 @@ var ( func makeUserAPITestFixtures() *userAPITestFixtures { f := &userAPITestFixtures{} - _, _, um := makeUserObjects(userUsers, userPasswords) + _, _, _, um := makeUserObjects(userUsers, userPasswords) cir := func() client.ClientIdentityRepo { repo, err := db.NewClientIdentityRepoFromClients(db.NewMemDB(), []oidc.ClientIdentity{ diff --git a/schema/adminschema/README.md b/schema/adminschema/README.md index cdb7bb89..c16da394 100644 --- a/schema/adminschema/README.md +++ b/schema/adminschema/README.md @@ -20,6 +20,35 @@ __Version:__ v1 } ``` +### ClientCreateRequest + +A request to register a client with dex. + +``` +{ + client: { + client_name: string // OPTIONAL. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ., + client_uri: string // 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 ) ., + logo_uri: string // OPTIONAL. URL that references a logo for the Client application. If present, the server SHOULD display this image to the End-User during approval. The value of this field MUST point to a valid image file. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ., + redirect_uris: [ + string + ] + }, + isAdmin: boolean +} +``` + +### ClientRegistrationResponse + +Upon successful registration, an ID and secret is assigned to the client. + +``` +{ + client_id: string, + client_secret: string +} +``` + ### State @@ -86,6 +115,32 @@ __Version:__ v1 | default | Unexpected error | | +### POST /client + +> __Summary__ + +> Create Client + +> __Description__ + +> Register an OpenID Connect client. + + +> __Parameters__ + +> |Name|Located in|Description|Required|Type| +|:-----|:-----|:-----|:-----|:-----| +| | body | | Yes | [ClientCreateRequest](#clientcreaterequest) | + + +> __Responses__ + +> |Code|Description|Type| +|:-----|:-----|:-----| +| 200 | | [ClientRegistrationResponse](#clientregistrationresponse) | +| default | Unexpected error | | + + ### GET /state > __Summary__ diff --git a/schema/adminschema/v1-gen.go b/schema/adminschema/v1-gen.go index 7e7549aa..8b276158 100644 --- a/schema/adminschema/v1-gen.go +++ b/schema/adminschema/v1-gen.go @@ -46,6 +46,7 @@ func New(client *http.Client) (*Service, error) { } s := &Service{client: client, BasePath: basePath} s.Admin = NewAdminService(s) + s.Client = NewClientService(s) s.State = NewStateService(s) return s, nil } @@ -56,6 +57,8 @@ type Service struct { Admin *AdminService + Client *ClientService + State *StateService } @@ -68,6 +71,15 @@ type AdminService struct { s *Service } +func NewClientService(s *Service) *ClientService { + rs := &ClientService{s: s} + return rs +} + +type ClientService struct { + s *Service +} + func NewStateService(s *Service) *StateService { rs := &StateService{s: s} return rs @@ -85,6 +97,51 @@ type Admin struct { Password string `json:"password,omitempty"` } +type ClientCreateRequest struct { + Client *ClientCreateRequestClient `json:"client,omitempty"` + + IsAdmin bool `json:"isAdmin,omitempty"` +} + +type ClientCreateRequestClient struct { + // Client_name: OPTIONAL. Name of the Client to be presented to the + // End-User. If desired, representation of this Claim in different + // languages and scripts is represented as described in Section 2.1 ( + // Metadata Languages and Scripts ) . + Client_name string `json:"client_name,omitempty"` + + // Client_uri: 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 ) . + Client_uri string `json:"client_uri,omitempty"` + + // Logo_uri: OPTIONAL. URL that references a logo for the Client + // application. If present, the server SHOULD display this image to the + // End-User during approval. The value of this field MUST point to a + // valid image file. If desired, representation of this Claim in + // different languages and scripts is represented as described in + // Section 2.1 ( Metadata Languages and Scripts ) . + Logo_uri string `json:"logo_uri,omitempty"` + + // Redirect_uris: REQUIRED. Array of Redirection URI values used by the + // Client. One of these registered Redirection URI values MUST exactly + // match the redirect_uri parameter value used in each Authorization + // Request, with the matching performed as described in Section 6.2.1 of + // [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, + // “Uniform Resource Identifier (URI): Generic Syntax,” January + // 2005. ) (Simple String Comparison). + Redirect_uris []string `json:"redirect_uris,omitempty"` +} + +type ClientRegistrationResponse struct { + Client_id string `json:"client_id,omitempty"` + + Client_secret string `json:"client_secret,omitempty"` +} + type State struct { AdminUserCreated bool `json:"AdminUserCreated,omitempty"` } @@ -230,6 +287,75 @@ func (c *AdminGetCall) Do() (*Admin, error) { } +// method id "dex.admin.Client.Create": + +type ClientCreateCall struct { + s *Service + clientcreaterequest *ClientCreateRequest + opt_ map[string]interface{} +} + +// Create: Register an OpenID Connect client. +func (r *ClientService) Create(clientcreaterequest *ClientCreateRequest) *ClientCreateCall { + c := &ClientCreateCall{s: r.s, opt_: make(map[string]interface{})} + c.clientcreaterequest = clientcreaterequest + 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 *ClientCreateCall) Fields(s ...googleapi.Field) *ClientCreateCall { + c.opt_["fields"] = googleapi.CombineFields(s) + return c +} + +func (c *ClientCreateCall) Do() (*ClientRegistrationResponse, error) { + var body io.Reader = nil + body, err := googleapi.WithoutDataWrapper.JSONReader(c.clientcreaterequest) + if err != nil { + return nil, err + } + ctype := "application/json" + 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, "client") + urls += "?" + params.Encode() + req, _ := http.NewRequest("POST", urls, body) + googleapi.SetOpaque(req.URL) + req.Header.Set("Content-Type", ctype) + req.Header.Set("User-Agent", "google-api-go-client/0.5") + res, err := c.s.client.Do(req) + if err != nil { + return nil, err + } + defer googleapi.CloseBody(res) + if err := googleapi.CheckResponse(res); err != nil { + return nil, err + } + var ret *ClientRegistrationResponse + if err := json.NewDecoder(res.Body).Decode(&ret); err != nil { + return nil, err + } + return ret, nil + // { + // "description": "Register an OpenID Connect client.", + // "httpMethod": "POST", + // "id": "dex.admin.Client.Create", + // "path": "client", + // "request": { + // "$ref": "ClientCreateRequest" + // }, + // "response": { + // "$ref": "ClientRegistrationResponse" + // } + // } + +} + // method id "dex.admin.State.Get": type StateGetCall struct { diff --git a/schema/adminschema/v1-json.go b/schema/adminschema/v1-json.go index 67a8d158..f99dfb71 100644 --- a/schema/adminschema/v1-json.go +++ b/schema/adminschema/v1-json.go @@ -50,6 +50,53 @@ const DiscoveryJSON = `{ "type": "boolean" } } + }, + "ClientCreateRequest": { + "id": "ClientCreateRequest", + "type": "object", + "description": "A request to register a client with dex.", + "properties": { + "isAdmin": { + "type": "boolean" + }, + "client": { + "type": "object", + "properties": { + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + }, + "description": "REQUIRED. Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” January 2005. ) (Simple String Comparison)." + }, + "client_name": { + "type": "string", + "description": "OPTIONAL. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ." + }, + "logo_uri": { + "type": "string", + "description": "OPTIONAL. URL that references a logo for the Client application. If present, the server SHOULD display this image to the End-User during approval. The value of this field MUST point to a valid image file. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ." + }, + "client_uri": { + "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 ) ." + } + } + } + } + }, + "ClientRegistrationResponse": { + "id": "ClientRegistrationResponse", + "type": "object", + "description": "Upon successful registration, an ID and secret is assigned to the client.", + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + } } }, "resources": { @@ -101,6 +148,22 @@ const DiscoveryJSON = `{ } } } + }, + "Client": { + "methods": { + "Create": { + "id": "dex.admin.Client.Create", + "description": "Register an OpenID Connect client.", + "httpMethod": "POST", + "path": "client", + "request": { + "$ref": "ClientCreateRequest" + }, + "response": { + "$ref": "ClientRegistrationResponse" + } + } + } } } } diff --git a/schema/adminschema/v1.json b/schema/adminschema/v1.json index 52fe0662..f7719620 100644 --- a/schema/adminschema/v1.json +++ b/schema/adminschema/v1.json @@ -44,6 +44,53 @@ "type": "boolean" } } + }, + "ClientCreateRequest": { + "id": "ClientCreateRequest", + "type": "object", + "description": "A request to register a client with dex.", + "properties": { + "isAdmin": { + "type": "boolean" + }, + "client": { + "type": "object", + "properties": { + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + }, + "description": "REQUIRED. Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” January 2005. ) (Simple String Comparison)." + }, + "client_name": { + "type": "string", + "description": "OPTIONAL. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ." + }, + "logo_uri": { + "type": "string", + "description": "OPTIONAL. URL that references a logo for the Client application. If present, the server SHOULD display this image to the End-User during approval. The value of this field MUST point to a valid image file. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ." + }, + "client_uri": { + "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 ) ." + } + } + } + } + }, + "ClientRegistrationResponse": { + "id": "ClientRegistrationResponse", + "type": "object", + "description": "Upon successful registration, an ID and secret is assigned to the client.", + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + } } }, "resources": { @@ -95,6 +142,22 @@ } } } + }, + "Client": { + "methods": { + "Create": { + "id": "dex.admin.Client.Create", + "description": "Register an OpenID Connect client.", + "httpMethod": "POST", + "path": "client", + "request": { + "$ref": "ClientCreateRequest" + }, + "response": { + "$ref": "ClientRegistrationResponse" + } + } + } } } } diff --git a/server/admin.go b/server/admin.go index 8295dfe6..21bd28d5 100644 --- a/server/admin.go +++ b/server/admin.go @@ -20,9 +20,10 @@ const ( ) var ( - AdminGetEndpoint = addBasePath("/admin/:id") - AdminCreateEndpoint = addBasePath("/admin") - AdminGetStateEndpoint = addBasePath("/state") + AdminGetEndpoint = addBasePath("/admin/:id") + AdminCreateEndpoint = addBasePath("/admin") + AdminGetStateEndpoint = addBasePath("/state") + AdminCreateClientEndpoint = addBasePath("/client") ) // AdminServer serves the admin API. @@ -49,6 +50,7 @@ func (s *AdminServer) HTTPHandler() http.Handler { r.GET(AdminGetEndpoint, s.getAdmin) r.POST(AdminCreateEndpoint, s.createAdmin) r.GET(AdminGetStateEndpoint, s.getState) + r.POST(AdminCreateClientEndpoint, s.createClient) r.Handler("GET", httpPathHealth, s.checker) r.HandlerFunc("GET", httpPathDebugVars, health.ExpvarHandler) @@ -113,6 +115,21 @@ func (s *AdminServer) getState(w http.ResponseWriter, r *http.Request, ps httpro writeResponseWithBody(w, http.StatusOK, state) } +func (s *AdminServer) createClient(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + var req admin.ClientRegistrationRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeInvalidRequest(w, "cannot parse JSON body") + return + } + + resp, err := s.adminAPI.CreateClient(req) + if err != nil { + s.writeError(w, err) + return + } + writeResponseWithBody(w, http.StatusOK, &resp) +} + func (s *AdminServer) writeError(w http.ResponseWriter, err error) { log.Errorf("Error calling admin API: %v: ", err) if adminErr, ok := err.(admin.Error); ok { diff --git a/server/client_registration.go b/server/client_registration.go index f53cc90d..13dacbeb 100644 --- a/server/client_registration.go +++ b/server/client_registration.go @@ -43,7 +43,7 @@ func (s *Server) handleClientRegistrationRequest(r *http.Request) (*oidc.ClientR return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata") } - creds, err := s.ClientIdentityRepo.New(id, clientMetadata) + creds, err := s.ClientIdentityRepo.New(id, clientMetadata, false) 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 45f7027b..c5134779 100644 --- a/server/client_resource.go +++ b/server/client_resource.go @@ -96,7 +96,7 @@ func (c *clientResource) create(w http.ResponseWriter, r *http.Request) { return } - creds, err := c.repo.New(clientID, ci.Metadata) + creds, err := c.repo.New(clientID, ci.Metadata, false) if err != nil { log.Errorf("Failed creating client: %v", err) writeAPIError(w, http.StatusInternalServerError, newAPIError(errorServerError, "unable to create client"))