Repository: dexidp/dex
Pull request #3245 (2023-12-27 09:12:36)

gRPC Connectors API
#### Overview

I added gRPC API support for connectors (CRUD operations)

#### What this PR does / why we need it

I think it could be useful to have the possibility of handling connectors dynamically without the need to restart Dex.

#### Special notes for your reviewer

- added a Config.Parse() function to prevent AdditionalFeatures field from being nil (default = empty slice)
- added a list of ValidAdditionalFeaturs to prevent invalid features from being provided and making detecting typos easier
-  addtionalFeatures slice is passed down to the API struct, so new features that affect apis can be easily added

#### Does this PR introduce a user-facing change?

It adds connectors functions and types to the gRPC client, but it shouldn't break anything.

```release-note
- Implemented additionalFeatures config entry (list)
- Implemented ConnectorsCRUD as additional feature
- Updated examples/config.dev.yaml file 
- Added gRPC API support for connectors CRUD operations
- Missing ConnectorsCRUD in additionalFeatures in config file will cause connectors gRPC functions to return an error
- Connectors configs are of the bytes type in .proto file and are checked for JSON validity when inserting and updating a connector
- All fields (id, name, type, config) are mandatory when inserting a new connector
```


twoojoo: hi @nabokihms, this time it should pass. I tried running that workflow in a separate branch.
twoojoo: I think that it shouldn't affect a standard usage of Dex (from what I know about it), but I totally understand your concern. Wouldn't increasing the minor version be sufficient as a way to alert developers? After all, it's a backward-compatible change, although it depends on what is meant by "backward compatibility."
twoojoo: Could adding an 'allowEditConnectors' (false if missing) in the grpc section of the config file be a solution? 
nabokihms: Yeah, something like that will work. I assume it is better to make it the list of features instead of a boolean flag, e.g., `additionalFeatures: ["ConnectorsCRUD"]`.

In the future, we will be able to extend the API server with more features.
twoojoo: hi @nabokihms , is there any way I could help move the PR forward? I am available to correct any inaccuracies or to add any missing elements
nabokihms: @twoojoo, everything looks ok. I'm ready to merge this as soon as the test/lint problem is fixed.

Patch:
diff --git a/api/v2/api.pb.go b/api/v2/api.pb.go
index 2d08ee85..3f6e8e99 100644
--- a/api/v2/api.pb.go
+++ b/api/v2/api.pb.go
@@ -1007,6 +1007,478 @@ func (x *ListPasswordResp) GetPasswords() []*Password {
 	return nil
 }
 
+// Connector is a strategy used by Dex for authenticating a user against another identity provider
+type Connector struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Id     string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	Type   string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	Name   string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"`
+}
+
+func (x *Connector) Reset() {
+	*x = Connector{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[18]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Connector) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Connector) ProtoMessage() {}
+
+func (x *Connector) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[18]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Connector.ProtoReflect.Descriptor instead.
+func (*Connector) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *Connector) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+func (x *Connector) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *Connector) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Connector) GetConfig() []byte {
+	if x != nil {
+		return x.Config
+	}
+	return nil
+}
+
+// CreateConnectorReq is a request to make a connector.
+type CreateConnectorReq struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Connector *Connector `protobuf:"bytes,1,opt,name=connector,proto3" json:"connector,omitempty"`
+}
+
+func (x *CreateConnectorReq) Reset() {
+	*x = CreateConnectorReq{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[19]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CreateConnectorReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateConnectorReq) ProtoMessage() {}
+
+func (x *CreateConnectorReq) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[19]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateConnectorReq.ProtoReflect.Descriptor instead.
+func (*CreateConnectorReq) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *CreateConnectorReq) GetConnector() *Connector {
+	if x != nil {
+		return x.Connector
+	}
+	return nil
+}
+
+// CreateConnectorResp returns the response from creating a connector.
+type CreateConnectorResp struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"`
+}
+
+func (x *CreateConnectorResp) Reset() {
+	*x = CreateConnectorResp{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[20]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CreateConnectorResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateConnectorResp) ProtoMessage() {}
+
+func (x *CreateConnectorResp) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[20]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateConnectorResp.ProtoReflect.Descriptor instead.
+func (*CreateConnectorResp) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *CreateConnectorResp) GetAlreadyExists() bool {
+	if x != nil {
+		return x.AlreadyExists
+	}
+	return false
+}
+
+// UpdateConnectorReq is a request to modify an existing connector.
+type UpdateConnectorReq struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The id used to lookup the connector. This field cannot be modified
+	Id        string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	NewType   string `protobuf:"bytes,2,opt,name=new_type,json=newType,proto3" json:"new_type,omitempty"`
+	NewName   string `protobuf:"bytes,3,opt,name=new_name,json=newName,proto3" json:"new_name,omitempty"`
+	NewConfig []byte `protobuf:"bytes,4,opt,name=new_config,json=newConfig,proto3" json:"new_config,omitempty"`
+}
+
+func (x *UpdateConnectorReq) Reset() {
+	*x = UpdateConnectorReq{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[21]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *UpdateConnectorReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateConnectorReq) ProtoMessage() {}
+
+func (x *UpdateConnectorReq) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[21]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateConnectorReq.ProtoReflect.Descriptor instead.
+func (*UpdateConnectorReq) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{21}
+}
+
+func (x *UpdateConnectorReq) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+func (x *UpdateConnectorReq) GetNewType() string {
+	if x != nil {
+		return x.NewType
+	}
+	return ""
+}
+
+func (x *UpdateConnectorReq) GetNewName() string {
+	if x != nil {
+		return x.NewName
+	}
+	return ""
+}
+
+func (x *UpdateConnectorReq) GetNewConfig() []byte {
+	if x != nil {
+		return x.NewConfig
+	}
+	return nil
+}
+
+// UpdateConnectorResp returns the response from modifying an existing connector.
+type UpdateConnectorResp struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"`
+}
+
+func (x *UpdateConnectorResp) Reset() {
+	*x = UpdateConnectorResp{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[22]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *UpdateConnectorResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateConnectorResp) ProtoMessage() {}
+
+func (x *UpdateConnectorResp) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[22]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateConnectorResp.ProtoReflect.Descriptor instead.
+func (*UpdateConnectorResp) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{22}
+}
+
+func (x *UpdateConnectorResp) GetNotFound() bool {
+	if x != nil {
+		return x.NotFound
+	}
+	return false
+}
+
+// DeleteConnectorReq is a request to delete a connector.
+type DeleteConnectorReq struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *DeleteConnectorReq) Reset() {
+	*x = DeleteConnectorReq{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[23]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DeleteConnectorReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteConnectorReq) ProtoMessage() {}
+
+func (x *DeleteConnectorReq) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[23]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteConnectorReq.ProtoReflect.Descriptor instead.
+func (*DeleteConnectorReq) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{23}
+}
+
+func (x *DeleteConnectorReq) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+// DeleteConnectorResp returns the response from deleting a connector.
+type DeleteConnectorResp struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"`
+}
+
+func (x *DeleteConnectorResp) Reset() {
+	*x = DeleteConnectorResp{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[24]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DeleteConnectorResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteConnectorResp) ProtoMessage() {}
+
+func (x *DeleteConnectorResp) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[24]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteConnectorResp.ProtoReflect.Descriptor instead.
+func (*DeleteConnectorResp) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *DeleteConnectorResp) GetNotFound() bool {
+	if x != nil {
+		return x.NotFound
+	}
+	return false
+}
+
+// ListConnectorReq is a request to enumerate connectors.
+type ListConnectorReq struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *ListConnectorReq) Reset() {
+	*x = ListConnectorReq{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[25]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListConnectorReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListConnectorReq) ProtoMessage() {}
+
+func (x *ListConnectorReq) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[25]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListConnectorReq.ProtoReflect.Descriptor instead.
+func (*ListConnectorReq) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{25}
+}
+
+// ListConnectorResp returns a list of connectors.
+type ListConnectorResp struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Connectors []*Connector `protobuf:"bytes,1,rep,name=connectors,proto3" json:"connectors,omitempty"`
+}
+
+func (x *ListConnectorResp) Reset() {
+	*x = ListConnectorResp{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_v2_api_proto_msgTypes[26]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListConnectorResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListConnectorResp) ProtoMessage() {}
+
+func (x *ListConnectorResp) ProtoReflect() protoreflect.Message {
+	mi := &file_api_v2_api_proto_msgTypes[26]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListConnectorResp.ProtoReflect.Descriptor instead.
+func (*ListConnectorResp) Descriptor() ([]byte, []int) {
+	return file_api_v2_api_proto_rawDescGZIP(), []int{26}
+}
+
+func (x *ListConnectorResp) GetConnectors() []*Connector {
+	if x != nil {
+		return x.Connectors
+	}
+	return nil
+}
+
 // VersionReq is a request to fetch version info.
 type VersionReq struct {
 	state         protoimpl.MessageState
@@ -1017,7 +1489,7 @@ type VersionReq struct {
 func (x *VersionReq) Reset() {
 	*x = VersionReq{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[18]
+		mi := &file_api_v2_api_proto_msgTypes[27]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1030,7 +1502,7 @@ func (x *VersionReq) String() string {
 func (*VersionReq) ProtoMessage() {}
 
 func (x *VersionReq) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[18]
+	mi := &file_api_v2_api_proto_msgTypes[27]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1043,7 +1515,7 @@ func (x *VersionReq) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use VersionReq.ProtoReflect.Descriptor instead.
 func (*VersionReq) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{18}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{27}
 }
 
 // VersionResp holds the version info of components.
@@ -1062,7 +1534,7 @@ type VersionResp struct {
 func (x *VersionResp) Reset() {
 	*x = VersionResp{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[19]
+		mi := &file_api_v2_api_proto_msgTypes[28]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1075,7 +1547,7 @@ func (x *VersionResp) String() string {
 func (*VersionResp) ProtoMessage() {}
 
 func (x *VersionResp) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[19]
+	mi := &file_api_v2_api_proto_msgTypes[28]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1088,7 +1560,7 @@ func (x *VersionResp) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use VersionResp.ProtoReflect.Descriptor instead.
 func (*VersionResp) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{19}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{28}
 }
 
 func (x *VersionResp) GetServer() string {
@@ -1121,7 +1593,7 @@ type RefreshTokenRef struct {
 func (x *RefreshTokenRef) Reset() {
 	*x = RefreshTokenRef{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[20]
+		mi := &file_api_v2_api_proto_msgTypes[29]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1134,7 +1606,7 @@ func (x *RefreshTokenRef) String() string {
 func (*RefreshTokenRef) ProtoMessage() {}
 
 func (x *RefreshTokenRef) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[20]
+	mi := &file_api_v2_api_proto_msgTypes[29]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1147,7 +1619,7 @@ func (x *RefreshTokenRef) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RefreshTokenRef.ProtoReflect.Descriptor instead.
 func (*RefreshTokenRef) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{20}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{29}
 }
 
 func (x *RefreshTokenRef) GetId() string {
@@ -1191,7 +1663,7 @@ type ListRefreshReq struct {
 func (x *ListRefreshReq) Reset() {
 	*x = ListRefreshReq{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[21]
+		mi := &file_api_v2_api_proto_msgTypes[30]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1204,7 +1676,7 @@ func (x *ListRefreshReq) String() string {
 func (*ListRefreshReq) ProtoMessage() {}
 
 func (x *ListRefreshReq) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[21]
+	mi := &file_api_v2_api_proto_msgTypes[30]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1217,7 +1689,7 @@ func (x *ListRefreshReq) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use ListRefreshReq.ProtoReflect.Descriptor instead.
 func (*ListRefreshReq) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{21}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{30}
 }
 
 func (x *ListRefreshReq) GetUserId() string {
@@ -1239,7 +1711,7 @@ type ListRefreshResp struct {
 func (x *ListRefreshResp) Reset() {
 	*x = ListRefreshResp{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[22]
+		mi := &file_api_v2_api_proto_msgTypes[31]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1252,7 +1724,7 @@ func (x *ListRefreshResp) String() string {
 func (*ListRefreshResp) ProtoMessage() {}
 
 func (x *ListRefreshResp) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[22]
+	mi := &file_api_v2_api_proto_msgTypes[31]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1265,7 +1737,7 @@ func (x *ListRefreshResp) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use ListRefreshResp.ProtoReflect.Descriptor instead.
 func (*ListRefreshResp) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{22}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{31}
 }
 
 func (x *ListRefreshResp) GetRefreshTokens() []*RefreshTokenRef {
@@ -1289,7 +1761,7 @@ type RevokeRefreshReq struct {
 func (x *RevokeRefreshReq) Reset() {
 	*x = RevokeRefreshReq{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[23]
+		mi := &file_api_v2_api_proto_msgTypes[32]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1302,7 +1774,7 @@ func (x *RevokeRefreshReq) String() string {
 func (*RevokeRefreshReq) ProtoMessage() {}
 
 func (x *RevokeRefreshReq) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[23]
+	mi := &file_api_v2_api_proto_msgTypes[32]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1315,7 +1787,7 @@ func (x *RevokeRefreshReq) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RevokeRefreshReq.ProtoReflect.Descriptor instead.
 func (*RevokeRefreshReq) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{23}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{32}
 }
 
 func (x *RevokeRefreshReq) GetUserId() string {
@@ -1345,7 +1817,7 @@ type RevokeRefreshResp struct {
 func (x *RevokeRefreshResp) Reset() {
 	*x = RevokeRefreshResp{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[24]
+		mi := &file_api_v2_api_proto_msgTypes[33]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1358,7 +1830,7 @@ func (x *RevokeRefreshResp) String() string {
 func (*RevokeRefreshResp) ProtoMessage() {}
 
 func (x *RevokeRefreshResp) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[24]
+	mi := &file_api_v2_api_proto_msgTypes[33]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1371,7 +1843,7 @@ func (x *RevokeRefreshResp) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RevokeRefreshResp.ProtoReflect.Descriptor instead.
 func (*RevokeRefreshResp) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{24}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{33}
 }
 
 func (x *RevokeRefreshResp) GetNotFound() bool {
@@ -1393,7 +1865,7 @@ type VerifyPasswordReq struct {
 func (x *VerifyPasswordReq) Reset() {
 	*x = VerifyPasswordReq{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[25]
+		mi := &file_api_v2_api_proto_msgTypes[34]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1406,7 +1878,7 @@ func (x *VerifyPasswordReq) String() string {
 func (*VerifyPasswordReq) ProtoMessage() {}
 
 func (x *VerifyPasswordReq) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[25]
+	mi := &file_api_v2_api_proto_msgTypes[34]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1419,7 +1891,7 @@ func (x *VerifyPasswordReq) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use VerifyPasswordReq.ProtoReflect.Descriptor instead.
 func (*VerifyPasswordReq) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{25}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{34}
 }
 
 func (x *VerifyPasswordReq) GetEmail() string {
@@ -1448,7 +1920,7 @@ type VerifyPasswordResp struct {
 func (x *VerifyPasswordResp) Reset() {
 	*x = VerifyPasswordResp{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_v2_api_proto_msgTypes[26]
+		mi := &file_api_v2_api_proto_msgTypes[35]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1461,7 +1933,7 @@ func (x *VerifyPasswordResp) String() string {
 func (*VerifyPasswordResp) ProtoMessage() {}
 
 func (x *VerifyPasswordResp) ProtoReflect() protoreflect.Message {
-	mi := &file_api_v2_api_proto_msgTypes[26]
+	mi := &file_api_v2_api_proto_msgTypes[35]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1474,7 +1946,7 @@ func (x *VerifyPasswordResp) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use VerifyPasswordResp.ProtoReflect.Descriptor instead.
 func (*VerifyPasswordResp) Descriptor() ([]byte, []int) {
-	return file_api_v2_api_proto_rawDescGZIP(), []int{26}
+	return file_api_v2_api_proto_rawDescGZIP(), []int{35}
 }
 
 func (x *VerifyPasswordResp) GetVerified() bool {
@@ -1576,96 +2048,149 @@ var file_api_v2_api_proto_rawDesc = []byte{
 	0x52, 0x65, 0x73, 0x70, 0x12, 0x2b, 0x0a, 0x09, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
 	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61,
 	0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x09, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
-	0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x22,
-	0x37, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16,
-	0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
-	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x70, 0x69, 0x22, 0x7a, 0x0a, 0x0f, 0x52, 0x65, 0x66, 0x72,
-	0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66, 0x12, 0x0e, 0x0a, 0x02, 0x69,
-	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63,
-	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
-	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61,
-	0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f,
-	0x75, 0x73, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74,
-	0x55, 0x73, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72,
-	0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
-	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22,
-	0x4e, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65,
-	0x73, 0x70, 0x12, 0x3b, 0x0a, 0x0e, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f,
-	0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69,
-	0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66,
-	0x52, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22,
-	0x48, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68,
-	0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09,
+	0x73, 0x22, 0x5b, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x0e,
+	0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
+	0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79,
+	0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x42,
+	0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f,
+	0x72, 0x52, 0x65, 0x71, 0x12, 0x2c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f,
+	0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f,
+	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
+	0x6f, 0x72, 0x22, 0x3c, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e,
+	0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72,
+	0x65, 0x61, 0x64, 0x79, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x0d, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73,
+	0x22, 0x79, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
+	0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x74, 0x79,
+	0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x54, 0x79, 0x70,
+	0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a,
+	0x6e, 0x65, 0x77, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x09, 0x6e, 0x65, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x32, 0x0a, 0x13, 0x55,
+	0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65,
+	0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22,
+	0x24, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
+	0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x32, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43,
+	0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09,
+	0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x12, 0x0a, 0x10, 0x4c, 0x69, 0x73,
+	0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x22, 0x43, 0x0a,
+	0x11, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65,
+	0x73, 0x70, 0x12, 0x2e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73,
+	0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e,
+	0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f,
+	0x72, 0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71,
+	0x22, 0x37, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12,
+	0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x70, 0x69, 0x22, 0x7a, 0x0a, 0x0f, 0x52, 0x65, 0x66,
+	0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66, 0x12, 0x0e, 0x0a, 0x02,
+	0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09,
 	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x30, 0x0a, 0x11, 0x52, 0x65, 0x76,
-	0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b,
-	0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x45, 0x0a, 0x11, 0x56,
-	0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71,
-	0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
-	0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
-	0x72, 0x64, 0x22, 0x4d, 0x0a, 0x12, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73,
-	0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x69,
-	0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69,
-	0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e,
-	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e,
-	0x64, 0x32, 0xfd, 0x05, 0x0a, 0x03, 0x44, 0x65, 0x78, 0x12, 0x34, 0x0a, 0x09, 0x47, 0x65, 0x74,
-	0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74,
-	0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e,
-	0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12,
-	0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12,
-	0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65,
-	0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
+	0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65,
+	0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74,
+	0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x61, 0x73,
+	0x74, 0x55, 0x73, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66,
+	0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f,
+	0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
+	0x22, 0x4e, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52,
+	0x65, 0x73, 0x70, 0x12, 0x3b, 0x0a, 0x0e, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74,
+	0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70,
+	0x69, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65,
+	0x66, 0x52, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73,
+	0x22, 0x48, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73,
+	0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a,
+	0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x30, 0x0a, 0x11, 0x52, 0x65,
+	0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12,
+	0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x45, 0x0a, 0x11,
+	0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65,
+	0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
+	0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
+	0x6f, 0x72, 0x64, 0x22, 0x4d, 0x0a, 0x12, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73,
+	0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72,
+	0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72,
+	0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75,
+	0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75,
+	0x6e, 0x64, 0x32, 0x98, 0x08, 0x0a, 0x03, 0x44, 0x65, 0x78, 0x12, 0x34, 0x0a, 0x09, 0x47, 0x65,
+	0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65,
+	0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69,
+	0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00,
+	0x12, 0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
+	0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69,
+	0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
+	0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12,
+	0x3d, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12,
+	0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65,
+	0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61,
 	0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d,
-	0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14,
-	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74,
-	0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a,
-	0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e,
-	0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
-	0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
-	0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e,
-	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16,
-	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77,
-	0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22,
-	0x00, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77,
-	0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
-	0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70,
-	0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
-	0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
-	0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44,
-	0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71,
-	0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73,
-	0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d, 0x4c,
-	0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x2e, 0x61,
-	0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52,
-	0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73,
-	0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x47,
-	0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e,
-	0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
-	0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3a,
-	0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x13, 0x2e,
-	0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52,
-	0x65, 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66,
-	0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x52, 0x65,
-	0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x15, 0x2e, 0x61, 0x70,
-	0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52,
-	0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52,
-	0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e,
-	0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16,
-	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77,
-	0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72,
-	0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22,
-	0x00, 0x42, 0x36, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, 0x2e,
-	0x64, 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
-	0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, 0x61,
-	0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x33,
+	0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14,
+	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e,
+	0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
+	0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a,
+	0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12,
+	0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73,
+	0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
+	0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70,
+	0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73,
+	0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74,
+	0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61,
+	0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72,
+	0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74,
+	0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e,
+	0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65,
+	0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61,
+	0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d,
+	0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x2e,
+	0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
+	0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61,
+	0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f,
+	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12,
+	0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e,
+	0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65,
+	0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f,
+	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70,
+	0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71,
+	0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e,
+	0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f,
+	0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12,
+	0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e,
+	0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44,
+	0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65,
+	0x73, 0x70, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e,
+	0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
+	0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e,
+	0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f,
+	0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x56, 0x65,
+	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73,
+	0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72,
+	0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0b, 0x4c, 0x69,
+	0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e,
+	0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x14,
+	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68,
+	0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65,
+	0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65,
+	0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x16,
+	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65,
+	0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69,
+	0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69,
+	0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52,
+	0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50,
+	0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x42, 0x36, 0x0a,
+	0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, 0x2e, 0x64, 0x65, 0x78, 0x2e,
+	0x61, 0x70, 0x69, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+	0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76,
+	0x32, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1680,35 +2205,44 @@ func file_api_v2_api_proto_rawDescGZIP() []byte {
 	return file_api_v2_api_proto_rawDescData
 }
 
-var file_api_v2_api_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
+var file_api_v2_api_proto_msgTypes = make([]protoimpl.MessageInfo, 36)
 var file_api_v2_api_proto_goTypes = []interface{}{
-	(*Client)(nil),             // 0: api.Client
-	(*GetClientReq)(nil),       // 1: api.GetClientReq
-	(*GetClientResp)(nil),      // 2: api.GetClientResp
-	(*CreateClientReq)(nil),    // 3: api.CreateClientReq
-	(*CreateClientResp)(nil),   // 4: api.CreateClientResp
-	(*DeleteClientReq)(nil),    // 5: api.DeleteClientReq
-	(*DeleteClientResp)(nil),   // 6: api.DeleteClientResp
-	(*UpdateClientReq)(nil),    // 7: api.UpdateClientReq
-	(*UpdateClientResp)(nil),   // 8: api.UpdateClientResp
-	(*Password)(nil),           // 9: api.Password
-	(*CreatePasswordReq)(nil),  // 10: api.CreatePasswordReq
-	(*CreatePasswordResp)(nil), // 11: api.CreatePasswordResp
-	(*UpdatePasswordReq)(nil),  // 12: api.UpdatePasswordReq
-	(*UpdatePasswordResp)(nil), // 13: api.UpdatePasswordResp
-	(*DeletePasswordReq)(nil),  // 14: api.DeletePasswordReq
-	(*DeletePasswordResp)(nil), // 15: api.DeletePasswordResp
-	(*ListPasswordReq)(nil),    // 16: api.ListPasswordReq
-	(*ListPasswordResp)(nil),   // 17: api.ListPasswordResp
-	(*VersionReq)(nil),         // 18: api.VersionReq
-	(*VersionResp)(nil),        // 19: api.VersionResp
-	(*RefreshTokenRef)(nil),    // 20: api.RefreshTokenRef
-	(*ListRefreshReq)(nil),     // 21: api.ListRefreshReq
-	(*ListRefreshResp)(nil),    // 22: api.ListRefreshResp
-	(*RevokeRefreshReq)(nil),   // 23: api.RevokeRefreshReq
-	(*RevokeRefreshResp)(nil),  // 24: api.RevokeRefreshResp
-	(*VerifyPasswordReq)(nil),  // 25: api.VerifyPasswordReq
-	(*VerifyPasswordResp)(nil), // 26: api.VerifyPasswordResp
+	(*Client)(nil),              // 0: api.Client
+	(*GetClientReq)(nil),        // 1: api.GetClientReq
+	(*GetClientResp)(nil),       // 2: api.GetClientResp
+	(*CreateClientReq)(nil),     // 3: api.CreateClientReq
+	(*CreateClientResp)(nil),    // 4: api.CreateClientResp
+	(*DeleteClientReq)(nil),     // 5: api.DeleteClientReq
+	(*DeleteClientResp)(nil),    // 6: api.DeleteClientResp
+	(*UpdateClientReq)(nil),     // 7: api.UpdateClientReq
+	(*UpdateClientResp)(nil),    // 8: api.UpdateClientResp
+	(*Password)(nil),            // 9: api.Password
+	(*CreatePasswordReq)(nil),   // 10: api.CreatePasswordReq
+	(*CreatePasswordResp)(nil),  // 11: api.CreatePasswordResp
+	(*UpdatePasswordReq)(nil),   // 12: api.UpdatePasswordReq
+	(*UpdatePasswordResp)(nil),  // 13: api.UpdatePasswordResp
+	(*DeletePasswordReq)(nil),   // 14: api.DeletePasswordReq
+	(*DeletePasswordResp)(nil),  // 15: api.DeletePasswordResp
+	(*ListPasswordReq)(nil),     // 16: api.ListPasswordReq
+	(*ListPasswordResp)(nil),    // 17: api.ListPasswordResp
+	(*Connector)(nil),           // 18: api.Connector
+	(*CreateConnectorReq)(nil),  // 19: api.CreateConnectorReq
+	(*CreateConnectorResp)(nil), // 20: api.CreateConnectorResp
+	(*UpdateConnectorReq)(nil),  // 21: api.UpdateConnectorReq
+	(*UpdateConnectorResp)(nil), // 22: api.UpdateConnectorResp
+	(*DeleteConnectorReq)(nil),  // 23: api.DeleteConnectorReq
+	(*DeleteConnectorResp)(nil), // 24: api.DeleteConnectorResp
+	(*ListConnectorReq)(nil),    // 25: api.ListConnectorReq
+	(*ListConnectorResp)(nil),   // 26: api.ListConnectorResp
+	(*VersionReq)(nil),          // 27: api.VersionReq
+	(*VersionResp)(nil),         // 28: api.VersionResp
+	(*RefreshTokenRef)(nil),     // 29: api.RefreshTokenRef
+	(*ListRefreshReq)(nil),      // 30: api.ListRefreshReq
+	(*ListRefreshResp)(nil),     // 31: api.ListRefreshResp
+	(*RevokeRefreshReq)(nil),    // 32: api.RevokeRefreshReq
+	(*RevokeRefreshResp)(nil),   // 33: api.RevokeRefreshResp
+	(*VerifyPasswordReq)(nil),   // 34: api.VerifyPasswordReq
+	(*VerifyPasswordResp)(nil),  // 35: api.VerifyPasswordResp
 }
 var file_api_v2_api_proto_depIdxs = []int32{
 	0,  // 0: api.GetClientResp.client:type_name -> api.Client
@@ -1716,36 +2250,46 @@ var file_api_v2_api_proto_depIdxs = []int32{
 	0,  // 2: api.CreateClientResp.client:type_name -> api.Client
 	9,  // 3: api.CreatePasswordReq.password:type_name -> api.Password
 	9,  // 4: api.ListPasswordResp.passwords:type_name -> api.Password
-	20, // 5: api.ListRefreshResp.refresh_tokens:type_name -> api.RefreshTokenRef
-	1,  // 6: api.Dex.GetClient:input_type -> api.GetClientReq
-	3,  // 7: api.Dex.CreateClient:input_type -> api.CreateClientReq
-	7,  // 8: api.Dex.UpdateClient:input_type -> api.UpdateClientReq
-	5,  // 9: api.Dex.DeleteClient:input_type -> api.DeleteClientReq
-	10, // 10: api.Dex.CreatePassword:input_type -> api.CreatePasswordReq
-	12, // 11: api.Dex.UpdatePassword:input_type -> api.UpdatePasswordReq
-	14, // 12: api.Dex.DeletePassword:input_type -> api.DeletePasswordReq
-	16, // 13: api.Dex.ListPasswords:input_type -> api.ListPasswordReq
-	18, // 14: api.Dex.GetVersion:input_type -> api.VersionReq
-	21, // 15: api.Dex.ListRefresh:input_type -> api.ListRefreshReq
-	23, // 16: api.Dex.RevokeRefresh:input_type -> api.RevokeRefreshReq
-	25, // 17: api.Dex.VerifyPassword:input_type -> api.VerifyPasswordReq
-	2,  // 18: api.Dex.GetClient:output_type -> api.GetClientResp
-	4,  // 19: api.Dex.CreateClient:output_type -> api.CreateClientResp
-	8,  // 20: api.Dex.UpdateClient:output_type -> api.UpdateClientResp
-	6,  // 21: api.Dex.DeleteClient:output_type -> api.DeleteClientResp
-	11, // 22: api.Dex.CreatePassword:output_type -> api.CreatePasswordResp
-	13, // 23: api.Dex.UpdatePassword:output_type -> api.UpdatePasswordResp
-	15, // 24: api.Dex.DeletePassword:output_type -> api.DeletePasswordResp
-	17, // 25: api.Dex.ListPasswords:output_type -> api.ListPasswordResp
-	19, // 26: api.Dex.GetVersion:output_type -> api.VersionResp
-	22, // 27: api.Dex.ListRefresh:output_type -> api.ListRefreshResp
-	24, // 28: api.Dex.RevokeRefresh:output_type -> api.RevokeRefreshResp
-	26, // 29: api.Dex.VerifyPassword:output_type -> api.VerifyPasswordResp
-	18, // [18:30] is the sub-list for method output_type
-	6,  // [6:18] is the sub-list for method input_type
-	6,  // [6:6] is the sub-list for extension type_name
-	6,  // [6:6] is the sub-list for extension extendee
-	0,  // [0:6] is the sub-list for field type_name
+	18, // 5: api.CreateConnectorReq.connector:type_name -> api.Connector
+	18, // 6: api.ListConnectorResp.connectors:type_name -> api.Connector
+	29, // 7: api.ListRefreshResp.refresh_tokens:type_name -> api.RefreshTokenRef
+	1,  // 8: api.Dex.GetClient:input_type -> api.GetClientReq
+	3,  // 9: api.Dex.CreateClient:input_type -> api.CreateClientReq
+	7,  // 10: api.Dex.UpdateClient:input_type -> api.UpdateClientReq
+	5,  // 11: api.Dex.DeleteClient:input_type -> api.DeleteClientReq
+	10, // 12: api.Dex.CreatePassword:input_type -> api.CreatePasswordReq
+	12, // 13: api.Dex.UpdatePassword:input_type -> api.UpdatePasswordReq
+	14, // 14: api.Dex.DeletePassword:input_type -> api.DeletePasswordReq
+	16, // 15: api.Dex.ListPasswords:input_type -> api.ListPasswordReq
+	19, // 16: api.Dex.CreateConnector:input_type -> api.CreateConnectorReq
+	21, // 17: api.Dex.UpdateConnector:input_type -> api.UpdateConnectorReq
+	23, // 18: api.Dex.DeleteConnector:input_type -> api.DeleteConnectorReq
+	25, // 19: api.Dex.ListConnectors:input_type -> api.ListConnectorReq
+	27, // 20: api.Dex.GetVersion:input_type -> api.VersionReq
+	30, // 21: api.Dex.ListRefresh:input_type -> api.ListRefreshReq
+	32, // 22: api.Dex.RevokeRefresh:input_type -> api.RevokeRefreshReq
+	34, // 23: api.Dex.VerifyPassword:input_type -> api.VerifyPasswordReq
+	2,  // 24: api.Dex.GetClient:output_type -> api.GetClientResp
+	4,  // 25: api.Dex.CreateClient:output_type -> api.CreateClientResp
+	8,  // 26: api.Dex.UpdateClient:output_type -> api.UpdateClientResp
+	6,  // 27: api.Dex.DeleteClient:output_type -> api.DeleteClientResp
+	11, // 28: api.Dex.CreatePassword:output_type -> api.CreatePasswordResp
+	13, // 29: api.Dex.UpdatePassword:output_type -> api.UpdatePasswordResp
+	15, // 30: api.Dex.DeletePassword:output_type -> api.DeletePasswordResp
+	17, // 31: api.Dex.ListPasswords:output_type -> api.ListPasswordResp
+	20, // 32: api.Dex.CreateConnector:output_type -> api.CreateConnectorResp
+	22, // 33: api.Dex.UpdateConnector:output_type -> api.UpdateConnectorResp
+	24, // 34: api.Dex.DeleteConnector:output_type -> api.DeleteConnectorResp
+	26, // 35: api.Dex.ListConnectors:output_type -> api.ListConnectorResp
+	28, // 36: api.Dex.GetVersion:output_type -> api.VersionResp
+	31, // 37: api.Dex.ListRefresh:output_type -> api.ListRefreshResp
+	33, // 38: api.Dex.RevokeRefresh:output_type -> api.RevokeRefreshResp
+	35, // 39: api.Dex.VerifyPassword:output_type -> api.VerifyPasswordResp
+	24, // [24:40] is the sub-list for method output_type
+	8,  // [8:24] is the sub-list for method input_type
+	8,  // [8:8] is the sub-list for extension type_name
+	8,  // [8:8] is the sub-list for extension extendee
+	0,  // [0:8] is the sub-list for field type_name
 }
 
 func init() { file_api_v2_api_proto_init() }
@@ -1971,7 +2515,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*VersionReq); i {
+			switch v := v.(*Connector); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1983,7 +2527,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*VersionResp); i {
+			switch v := v.(*CreateConnectorReq); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1995,7 +2539,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RefreshTokenRef); i {
+			switch v := v.(*CreateConnectorResp); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2007,7 +2551,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ListRefreshReq); i {
+			switch v := v.(*UpdateConnectorReq); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2019,7 +2563,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ListRefreshResp); i {
+			switch v := v.(*UpdateConnectorResp); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2031,7 +2575,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RevokeRefreshReq); i {
+			switch v := v.(*DeleteConnectorReq); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2043,7 +2587,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RevokeRefreshResp); i {
+			switch v := v.(*DeleteConnectorResp); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2055,7 +2599,7 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*VerifyPasswordReq); i {
+			switch v := v.(*ListConnectorReq); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2067,6 +2611,114 @@ func file_api_v2_api_proto_init() {
 			}
 		}
 		file_api_v2_api_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListConnectorResp); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*VersionReq); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*VersionResp); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RefreshTokenRef); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListRefreshReq); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListRefreshResp); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RevokeRefreshReq); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RevokeRefreshResp); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*VerifyPasswordReq); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_v2_api_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*VerifyPasswordResp); i {
 			case 0:
 				return &v.state
@@ -2085,7 +2737,7 @@ func file_api_v2_api_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_api_v2_api_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   27,
+			NumMessages:   36,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/api/v2/api.proto b/api/v2/api.proto
index c429fd80..7f2c80b9 100644
--- a/api/v2/api.proto
+++ b/api/v2/api.proto
@@ -116,6 +116,56 @@ message ListPasswordResp {
   repeated Password passwords = 1;
 }
 
+// Connector is a strategy used by Dex for authenticating a user against another identity provider
+message Connector {
+  string id = 1;
+  string type = 2;
+  string name = 3;
+  bytes config = 4;
+}
+
+// CreateConnectorReq is a request to make a connector.
+message CreateConnectorReq {
+  Connector connector = 1;
+}
+
+// CreateConnectorResp returns the response from creating a connector.
+message CreateConnectorResp {
+  bool already_exists = 1;
+}
+
+// UpdateConnectorReq is a request to modify an existing connector.
+message UpdateConnectorReq {
+  // The id used to lookup the connector. This field cannot be modified
+  string id = 1;
+  string new_type = 2;
+  string new_name = 3;
+  bytes new_config = 4;
+}
+
+// UpdateConnectorResp returns the response from modifying an existing connector.
+message UpdateConnectorResp {
+  bool not_found = 1;
+}
+
+// DeleteConnectorReq is a request to delete a connector.
+message DeleteConnectorReq {
+  string id = 1;
+}
+
+// DeleteConnectorResp returns the response from deleting a connector.
+message DeleteConnectorResp {
+  bool not_found = 1;
+}
+
+// ListConnectorReq is a request to enumerate connectors.
+message ListConnectorReq {}
+
+// ListConnectorResp returns a list of connectors.
+message ListConnectorResp {
+  repeated Connector connectors = 1;
+}
+
 // VersionReq is a request to fetch version info.
 message VersionReq {}
 
@@ -189,6 +239,14 @@ service Dex {
   rpc DeletePassword(DeletePasswordReq) returns (DeletePasswordResp) {};
   // ListPassword lists all password entries.
   rpc ListPasswords(ListPasswordReq) returns (ListPasswordResp) {};
+  // CreateConnector creates a connector.
+  rpc CreateConnector(CreateConnectorReq) returns (CreateConnectorResp) {};
+  // UpdateConnector modifies existing connector.
+  rpc UpdateConnector(UpdateConnectorReq) returns (UpdateConnectorResp) {};
+  // DeleteConnector deletes the connector.
+  rpc DeleteConnector(DeleteConnectorReq) returns (DeleteConnectorResp) {};
+  // ListConnectors lists all connector entries.
+  rpc ListConnectors(ListConnectorReq) returns (ListConnectorResp) {};
   // GetVersion returns version information of the server.
   rpc GetVersion(VersionReq) returns (VersionResp) {};
   // ListRefresh lists all the refresh token entries for a particular user.
diff --git a/api/v2/api_grpc.pb.go b/api/v2/api_grpc.pb.go
index fcbb9854..7ffab5ab 100644
--- a/api/v2/api_grpc.pb.go
+++ b/api/v2/api_grpc.pb.go
@@ -19,18 +19,22 @@ import (
 const _ = grpc.SupportPackageIsVersion7
 
 const (
-	Dex_GetClient_FullMethodName      = "/api.Dex/GetClient"
-	Dex_CreateClient_FullMethodName   = "/api.Dex/CreateClient"
-	Dex_UpdateClient_FullMethodName   = "/api.Dex/UpdateClient"
-	Dex_DeleteClient_FullMethodName   = "/api.Dex/DeleteClient"
-	Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword"
-	Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword"
-	Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword"
-	Dex_ListPasswords_FullMethodName  = "/api.Dex/ListPasswords"
-	Dex_GetVersion_FullMethodName     = "/api.Dex/GetVersion"
-	Dex_ListRefresh_FullMethodName    = "/api.Dex/ListRefresh"
-	Dex_RevokeRefresh_FullMethodName  = "/api.Dex/RevokeRefresh"
-	Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword"
+	Dex_GetClient_FullMethodName       = "/api.Dex/GetClient"
+	Dex_CreateClient_FullMethodName    = "/api.Dex/CreateClient"
+	Dex_UpdateClient_FullMethodName    = "/api.Dex/UpdateClient"
+	Dex_DeleteClient_FullMethodName    = "/api.Dex/DeleteClient"
+	Dex_CreatePassword_FullMethodName  = "/api.Dex/CreatePassword"
+	Dex_UpdatePassword_FullMethodName  = "/api.Dex/UpdatePassword"
+	Dex_DeletePassword_FullMethodName  = "/api.Dex/DeletePassword"
+	Dex_ListPasswords_FullMethodName   = "/api.Dex/ListPasswords"
+	Dex_CreateConnector_FullMethodName = "/api.Dex/CreateConnector"
+	Dex_UpdateConnector_FullMethodName = "/api.Dex/UpdateConnector"
+	Dex_DeleteConnector_FullMethodName = "/api.Dex/DeleteConnector"
+	Dex_ListConnectors_FullMethodName  = "/api.Dex/ListConnectors"
+	Dex_GetVersion_FullMethodName      = "/api.Dex/GetVersion"
+	Dex_ListRefresh_FullMethodName     = "/api.Dex/ListRefresh"
+	Dex_RevokeRefresh_FullMethodName   = "/api.Dex/RevokeRefresh"
+	Dex_VerifyPassword_FullMethodName  = "/api.Dex/VerifyPassword"
 )
 
 // DexClient is the client API for Dex service.
@@ -53,6 +57,14 @@ type DexClient interface {
 	DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error)
 	// ListPassword lists all password entries.
 	ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error)
+	// CreateConnector creates a connector.
+	CreateConnector(ctx context.Context, in *CreateConnectorReq, opts ...grpc.CallOption) (*CreateConnectorResp, error)
+	// UpdateConnector modifies existing connector.
+	UpdateConnector(ctx context.Context, in *UpdateConnectorReq, opts ...grpc.CallOption) (*UpdateConnectorResp, error)
+	// DeleteConnector deletes the connector.
+	DeleteConnector(ctx context.Context, in *DeleteConnectorReq, opts ...grpc.CallOption) (*DeleteConnectorResp, error)
+	// ListConnectors lists all connector entries.
+	ListConnectors(ctx context.Context, in *ListConnectorReq, opts ...grpc.CallOption) (*ListConnectorResp, error)
 	// GetVersion returns version information of the server.
 	GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error)
 	// ListRefresh lists all the refresh token entries for a particular user.
@@ -145,6 +157,42 @@ func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts
 	return out, nil
 }
 
+func (c *dexClient) CreateConnector(ctx context.Context, in *CreateConnectorReq, opts ...grpc.CallOption) (*CreateConnectorResp, error) {
+	out := new(CreateConnectorResp)
+	err := c.cc.Invoke(ctx, Dex_CreateConnector_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *dexClient) UpdateConnector(ctx context.Context, in *UpdateConnectorReq, opts ...grpc.CallOption) (*UpdateConnectorResp, error) {
+	out := new(UpdateConnectorResp)
+	err := c.cc.Invoke(ctx, Dex_UpdateConnector_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *dexClient) DeleteConnector(ctx context.Context, in *DeleteConnectorReq, opts ...grpc.CallOption) (*DeleteConnectorResp, error) {
+	out := new(DeleteConnectorResp)
+	err := c.cc.Invoke(ctx, Dex_DeleteConnector_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *dexClient) ListConnectors(ctx context.Context, in *ListConnectorReq, opts ...grpc.CallOption) (*ListConnectorResp, error) {
+	out := new(ListConnectorResp)
+	err := c.cc.Invoke(ctx, Dex_ListConnectors_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) {
 	out := new(VersionResp)
 	err := c.cc.Invoke(ctx, Dex_GetVersion_FullMethodName, in, out, opts...)
@@ -201,6 +249,14 @@ type DexServer interface {
 	DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error)
 	// ListPassword lists all password entries.
 	ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error)
+	// CreateConnector creates a connector.
+	CreateConnector(context.Context, *CreateConnectorReq) (*CreateConnectorResp, error)
+	// UpdateConnector modifies existing connector.
+	UpdateConnector(context.Context, *UpdateConnectorReq) (*UpdateConnectorResp, error)
+	// DeleteConnector deletes the connector.
+	DeleteConnector(context.Context, *DeleteConnectorReq) (*DeleteConnectorResp, error)
+	// ListConnectors lists all connector entries.
+	ListConnectors(context.Context, *ListConnectorReq) (*ListConnectorResp, error)
 	// GetVersion returns version information of the server.
 	GetVersion(context.Context, *VersionReq) (*VersionResp, error)
 	// ListRefresh lists all the refresh token entries for a particular user.
@@ -242,6 +298,18 @@ func (UnimplementedDexServer) DeletePassword(context.Context, *DeletePasswordReq
 func (UnimplementedDexServer) ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method ListPasswords not implemented")
 }
+func (UnimplementedDexServer) CreateConnector(context.Context, *CreateConnectorReq) (*CreateConnectorResp, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method CreateConnector not implemented")
+}
+func (UnimplementedDexServer) UpdateConnector(context.Context, *UpdateConnectorReq) (*UpdateConnectorResp, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method UpdateConnector not implemented")
+}
+func (UnimplementedDexServer) DeleteConnector(context.Context, *DeleteConnectorReq) (*DeleteConnectorResp, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method DeleteConnector not implemented")
+}
+func (UnimplementedDexServer) ListConnectors(context.Context, *ListConnectorReq) (*ListConnectorResp, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ListConnectors not implemented")
+}
 func (UnimplementedDexServer) GetVersion(context.Context, *VersionReq) (*VersionResp, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
 }
@@ -411,6 +479,78 @@ func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(i
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Dex_CreateConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(CreateConnectorReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(DexServer).CreateConnector(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Dex_CreateConnector_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(DexServer).CreateConnector(ctx, req.(*CreateConnectorReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Dex_UpdateConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(UpdateConnectorReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(DexServer).UpdateConnector(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Dex_UpdateConnector_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(DexServer).UpdateConnector(ctx, req.(*UpdateConnectorReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Dex_DeleteConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(DeleteConnectorReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(DexServer).DeleteConnector(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Dex_DeleteConnector_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(DexServer).DeleteConnector(ctx, req.(*DeleteConnectorReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Dex_ListConnectors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListConnectorReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(DexServer).ListConnectors(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: Dex_ListConnectors_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(DexServer).ListConnectors(ctx, req.(*ListConnectorReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(VersionReq)
 	if err := dec(in); err != nil {
@@ -522,6 +662,22 @@ var Dex_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "ListPasswords",
 			Handler:    _Dex_ListPasswords_Handler,
 		},
+		{
+			MethodName: "CreateConnector",
+			Handler:    _Dex_CreateConnector_Handler,
+		},
+		{
+			MethodName: "UpdateConnector",
+			Handler:    _Dex_UpdateConnector_Handler,
+		},
+		{
+			MethodName: "DeleteConnector",
+			Handler:    _Dex_DeleteConnector_Handler,
+		},
+		{
+			MethodName: "ListConnectors",
+			Handler:    _Dex_ListConnectors_Handler,
+		},
 		{
 			MethodName: "GetVersion",
 			Handler:    _Dex_GetVersion_Handler,
diff --git a/cmd/dex/config.go b/cmd/dex/config.go
index b6d34f37..474f28a6 100644
--- a/cmd/dex/config.go
+++ b/cmd/dex/config.go
@@ -7,6 +7,7 @@ import (
 	"log/slog"
 	"net/http"
 	"os"
+	"slices"
 	"strings"
 
 	"golang.org/x/crypto/bcrypt"
@@ -50,10 +51,22 @@ type Config struct {
 	// querying the storage. Cannot be specified without enabling a passwords
 	// database.
 	StaticPasswords []password `json:"staticPasswords"`
+
+	// AdditionalFeature allow the extension of Dex functionalities
+	AdditionalFeatures []server.AdditionalFeature `json:"additionalFeatures"`
+}
+
+// Parse the configuration
+func (c *Config) Parse() {
+	if c.AdditionalFeatures == nil {
+		c.AdditionalFeatures = []server.AdditionalFeature{}
+	}
 }
 
 // Validate the configuration
 func (c Config) Validate() error {
+	invalidFeatures := c.findInvalidAdditionalFeatures()
+
 	// Fast checks. Perform these first for a more responsive CLI.
 	checks := []struct {
 		bad    bool
@@ -72,6 +85,7 @@ func (c Config) Validate() error {
 		{c.GRPC.TLSKey != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
 		{(c.GRPC.TLSCert == "") != (c.GRPC.TLSKey == ""), "must specific both a gRPC TLS cert and key"},
 		{c.GRPC.TLSCert == "" && c.GRPC.TLSClientCA != "", "cannot specify gRPC TLS client CA without a gRPC TLS cert"},
+		{len(invalidFeatures) > 0, fmt.Sprintf("invalid additionalFeatures supplied: %v. Valid entries: %s", invalidFeatures, server.ValidAdditionalFeatures)},
 		{c.GRPC.TLSMinVersion != "" && c.GRPC.TLSMinVersion != "1.2" && c.GRPC.TLSMinVersion != "1.3", "supported TLS versions are: 1.2, 1.3"},
 		{c.GRPC.TLSMaxVersion != "" && c.GRPC.TLSMaxVersion != "1.2" && c.GRPC.TLSMaxVersion != "1.3", "supported TLS versions are: 1.2, 1.3"},
 		{c.GRPC.TLSMaxVersion != "" && c.GRPC.TLSMinVersion != "" && c.GRPC.TLSMinVersion > c.GRPC.TLSMaxVersion, "TLSMinVersion greater than TLSMaxVersion"},
@@ -90,6 +104,22 @@ func (c Config) Validate() error {
 	return nil
 }
 
+// findInvalidAdditionalFeatures returns additional features that are not considered valid
+func (c Config) findInvalidAdditionalFeatures() []server.AdditionalFeature {
+	if c.AdditionalFeatures == nil {
+		return []server.AdditionalFeature{}
+	}
+
+	badFeatures := []server.AdditionalFeature{}
+	for _, feature := range c.AdditionalFeatures {
+		if !slices.Contains(server.ValidAdditionalFeatures, feature) {
+			badFeatures = append(badFeatures, feature)
+		}
+	}
+
+	return badFeatures
+}
+
 type password storage.Password
 
 func (p *password) UnmarshalJSON(b []byte) error {
diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go
index 19bf2637..863f039b 100644
--- a/cmd/dex/serve.go
+++ b/cmd/dex/serve.go
@@ -99,6 +99,7 @@ func runServe(options serveOptions) error {
 		return fmt.Errorf("error parse config file %s: %v", configFile, err)
 	}
 
+	c.Parse()
 	applyConfigOverrides(options, &c)
 
 	logger, err := newLogger(c.Logger.Level, c.Logger.Format)
@@ -501,7 +502,7 @@ func runServe(options serveOptions) error {
 		}
 
 		grpcSrv := grpc.NewServer(grpcOptions...)
-		api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version))
+		api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version, c.AdditionalFeatures))
 
 		grpcMetrics.InitializeMetrics(grpcSrv)
 		if c.GRPC.Reflection {
diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml
index 8f1018ff..8e569949 100644
--- a/examples/config-dev.yaml
+++ b/examples/config-dev.yaml
@@ -162,3 +162,8 @@ staticPasswords:
   hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
   username: "admin"
   userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
+
+# A list of features that extend Dex functionalities
+# additionalFeatures: 
+#   # allows CRUD operations on connectors through the gRPC API
+#   - "ConnectorsCRUD"
\ No newline at end of file
diff --git a/server/api.go b/server/api.go
index 6a0071ef..090b412c 100644
--- a/server/api.go
+++ b/server/api.go
@@ -2,9 +2,11 @@ package server
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"log/slog"
+	"slices"
 
 	"golang.org/x/crypto/bcrypt"
 
@@ -29,11 +31,12 @@ const (
 )
 
 // NewAPI returns a server which implements the gRPC API interface.
-func NewAPI(s storage.Storage, logger *slog.Logger, version string) api.DexServer {
+func NewAPI(s storage.Storage, logger *slog.Logger, version string, additionalFeatures []AdditionalFeature) api.DexServer {
 	return dexAPI{
-		s:       s,
-		logger:  logger.With("component", "api"),
-		version: version,
+		s:                  s,
+		logger:             logger.With("component", "api"),
+		version:            version,
+		additionalFeatures: additionalFeatures,
 	}
 }
 
@@ -43,6 +46,8 @@ type dexAPI struct {
 	s       storage.Storage
 	logger  *slog.Logger
 	version string
+
+	additionalFeatures []AdditionalFeature
 }
 
 func (d dexAPI) GetClient(ctx context.Context, req *api.GetClientReq) (*api.GetClientResp, error) {
@@ -385,3 +390,136 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 
 	return &api.RevokeRefreshResp{}, nil
 }
+
+func (d dexAPI) CreateConnector(ctx context.Context, req *api.CreateConnectorReq) (*api.CreateConnectorResp, error) {
+	if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
+		return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
+	}
+
+	if req.Connector.Id == "" {
+		return nil, errors.New("no id supplied")
+	}
+
+	if req.Connector.Type == "" {
+		return nil, errors.New("no type supplied")
+	}
+
+	if req.Connector.Name == "" {
+		return nil, errors.New("no name supplied")
+	}
+
+	if len(req.Connector.Config) == 0 {
+		return nil, errors.New("no config supplied")
+	}
+
+	if !json.Valid(req.Connector.Config) {
+		return nil, errors.New("invalid config supplied")
+	}
+
+	c := storage.Connector{
+		ID:     req.Connector.Id,
+		Name:   req.Connector.Name,
+		Type:   req.Connector.Type,
+		Config: req.Connector.Config,
+	}
+	if err := d.s.CreateConnector(ctx, c); err != nil {
+		if err == storage.ErrAlreadyExists {
+			return &api.CreateConnectorResp{AlreadyExists: true}, nil
+		}
+		d.logger.Error("api: failed to create connector", "err", err)
+		return nil, fmt.Errorf("create connector: %v", err)
+	}
+
+	return &api.CreateConnectorResp{}, nil
+}
+
+func (d dexAPI) UpdateConnector(ctx context.Context, req *api.UpdateConnectorReq) (*api.UpdateConnectorResp, error) {
+	if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
+		return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
+	}
+
+	if req.Id == "" {
+		return nil, errors.New("no email supplied")
+	}
+
+	if len(req.NewConfig) == 0 && req.NewName == "" && req.NewType == "" {
+		return nil, errors.New("nothing to update")
+	}
+
+	if !json.Valid(req.NewConfig) {
+		return nil, errors.New("invalid config supplied")
+	}
+
+	updater := func(old storage.Connector) (storage.Connector, error) {
+		if req.NewType != "" {
+			old.Type = req.NewType
+		}
+
+		if req.NewName != "" {
+			old.Name = req.NewName
+		}
+
+		if len(req.NewConfig) != 0 {
+			old.Config = req.NewConfig
+		}
+
+		return old, nil
+	}
+
+	if err := d.s.UpdateConnector(req.Id, updater); err != nil {
+		if err == storage.ErrNotFound {
+			return &api.UpdateConnectorResp{NotFound: true}, nil
+		}
+		d.logger.Error("api: failed to update connector", "err", err)
+		return nil, fmt.Errorf("update connector: %v", err)
+	}
+
+	return &api.UpdateConnectorResp{}, nil
+}
+
+func (d dexAPI) DeleteConnector(ctx context.Context, req *api.DeleteConnectorReq) (*api.DeleteConnectorResp, error) {
+	if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
+		return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
+	}
+
+	if req.Id == "" {
+		return nil, errors.New("no id supplied")
+	}
+
+	err := d.s.DeleteConnector(req.Id)
+	if err != nil {
+		if err == storage.ErrNotFound {
+			return &api.DeleteConnectorResp{NotFound: true}, nil
+		}
+		d.logger.Error("api: failed to delete connector", "err", err)
+		return nil, fmt.Errorf("delete connector: %v", err)
+	}
+	return &api.DeleteConnectorResp{}, nil
+}
+
+func (d dexAPI) ListConnectors(ctx context.Context, req *api.ListConnectorReq) (*api.ListConnectorResp, error) {
+	if !slices.Contains(d.additionalFeatures, ConnectorsCRUD) {
+		return nil, fmt.Errorf("%v not provided in addtionalFeatures", ConnectorsCRUD)
+	}
+
+	connectorList, err := d.s.ListConnectors()
+	if err != nil {
+		d.logger.Error("api: failed to list connectors", "err", err)
+		return nil, fmt.Errorf("list connectors: %v", err)
+	}
+
+	connectors := make([]*api.Connector, 0, len(connectorList))
+	for _, connector := range connectorList {
+		c := api.Connector{
+			Id:     connector.ID,
+			Name:   connector.Name,
+			Type:   connector.Type,
+			Config: connector.Config,
+		}
+		connectors = append(connectors, &c)
+	}
+
+	return &api.ListConnectorResp{
+		Connectors: connectors,
+	}, nil
+}
diff --git a/server/server.go b/server/server.go
index 5c1a97b8..68294885 100644
--- a/server/server.go
+++ b/server/server.go
@@ -47,6 +47,16 @@ import (
 	"github.com/dexidp/dex/web"
 )
 
+// AdditionalFeature allows the extension of Dex server functionalities
+type AdditionalFeature string
+
+// ConnectorsCRUD is an additional feature that allows CRUD operations on connectors
+var ConnectorsCRUD AdditionalFeature = "ConnectorsCRUD"
+
+var ValidAdditionalFeatures []AdditionalFeature = []AdditionalFeature{
+	ConnectorsCRUD,
+}
+
 // LocalConnector is the local passwordDB connector which is an internal
 // connector maintained by the server.
 const LocalConnector = "local"


Test patch:
diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go
index 95a79eb2..8924931d 100644
--- a/cmd/dex/config_test.go
+++ b/cmd/dex/config_test.go
@@ -37,7 +37,11 @@ func TestValidConfiguration(t *testing.T) {
 				Config: &mock.CallbackConfig{},
 			},
 		},
+		AdditionalFeatures: server.ValidAdditionalFeatures,
 	}
+
+	configuration.Parse()
+
 	if err := configuration.Validate(); err != nil {
 		t.Fatalf("this configuration should have been valid: %v", err)
 	}
@@ -45,6 +49,7 @@ func TestValidConfiguration(t *testing.T) {
 
 func TestInvalidConfiguration(t *testing.T) {
 	configuration := Config{}
+	configuration.Parse()
 	err := configuration.Validate()
 	if err == nil {
 		t.Fatal("this configuration should be invalid")
@@ -131,6 +136,10 @@ expiry:
 logger:
   level: "debug"
   format: "json"
+
+additionalFeatures: [
+	"ConnectorsCRUD"
+]
 `)
 
 	want := Config{
@@ -223,12 +232,16 @@ logger:
 			Level:  slog.LevelDebug,
 			Format: "json",
 		},
+		AdditionalFeatures: server.ValidAdditionalFeatures,
 	}
 
 	var c Config
 	if err := yaml.Unmarshal(rawConfig, &c); err != nil {
 		t.Fatalf("failed to decode config: %v", err)
 	}
+
+	c.Parse()
+
 	if diff := pretty.Compare(c, want); diff != "" {
 		t.Errorf("got!=want: %s", diff)
 	}
@@ -436,7 +449,19 @@ logger:
 	if err := yaml.Unmarshal(rawConfig, &c); err != nil {
 		t.Fatalf("failed to decode config: %v", err)
 	}
+
+	c.Parse()
+
 	if diff := pretty.Compare(c, want); diff != "" {
 		t.Errorf("got!=want: %s", diff)
 	}
 }
+
+func TestParseConfig(t *testing.T) {
+	configuration := Config{}
+	configuration.Parse()
+
+	if configuration.AdditionalFeatures == nil || len(configuration.AdditionalFeatures) != 0 {
+		t.Fatal("AdditionalFeatures should be an empty slice")
+	}
+}
diff --git a/server/api_test.go b/server/api_test.go
index e4150f1f..88601ec9 100644
--- a/server/api_test.go
+++ b/server/api_test.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"log/slog"
 	"net"
+	"strings"
 	"testing"
 	"time"
 
@@ -29,14 +30,14 @@ type apiClient struct {
 }
 
 // newAPI constructs a gRCP client connected to a backing server.
-func newAPI(s storage.Storage, logger *slog.Logger, t *testing.T) *apiClient {
+func newAPI(s storage.Storage, logger *slog.Logger, t *testing.T, addtionalFeatures []AdditionalFeature) *apiClient {
 	l, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	serv := grpc.NewServer()
-	api.RegisterDexServer(serv, NewAPI(s, logger, "test"))
+	api.RegisterDexServer(serv, NewAPI(s, logger, "test", addtionalFeatures))
 	go serv.Serve(l)
 
 	// NewClient will retry automatically if the serv.Serve() goroutine
@@ -61,7 +62,7 @@ func TestPassword(t *testing.T) {
 	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
-	client := newAPI(s, logger, t)
+	client := newAPI(s, logger, t, []AdditionalFeature{})
 	defer client.Close()
 
 	ctx := context.Background()
@@ -170,7 +171,7 @@ func TestCheckCost(t *testing.T) {
 	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
-	client := newAPI(s, logger, t)
+	client := newAPI(s, logger, t, []AdditionalFeature{})
 	defer client.Close()
 
 	tests := []struct {
@@ -223,7 +224,7 @@ func TestRefreshToken(t *testing.T) {
 	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
-	client := newAPI(s, logger, t)
+	client := newAPI(s, logger, t, []AdditionalFeature{})
 	defer client.Close()
 
 	ctx := context.Background()
@@ -332,7 +333,7 @@ func TestUpdateClient(t *testing.T) {
 	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
-	client := newAPI(s, logger, t)
+	client := newAPI(s, logger, t, []AdditionalFeature{})
 	defer client.Close()
 	ctx := context.Background()
 
@@ -490,3 +491,233 @@ func find(item string, items []string) bool {
 	}
 	return false
 }
+
+func TestCreateConnector(t *testing.T) {
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
+
+	s := memory.New(logger)
+	client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
+	defer client.Close()
+
+	ctx := context.Background()
+	connectorID := "connector123"
+	connectorName := "TestConnector"
+	connectorType := "TestType"
+	connectorConfig := []byte(`{"key": "value"}`)
+
+	createReq := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     connectorID,
+			Name:   connectorName,
+			Type:   connectorType,
+			Config: connectorConfig,
+		},
+	}
+
+	// Test valid connector creation
+	if resp, err := client.CreateConnector(ctx, &createReq); err != nil || resp.AlreadyExists {
+		if err != nil {
+			t.Fatalf("Unable to create connector: %v", err)
+		} else if resp.AlreadyExists {
+			t.Fatalf("Unable to create connector since %s already exists", connectorID)
+		}
+		t.Fatalf("Unable to create connector: %v", err)
+	}
+
+	// Test creating the same connector again (expecting failure)
+	if resp, _ := client.CreateConnector(ctx, &createReq); !resp.AlreadyExists {
+		t.Fatalf("Created connector %s twice", connectorID)
+	}
+
+	createReq.Connector.Config = []byte("invalid_json")
+
+	// Test invalid JSON config
+	if _, err := client.CreateConnector(ctx, &createReq); err == nil {
+		t.Fatal("Expected an error for invalid JSON config, but none occurred")
+	} else if !strings.Contains(err.Error(), "invalid config supplied") {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+}
+
+func TestUpdateConnector(t *testing.T) {
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
+
+	s := memory.New(logger)
+	client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
+	defer client.Close()
+
+	ctx := context.Background()
+	connectorID := "connector123"
+	newConnectorName := "UpdatedConnector"
+	newConnectorType := "UpdatedType"
+	newConnectorConfig := []byte(`{"updated_key": "updated_value"}`)
+
+	// Create a connector for testing
+	createReq := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     connectorID,
+			Name:   "TestConnector",
+			Type:   "TestType",
+			Config: []byte(`{"key": "value"}`),
+		},
+	}
+	client.CreateConnector(ctx, &createReq)
+
+	updateReq := api.UpdateConnectorReq{
+		Id:        connectorID,
+		NewName:   newConnectorName,
+		NewType:   newConnectorType,
+		NewConfig: newConnectorConfig,
+	}
+
+	// Test valid connector update
+	if _, err := client.UpdateConnector(ctx, &updateReq); err != nil {
+		t.Fatalf("Unable to update connector: %v", err)
+	}
+
+	resp, err := client.ListConnectors(ctx, &api.ListConnectorReq{})
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+
+	for _, connector := range resp.Connectors {
+		if connector.Id == connectorID {
+			if connector.Name != newConnectorName {
+				t.Fatal("connector name should have been updated")
+			}
+			if string(connector.Config) != string(newConnectorConfig) {
+				t.Fatal("connector config should have been updated")
+			}
+			if connector.Type != newConnectorType {
+				t.Fatal("connector type should have been updated")
+			}
+		}
+	}
+
+	updateReq.NewConfig = []byte("invalid_json")
+
+	// Test invalid JSON config in update request
+	if _, err := client.UpdateConnector(ctx, &updateReq); err == nil {
+		t.Fatal("Expected an error for invalid JSON config in update, but none occurred")
+	} else if !strings.Contains(err.Error(), "invalid config supplied") {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+}
+
+func TestDeleteConnector(t *testing.T) {
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
+
+	s := memory.New(logger)
+	client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
+	defer client.Close()
+
+	ctx := context.Background()
+	connectorID := "connector123"
+
+	// Create a connector for testing
+	createReq := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     connectorID,
+			Name:   "TestConnector",
+			Type:   "TestType",
+			Config: []byte(`{"key": "value"}`),
+		},
+	}
+	client.CreateConnector(ctx, &createReq)
+
+	deleteReq := api.DeleteConnectorReq{
+		Id: connectorID,
+	}
+
+	// Test valid connector deletion
+	if _, err := client.DeleteConnector(ctx, &deleteReq); err != nil {
+		t.Fatalf("Unable to delete connector: %v", err)
+	}
+
+	// Test non existent connector deletion
+	resp, err := client.DeleteConnector(ctx, &deleteReq)
+	if err != nil {
+		t.Fatalf("Unable to delete connector: %v", err)
+	}
+
+	if !resp.NotFound {
+		t.Fatal("Should return not found")
+	}
+}
+
+func TestListConnectors(t *testing.T) {
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
+
+	s := memory.New(logger)
+	client := newAPI(s, logger, t, []AdditionalFeature{ConnectorsCRUD})
+	defer client.Close()
+
+	ctx := context.Background()
+
+	// Create connectors for testing
+	createReq1 := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     "connector1",
+			Name:   "Connector1",
+			Type:   "Type1",
+			Config: []byte(`{"key": "value1"}`),
+		},
+	}
+	client.CreateConnector(ctx, &createReq1)
+
+	createReq2 := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     "connector2",
+			Name:   "Connector2",
+			Type:   "Type2",
+			Config: []byte(`{"key": "value2"}`),
+		},
+	}
+	client.CreateConnector(ctx, &createReq2)
+
+	listReq := api.ListConnectorReq{}
+
+	// Test listing connectors
+	if resp, err := client.ListConnectors(ctx, &listReq); err != nil {
+		t.Fatalf("Unable to list connectors: %v", err)
+	} else if len(resp.Connectors) != 2 { // Check the number of connectors in the response
+		t.Fatalf("Expected 2 connectors, found %d", len(resp.Connectors))
+	}
+}
+
+func TestMissingAdditionalFeature(t *testing.T) {
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
+
+	s := memory.New(logger)
+	client := newAPI(s, logger, t, []AdditionalFeature{})
+	defer client.Close()
+
+	ctx := context.Background()
+
+	// Create connectors for testing
+	createReq1 := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     "connector1",
+			Name:   "Connector1",
+			Type:   "Type1",
+			Config: []byte(`{"key": "value1"}`),
+		},
+	}
+	client.CreateConnector(ctx, &createReq1)
+
+	createReq2 := api.CreateConnectorReq{
+		Connector: &api.Connector{
+			Id:     "connector2",
+			Name:   "Connector2",
+			Type:   "Type2",
+			Config: []byte(`{"key": "value2"}`),
+		},
+	}
+	client.CreateConnector(ctx, &createReq2)
+
+	listReq := api.ListConnectorReq{}
+
+	if _, err := client.ListConnectors(ctx, &listReq); err == nil {
+		t.Fatal("ListConnectors should have returned an error")
+	}
+}