Repository: coreos/dex
Pull request #802 (2017-02-14 22:50:09)

api: adding a gRPC call for revoking refresh tokens.
Added a gRPC call that will enable users to revoke client specific refresh tokens.

Also added a test in api_test.go. Since we do not have APIs in place for creating refresh tokens and offline session objects, this has been done using the storage methods. If this test case seems like an overkill, I could get rid of it.

ericchiang: thanks for the test. couple nits

Patch:
diff --git a/api/api.pb.go b/api/api.pb.go
index ac500a18..43bb6247 100644
--- a/api/api.pb.go
+++ b/api/api.pb.go
@@ -28,6 +28,8 @@ It has these top-level messages:
 	RefreshTokenRef
 	ListRefreshReq
 	ListRefreshResp
+	RevokeRefreshReq
+	RevokeRefreshResp
 */
 package api
 
@@ -298,6 +300,26 @@ func (m *ListRefreshResp) GetRefreshTokens() []*RefreshTokenRef {
 	return nil
 }
 
+type RevokeRefreshReq struct {
+	// The "sub" claim returned in the ID Token.
+	UserId   string `protobuf:"bytes,1,opt,name=user_id,json=userId" json:"user_id,omitempty"`
+	ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId" json:"client_id,omitempty"`
+}
+
+func (m *RevokeRefreshReq) Reset()                    { *m = RevokeRefreshReq{} }
+func (m *RevokeRefreshReq) String() string            { return proto.CompactTextString(m) }
+func (*RevokeRefreshReq) ProtoMessage()               {}
+func (*RevokeRefreshReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{19} }
+
+type RevokeRefreshResp struct {
+	NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound" json:"not_found,omitempty"`
+}
+
+func (m *RevokeRefreshResp) Reset()                    { *m = RevokeRefreshResp{} }
+func (m *RevokeRefreshResp) String() string            { return proto.CompactTextString(m) }
+func (*RevokeRefreshResp) ProtoMessage()               {}
+func (*RevokeRefreshResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{20} }
+
 func init() {
 	proto.RegisterType((*Client)(nil), "api.Client")
 	proto.RegisterType((*CreateClientReq)(nil), "api.CreateClientReq")
@@ -318,6 +340,8 @@ func init() {
 	proto.RegisterType((*RefreshTokenRef)(nil), "api.RefreshTokenRef")
 	proto.RegisterType((*ListRefreshReq)(nil), "api.ListRefreshReq")
 	proto.RegisterType((*ListRefreshResp)(nil), "api.ListRefreshResp")
+	proto.RegisterType((*RevokeRefreshReq)(nil), "api.RevokeRefreshReq")
+	proto.RegisterType((*RevokeRefreshResp)(nil), "api.RevokeRefreshResp")
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -347,6 +371,9 @@ type DexClient interface {
 	GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error)
 	// ListRefresh lists all the refresh token entries for a particular user.
 	ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error)
+	// RevokeRefresh revokes the refresh token for the provided user-client pair.
+	// Note that each user-client pair can have only one refresh token at a time.
+	RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error)
 }
 
 type dexClient struct {
@@ -429,6 +456,15 @@ func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts ..
 	return out, nil
 }
 
+func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error) {
+	out := new(RevokeRefreshResp)
+	err := grpc.Invoke(ctx, "/api.Dex/RevokeRefresh", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // Server API for Dex service
 
 type DexServer interface {
@@ -448,6 +484,9 @@ type DexServer interface {
 	GetVersion(context.Context, *VersionReq) (*VersionResp, error)
 	// ListRefresh lists all the refresh token entries for a particular user.
 	ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error)
+	// RevokeRefresh revokes the refresh token for the provided user-client pair.
+	// Note that each user-client pair can have only one refresh token at a time.
+	RevokeRefresh(context.Context, *RevokeRefreshReq) (*RevokeRefreshResp, error)
 }
 
 func RegisterDexServer(s *grpc.Server, srv DexServer) {
@@ -598,6 +637,24 @@ func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(int
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Dex_RevokeRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RevokeRefreshReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(DexServer).RevokeRefresh(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/api.Dex/RevokeRefresh",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(DexServer).RevokeRefresh(ctx, req.(*RevokeRefreshReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 var _Dex_serviceDesc = grpc.ServiceDesc{
 	ServiceName: "api.Dex",
 	HandlerType: (*DexServer)(nil),
@@ -634,6 +691,10 @@ var _Dex_serviceDesc = grpc.ServiceDesc{
 			MethodName: "ListRefresh",
 			Handler:    _Dex_ListRefresh_Handler,
 		},
+		{
+			MethodName: "RevokeRefresh",
+			Handler:    _Dex_RevokeRefresh_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: fileDescriptor0,
@@ -642,52 +703,54 @@ var _Dex_serviceDesc = grpc.ServiceDesc{
 func init() { proto.RegisterFile("api/api.proto", fileDescriptor0) }
 
 var fileDescriptor0 = []byte{
-	// 742 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x55, 0x6b, 0x4b, 0x1c, 0x4b,
-	0x10, 0xdd, 0x97, 0xbb, 0xb3, 0xb5, 0xef, 0xbe, 0x5e, 0x1d, 0x57, 0x2e, 0x68, 0xcb, 0x05, 0xe5,
-	0x82, 0xa2, 0x17, 0x12, 0x88, 0xc4, 0x10, 0x34, 0x0f, 0x21, 0x04, 0x19, 0xb2, 0xf9, 0x98, 0x61,
-	0xdc, 0x29, 0xb5, 0xc9, 0x38, 0x33, 0xe9, 0xee, 0xcd, 0x9a, 0x7c, 0xcb, 0x4f, 0xcb, 0x3f, 0x0b,
-	0xfd, 0xd8, 0x75, 0x1e, 0x06, 0xf3, 0x6d, 0xea, 0x74, 0xd7, 0xa9, 0xae, 0xd3, 0xa7, 0x6b, 0xa0,
-	0x17, 0xa4, 0xec, 0x20, 0x48, 0xd9, 0x7e, 0xca, 0x13, 0x99, 0x90, 0x7a, 0x90, 0x32, 0xfa, 0xb3,
-	0x0a, 0xcd, 0xd3, 0x88, 0x61, 0x2c, 0x49, 0x1f, 0x6a, 0x2c, 0x74, 0xab, 0x5b, 0xd5, 0xdd, 0xb6,
-	0x57, 0x63, 0x21, 0x59, 0x83, 0xa6, 0xc0, 0x29, 0x47, 0xe9, 0xd6, 0x34, 0x66, 0x23, 0xb2, 0x03,
-	0x3d, 0x8e, 0x21, 0xe3, 0x38, 0x95, 0xfe, 0x8c, 0x33, 0xe1, 0xd6, 0xb7, 0xea, 0xbb, 0x6d, 0xaf,
-	0xbb, 0x00, 0x27, 0x9c, 0x09, 0xb5, 0x49, 0xf2, 0x99, 0x90, 0x18, 0xfa, 0x29, 0x22, 0x17, 0x6e,
-	0xc3, 0x6c, 0xb2, 0xe0, 0x85, 0xc2, 0x54, 0x85, 0x74, 0x76, 0x19, 0xb1, 0xa9, 0xbb, 0xb2, 0x55,
-	0xdd, 0x75, 0x3c, 0x1b, 0x11, 0x02, 0x8d, 0x38, 0xb8, 0x45, 0xb7, 0xa9, 0xeb, 0xea, 0x6f, 0xb2,
-	0x01, 0x4e, 0x94, 0x5c, 0x27, 0xfe, 0x8c, 0x47, 0x6e, 0x4b, 0xe3, 0x2d, 0x15, 0x4f, 0x78, 0x44,
-	0x9f, 0xc0, 0xe0, 0x94, 0x63, 0x20, 0xd1, 0x34, 0xe2, 0xe1, 0x17, 0xb2, 0x03, 0xcd, 0xa9, 0x0e,
-	0x74, 0x3f, 0x9d, 0xa3, 0xce, 0xbe, 0xea, 0xdb, 0xae, 0xdb, 0x25, 0xfa, 0x09, 0x86, 0xf9, 0x3c,
-	0x91, 0x92, 0x7f, 0xa1, 0x1f, 0x44, 0x1c, 0x83, 0xf0, 0x9b, 0x8f, 0x77, 0x4c, 0x48, 0xa1, 0x09,
-	0x1c, 0xaf, 0x67, 0xd1, 0x57, 0x1a, 0xcc, 0xf0, 0xd7, 0x7e, 0xcf, 0xbf, 0x0d, 0x83, 0x33, 0x8c,
-	0x30, 0x7b, 0xae, 0x82, 0xc6, 0xf4, 0x00, 0x86, 0xf9, 0x2d, 0x22, 0x25, 0x9b, 0xd0, 0x8e, 0x13,
-	0xe9, 0x5f, 0x25, 0xb3, 0x38, 0xb4, 0xd5, 0x9d, 0x38, 0x91, 0xaf, 0x55, 0x4c, 0x19, 0x38, 0x17,
-	0x81, 0x10, 0xf3, 0x84, 0x87, 0x64, 0x15, 0x56, 0xf0, 0x36, 0x60, 0x91, 0xe5, 0x33, 0x81, 0x12,
-	0xef, 0x26, 0x10, 0x37, 0xfa, 0x60, 0x5d, 0x4f, 0x7f, 0x93, 0x31, 0x38, 0x33, 0x81, 0x5c, 0x8b,
-	0x5a, 0xd7, 0x9b, 0x97, 0x31, 0x59, 0x87, 0x96, 0xfa, 0xf6, 0x59, 0xe8, 0x36, 0xcc, 0x3d, 0xab,
-	0xf0, 0x3c, 0xa4, 0x27, 0x30, 0x32, 0xf2, 0x2c, 0x0a, 0xaa, 0x06, 0xf6, 0xc0, 0x49, 0x6d, 0x68,
-	0xa5, 0xed, 0xe9, 0xd6, 0x97, 0x7b, 0x96, 0xcb, 0xf4, 0x18, 0x48, 0x31, 0xff, 0x8f, 0x05, 0xa6,
-	0xd7, 0x30, 0x9a, 0xa4, 0x61, 0xa1, 0xf8, 0xc3, 0x0d, 0x6f, 0x80, 0x13, 0xe3, 0xdc, 0xcf, 0x34,
-	0xdd, 0x8a, 0x71, 0xfe, 0x56, 0xf5, 0xbd, 0x0d, 0x5d, 0xb5, 0x54, 0xe8, 0xbd, 0x13, 0xe3, 0x7c,
-	0x62, 0x21, 0x7a, 0x08, 0xa4, 0x58, 0xe8, 0xb1, 0x3b, 0xd8, 0x83, 0x91, 0xb9, 0xb4, 0x47, 0xcf,
-	0xa6, 0xd8, 0x8b, 0x5b, 0x1f, 0x63, 0x1f, 0xc1, 0xe0, 0x1d, 0x13, 0x32, 0xc3, 0x4d, 0x5f, 0xc0,
-	0x30, 0x0f, 0x89, 0x94, 0xfc, 0x07, 0xed, 0x85, 0xd2, 0x4a, 0xc2, 0x7a, 0xf9, 0x26, 0xee, 0xd7,
-	0x69, 0x17, 0xe0, 0x23, 0x72, 0xc1, 0x92, 0x58, 0xd1, 0x3d, 0x85, 0xce, 0x32, 0x12, 0xa9, 0x79,
-	0xe7, 0xfc, 0x2b, 0x72, 0x7b, 0x74, 0x1b, 0x91, 0x21, 0xa8, 0x09, 0xa1, 0x25, 0x5d, 0xf1, 0xf4,
-	0xb0, 0xf8, 0x0e, 0x03, 0x0f, 0xaf, 0x38, 0x8a, 0x9b, 0x0f, 0xc9, 0x67, 0x8c, 0x3d, 0xbc, 0x2a,
-	0x0d, 0x8d, 0x4d, 0x68, 0x1b, 0xf7, 0x2b, 0x3f, 0x99, 0xb9, 0xe1, 0x18, 0xe0, 0x3c, 0x24, 0xff,
-	0x00, 0x4c, 0xb5, 0x23, 0x42, 0x3f, 0x90, 0xf6, 0x32, 0xda, 0x16, 0x79, 0x29, 0x55, 0x6e, 0x14,
-	0x08, 0xa9, 0xae, 0x6b, 0xe1, 0x45, 0x47, 0x01, 0x13, 0x81, 0x4a, 0xf4, 0xbe, 0xd2, 0xc0, 0xd6,
-	0x57, 0x8a, 0x67, 0x8c, 0x5b, 0xcd, 0x19, 0xf7, 0xbd, 0x51, 0x70, 0xb9, 0x55, 0xa4, 0xe4, 0x18,
-	0xfa, 0xdc, 0x84, 0xbe, 0x54, 0x47, 0x5f, 0x48, 0xb6, 0xaa, 0x25, 0x2b, 0x34, 0xe5, 0xf5, 0x78,
-	0x06, 0x10, 0x47, 0x3f, 0x1a, 0x50, 0x3f, 0xc3, 0x3b, 0xf2, 0x1c, 0xba, 0xd9, 0x79, 0x41, 0x4c,
-	0x72, 0x61, 0xf4, 0x8c, 0xff, 0x7e, 0x00, 0x15, 0x29, 0xad, 0xa8, 0xf4, 0xec, 0x5b, 0xb7, 0xe9,
-	0x85, 0x09, 0x61, 0xd3, 0x8b, 0x43, 0x81, 0x56, 0xc8, 0x29, 0xf4, 0xf3, 0xcf, 0x89, 0xac, 0x65,
-	0x2a, 0x65, 0xec, 0x32, 0x5e, 0x7f, 0x10, 0x5f, 0x90, 0xe4, 0xdd, 0x6e, 0x49, 0x4a, 0x6f, 0xcd,
-	0x92, 0x94, 0x9f, 0x86, 0x21, 0xc9, 0x9b, 0xda, 0x92, 0x94, 0x1e, 0x85, 0x25, 0x29, 0xbf, 0x00,
-	0x5a, 0x21, 0x27, 0xd0, 0xcb, 0x7a, 0x5a, 0x58, 0x39, 0x0a, 0xd6, 0xb7, 0x72, 0x14, 0xdd, 0x4f,
-	0x2b, 0xe4, 0x10, 0xe0, 0x0d, 0x4a, 0xeb, 0x63, 0x32, 0xd0, 0xdb, 0xee, 0x3d, 0x3e, 0x1e, 0xe6,
-	0x01, 0x9d, 0xf2, 0x0c, 0x3a, 0x19, 0x5f, 0x90, 0xbf, 0x96, 0xd4, 0xf7, 0xa6, 0x1a, 0xaf, 0x96,
-	0x41, 0x95, 0x7b, 0xd9, 0xd4, 0xff, 0xcc, 0xff, 0x7f, 0x05, 0x00, 0x00, 0xff, 0xff, 0x57, 0x96,
-	0xbe, 0xf0, 0x44, 0x07, 0x00, 0x00,
+	// 781 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x55, 0x6d, 0x4f, 0xdb, 0x48,
+	0x10, 0xce, 0x1b, 0x89, 0x33, 0x79, 0xdf, 0xe3, 0xc5, 0x04, 0x9d, 0x04, 0x8b, 0x4e, 0x02, 0x9d,
+	0x04, 0x07, 0x27, 0xb5, 0x52, 0x51, 0x69, 0x2b, 0x68, 0x0b, 0x52, 0x55, 0x21, 0xab, 0xe9, 0xc7,
+	0x5a, 0x26, 0x1e, 0x60, 0x85, 0xb1, 0xdd, 0xdd, 0x0d, 0xa1, 0xfd, 0x77, 0x55, 0xff, 0x58, 0xb5,
+	0xeb, 0x4d, 0xb0, 0x9d, 0xb4, 0xe1, 0x9b, 0xe7, 0xd9, 0x99, 0x67, 0x76, 0x9e, 0x9d, 0x19, 0x43,
+	0xcb, 0x8b, 0xd9, 0xbe, 0x17, 0xb3, 0xbd, 0x98, 0x47, 0x32, 0x22, 0x65, 0x2f, 0x66, 0xf4, 0x47,
+	0x11, 0xaa, 0x27, 0x01, 0xc3, 0x50, 0x92, 0x36, 0x94, 0x98, 0x6f, 0x17, 0x37, 0x8b, 0x3b, 0x75,
+	0xa7, 0xc4, 0x7c, 0xb2, 0x0a, 0x55, 0x81, 0x43, 0x8e, 0xd2, 0x2e, 0x69, 0xcc, 0x58, 0x64, 0x1b,
+	0x5a, 0x1c, 0x7d, 0xc6, 0x71, 0x28, 0xdd, 0x11, 0x67, 0xc2, 0x2e, 0x6f, 0x96, 0x77, 0xea, 0x4e,
+	0x73, 0x02, 0x0e, 0x38, 0x13, 0xca, 0x49, 0xf2, 0x91, 0x90, 0xe8, 0xbb, 0x31, 0x22, 0x17, 0x76,
+	0x25, 0x71, 0x32, 0xe0, 0x85, 0xc2, 0x54, 0x86, 0x78, 0x74, 0x19, 0xb0, 0xa1, 0xbd, 0xb4, 0x59,
+	0xdc, 0xb1, 0x1c, 0x63, 0x11, 0x02, 0x95, 0xd0, 0xbb, 0x43, 0xbb, 0xaa, 0xf3, 0xea, 0x6f, 0xb2,
+	0x0e, 0x56, 0x10, 0x5d, 0x47, 0xee, 0x88, 0x07, 0x76, 0x4d, 0xe3, 0x35, 0x65, 0x0f, 0x78, 0x40,
+	0x9f, 0x41, 0xe7, 0x84, 0xa3, 0x27, 0x31, 0x29, 0xc4, 0xc1, 0xaf, 0x64, 0x1b, 0xaa, 0x43, 0x6d,
+	0xe8, 0x7a, 0x1a, 0x87, 0x8d, 0x3d, 0x55, 0xb7, 0x39, 0x37, 0x47, 0xf4, 0x0b, 0x74, 0xb3, 0x71,
+	0x22, 0x26, 0xff, 0x40, 0xdb, 0x0b, 0x38, 0x7a, 0xfe, 0x37, 0x17, 0x1f, 0x98, 0x90, 0x42, 0x13,
+	0x58, 0x4e, 0xcb, 0xa0, 0x6f, 0x35, 0x98, 0xe2, 0x2f, 0xfd, 0x9e, 0x7f, 0x0b, 0x3a, 0xa7, 0x18,
+	0x60, 0xfa, 0x5e, 0x39, 0x8d, 0xe9, 0x3e, 0x74, 0xb3, 0x2e, 0x22, 0x26, 0x1b, 0x50, 0x0f, 0x23,
+	0xe9, 0x5e, 0x45, 0xa3, 0xd0, 0x37, 0xd9, 0xad, 0x30, 0x92, 0xef, 0x94, 0x4d, 0x19, 0x58, 0x17,
+	0x9e, 0x10, 0xe3, 0x88, 0xfb, 0x64, 0x19, 0x96, 0xf0, 0xce, 0x63, 0x81, 0xe1, 0x4b, 0x0c, 0x25,
+	0xde, 0x8d, 0x27, 0x6e, 0xf4, 0xc5, 0x9a, 0x8e, 0xfe, 0x26, 0x7d, 0xb0, 0x46, 0x02, 0xb9, 0x16,
+	0xb5, 0xac, 0x9d, 0xa7, 0x36, 0x59, 0x83, 0x9a, 0xfa, 0x76, 0x99, 0x6f, 0x57, 0x92, 0x77, 0x56,
+	0xe6, 0xb9, 0x4f, 0x8f, 0xa1, 0x97, 0xc8, 0x33, 0x49, 0xa8, 0x0a, 0xd8, 0x05, 0x2b, 0x36, 0xa6,
+	0x91, 0xb6, 0xa5, 0x4b, 0x9f, 0xfa, 0x4c, 0x8f, 0xe9, 0x11, 0x90, 0x7c, 0xfc, 0x93, 0x05, 0xa6,
+	0xd7, 0xd0, 0x1b, 0xc4, 0x7e, 0x2e, 0xf9, 0xfc, 0x82, 0xd7, 0xc1, 0x0a, 0x71, 0xec, 0xa6, 0x8a,
+	0xae, 0x85, 0x38, 0x3e, 0x53, 0x75, 0x6f, 0x41, 0x53, 0x1d, 0xe5, 0x6a, 0x6f, 0x84, 0x38, 0x1e,
+	0x18, 0x88, 0x1e, 0x00, 0xc9, 0x27, 0x5a, 0xf4, 0x06, 0xbb, 0xd0, 0x4b, 0x1e, 0x6d, 0xe1, 0xdd,
+	0x14, 0x7b, 0xde, 0x75, 0x11, 0x7b, 0x0f, 0x3a, 0x1f, 0x98, 0x90, 0x29, 0x6e, 0xfa, 0x0a, 0xba,
+	0x59, 0x48, 0xc4, 0xe4, 0x5f, 0xa8, 0x4f, 0x94, 0x56, 0x12, 0x96, 0x67, 0x5f, 0xe2, 0xf1, 0x9c,
+	0x36, 0x01, 0x3e, 0x23, 0x17, 0x2c, 0x0a, 0x15, 0xdd, 0x73, 0x68, 0x4c, 0x2d, 0x11, 0x27, 0x73,
+	0xce, 0xef, 0x91, 0x9b, 0xab, 0x1b, 0x8b, 0x74, 0x41, 0x6d, 0x08, 0x2d, 0xe9, 0x92, 0xa3, 0x97,
+	0xc5, 0x77, 0xe8, 0x38, 0x78, 0xc5, 0x51, 0xdc, 0x7c, 0x8a, 0x6e, 0x31, 0x74, 0xf0, 0x6a, 0x66,
+	0x69, 0x6c, 0x40, 0x3d, 0xe9, 0x7e, 0xd5, 0x4f, 0xc9, 0xde, 0xb0, 0x12, 0xe0, 0xdc, 0x27, 0x7f,
+	0x03, 0x0c, 0x75, 0x47, 0xf8, 0xae, 0x27, 0xcd, 0x63, 0xd4, 0x0d, 0xf2, 0x46, 0xaa, 0xd8, 0xc0,
+	0x13, 0x52, 0x3d, 0xd7, 0xa4, 0x17, 0x2d, 0x05, 0x0c, 0x04, 0x2a, 0xd1, 0xdb, 0x4a, 0x03, 0x93,
+	0x5f, 0x29, 0x9e, 0x6a, 0xdc, 0x62, 0xa6, 0x71, 0x3f, 0x26, 0x0a, 0x4e, 0x5d, 0x45, 0x4c, 0x8e,
+	0xa0, 0xcd, 0x13, 0xd3, 0x95, 0xea, 0xea, 0x13, 0xc9, 0x96, 0xb5, 0x64, 0xb9, 0xa2, 0x9c, 0x16,
+	0x4f, 0x01, 0x82, 0x9e, 0x41, 0xd7, 0xc1, 0xfb, 0xe8, 0x16, 0x9f, 0x90, 0xfc, 0x8f, 0x02, 0xd0,
+	0xff, 0xa0, 0x97, 0x63, 0x5a, 0xd0, 0x0d, 0x87, 0x3f, 0x2b, 0x50, 0x3e, 0xc5, 0x07, 0xf2, 0x12,
+	0x9a, 0xe9, 0x5d, 0x45, 0x92, 0x8b, 0xe7, 0xd6, 0x5e, 0x7f, 0x65, 0x0e, 0x2a, 0x62, 0x5a, 0x50,
+	0xe1, 0xe9, 0x3d, 0x63, 0xc2, 0x73, 0xdb, 0xc9, 0x84, 0xe7, 0x17, 0x12, 0x2d, 0x90, 0x13, 0x68,
+	0x67, 0x47, 0x99, 0xac, 0xa6, 0x32, 0xa5, 0x5a, 0xb5, 0xbf, 0x36, 0x17, 0x9f, 0x90, 0x64, 0x27,
+	0xcd, 0x90, 0xcc, 0xcc, 0xb9, 0x21, 0x99, 0x1d, 0xcb, 0x84, 0x24, 0x3b, 0x50, 0x86, 0x64, 0x66,
+	0x20, 0x0d, 0xc9, 0xec, 0xf4, 0xd1, 0x02, 0x39, 0x86, 0x56, 0x7a, 0x9e, 0x84, 0x91, 0x23, 0x37,
+	0x76, 0x46, 0x8e, 0xfc, 0xe4, 0xd1, 0x02, 0x39, 0x00, 0x78, 0x8f, 0xd2, 0xcc, 0x10, 0xe9, 0x68,
+	0xb7, 0xc7, 0xf9, 0xea, 0x77, 0xb3, 0x80, 0x0e, 0x79, 0x01, 0x8d, 0x54, 0x4f, 0x92, 0xbf, 0xa6,
+	0xd4, 0x8f, 0x3d, 0xd5, 0x5f, 0x9e, 0x05, 0x75, 0xec, 0x6b, 0x68, 0x65, 0xba, 0x86, 0xac, 0x98,
+	0xae, 0xcd, 0xf6, 0x64, 0x7f, 0x75, 0x1e, 0xac, 0x18, 0x2e, 0xab, 0xfa, 0x8f, 0xff, 0xff, 0xaf,
+	0x00, 0x00, 0x00, 0xff, 0xff, 0x08, 0x02, 0xa2, 0x47, 0x02, 0x08, 0x00, 0x00,
 }
diff --git a/api/api.proto b/api/api.proto
index 82381d1a..5be7b337 100644
--- a/api/api.proto
+++ b/api/api.proto
@@ -30,7 +30,7 @@ message DeleteClientReq {
   string id = 1;
 }
 
-// DeleteClientResp determines if the. 
+// DeleteClientResp determines if the client is deleted successfully. 
 message DeleteClientResp {
   bool not_found = 1;
 }
@@ -120,6 +120,19 @@ message ListRefreshResp {
   repeated RefreshTokenRef refresh_tokens = 1;
 }
 
+// RevokeRefreshReq is a request to revoke the refresh token of the user-client pair.
+message RevokeRefreshReq {
+  // The "sub" claim returned in the ID Token.
+  string user_id = 1;
+  string client_id = 2;
+}
+
+// RevokeRefreshResp determines if the refresh token is revoked successfully. 
+message RevokeRefreshResp {
+  // Set to true is refresh token was not found and token could not be revoked.
+  bool not_found = 1;
+}
+
 // Dex represents the dex gRPC service.
 service Dex {
   // CreateClient creates a client.
@@ -138,4 +151,8 @@ service Dex {
   rpc GetVersion(VersionReq) returns (VersionResp) {};
   // ListRefresh lists all the refresh token entries for a particular user.
   rpc ListRefresh(ListRefreshReq) returns (ListRefreshResp) {};
+  // RevokeRefresh revokes the refresh token for the provided user-client pair.
+  //
+  // Note that each user-client pair can have only one refresh token at a time.
+  rpc RevokeRefresh(RevokeRefreshReq) returns (RevokeRefreshResp) {};
 }
diff --git a/server/api.go b/server/api.go
index 8dc2ea57..cbb030c7 100644
--- a/server/api.go
+++ b/server/api.go
@@ -16,7 +16,7 @@ import (
 
 // apiVersion increases every time a new call is added to the API. Clients should use this info
 // to determine if the server supports specific features.
-const apiVersion = 0
+const apiVersion = 1
 
 // NewAPI returns a server which implements the gRPC API interface.
 func NewAPI(s storage.Storage, logger logrus.FieldLogger) api.DexServer {
@@ -204,13 +204,13 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.
 	id := new(internal.IDTokenSubject)
 	if err := internal.Unmarshal(req.UserId, id); err != nil {
 		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
-		return nil, fmt.Errorf("unmarshal ID Token subject: %v", err)
+		return nil, err
 	}
 
 	offlineSessions, err := d.s.GetOfflineSessions(id.UserId, id.ConnId)
 	if err != nil {
 		d.logger.Errorf("api: failed to list refresh tokens: %v", err)
-		return nil, fmt.Errorf("list refresh tokens: %v", err)
+		return nil, err
 	}
 
 	var refreshTokenRefs []*api.RefreshTokenRef
@@ -228,3 +228,39 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.
 		RefreshTokens: refreshTokenRefs,
 	}, nil
 }
+
+func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) {
+	id := new(internal.IDTokenSubject)
+	if err := internal.Unmarshal(req.UserId, id); err != nil {
+		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
+		return nil, err
+	}
+
+	var refreshID string
+	updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
+		if refreshID = old.Refresh[req.ClientId].ID; refreshID == "" {
+			return old, fmt.Errorf("user does not have a refresh token for the client = %s", req.ClientId)
+		}
+
+		// Remove entry from Refresh list of the OfflineSession object.
+		delete(old.Refresh, req.ClientId)
+
+		return old, nil
+	}
+
+	if err := d.s.UpdateOfflineSessions(id.UserId, id.ConnId, updater); err != nil {
+		if err == storage.ErrNotFound {
+			return &api.RevokeRefreshResp{NotFound: true}, nil
+		}
+		d.logger.Errorf("api: failed to update offline session object: %v", err)
+		return nil, err
+	}
+
+	// Delete the refresh token from the storage
+	if err := d.s.DeleteRefresh(refreshID); err != nil {
+		d.logger.Errorf("failed to delete refresh token: %v", err)
+		return nil, err
+	}
+
+	return &api.RevokeRefreshResp{}, nil
+}


Test patch:
diff --git a/server/api_test.go b/server/api_test.go
index 1a386738..4ee285f2 100644
--- a/server/api_test.go
+++ b/server/api_test.go
@@ -4,9 +4,12 @@ import (
 	"context"
 	"os"
 	"testing"
+	"time"
 
 	"github.com/Sirupsen/logrus"
 	"github.com/coreos/dex/api"
+	"github.com/coreos/dex/server/internal"
+	"github.com/coreos/dex/storage"
 	"github.com/coreos/dex/storage/memory"
 )
 
@@ -65,3 +68,91 @@ func TestPassword(t *testing.T) {
 	}
 
 }
+
+// Attempts to list and revoke an exisiting refresh token.
+func TestRefreshToken(t *testing.T) {
+	logger := &logrus.Logger{
+		Out:       os.Stderr,
+		Formatter: &logrus.TextFormatter{DisableColors: true},
+		Level:     logrus.DebugLevel,
+	}
+
+	s := memory.New(logger)
+	serv := NewAPI(s, logger)
+
+	ctx := context.Background()
+
+	// Creating a storage with an existing refresh token and offline session for the user.
+	id := storage.NewID()
+	r := storage.RefreshToken{
+		ID:          id,
+		Token:       "bar",
+		Nonce:       "foo",
+		ClientID:    "client_id",
+		ConnectorID: "client_secret",
+		Scopes:      []string{"openid", "email", "profile"},
+		CreatedAt:   time.Now().UTC().Round(time.Millisecond),
+		LastUsed:    time.Now().UTC().Round(time.Millisecond),
+		Claims: storage.Claims{
+			UserID:        "1",
+			Username:      "jane",
+			Email:         "jane.doe@example.com",
+			EmailVerified: true,
+			Groups:        []string{"a", "b"},
+		},
+		ConnectorData: []byte(`{"some":"data"}`),
+	}
+
+	if err := s.CreateRefresh(r); err != nil {
+		t.Fatalf("create refresh token: %v", err)
+	}
+
+	tokenRef := storage.RefreshTokenRef{
+		ID:        r.ID,
+		ClientID:  r.ClientID,
+		CreatedAt: r.CreatedAt,
+		LastUsed:  r.LastUsed,
+	}
+
+	session := storage.OfflineSessions{
+		UserID:  r.Claims.UserID,
+		ConnID:  r.ConnectorID,
+		Refresh: make(map[string]*storage.RefreshTokenRef),
+	}
+	session.Refresh[tokenRef.ClientID] = &tokenRef
+
+	if err := s.CreateOfflineSessions(session); err != nil {
+		t.Fatalf("create offline session: %v", err)
+	}
+
+	subjectString, err := internal.Marshal(&internal.IDTokenSubject{
+		UserId: r.Claims.UserID,
+		ConnId: r.ConnectorID,
+	})
+	if err != nil {
+		t.Errorf("failed to marshal offline session ID: %v", err)
+	}
+
+	//Testing the api.
+	listReq := api.ListRefreshReq{
+		UserId: subjectString,
+	}
+
+	if _, err := serv.ListRefresh(ctx, &listReq); err != nil {
+		t.Fatalf("Unable to list refresh tokens for user: %v", err)
+	}
+
+	revokeReq := api.RevokeRefreshReq{
+		UserId:   subjectString,
+		ClientId: r.ClientID,
+	}
+
+	resp, err := serv.RevokeRefresh(ctx, &revokeReq)
+	if err != nil || resp.NotFound {
+		t.Fatalf("Unable to revoke refresh tokens for user: %v", err)
+	}
+
+	if resp, _ := serv.ListRefresh(ctx, &listReq); len(resp.RefreshTokens) != 0 {
+		t.Fatalf("Refresh token returned inspite of revoking it.")
+	}
+}