diff --git a/api/api.pb.go b/api/api.pb.go
index 5b11eaef9e737a859adde7634afe30829a4de2a4..ac500a184747539f56eb7dc3cc78cf61df05195e 100644
--- a/api/api.pb.go
+++ b/api/api.pb.go
@@ -25,6 +25,9 @@ It has these top-level messages:
 	ListPasswordResp
 	VersionReq
 	VersionResp
+	RefreshTokenRef
+	ListRefreshReq
+	ListRefreshResp
 */
 package api
 
@@ -213,7 +216,7 @@ func (m *ListPasswordReq) String() string            { return proto.CompactTextS
 func (*ListPasswordReq) ProtoMessage()               {}
 func (*ListPasswordReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} }
 
-// ListPasswordResp returs a list of passwords.
+// ListPasswordResp returns a list of passwords.
 type ListPasswordResp struct {
 	Passwords []*Password `protobuf:"bytes,1,rep,name=passwords" json:"passwords,omitempty"`
 }
@@ -253,6 +256,48 @@ func (m *VersionResp) String() string            { return proto.CompactTextStrin
 func (*VersionResp) ProtoMessage()               {}
 func (*VersionResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} }
 
+// RefreshTokenRef contains the metadata for a refresh token that is managed by the storage.
+type RefreshTokenRef struct {
+	// ID of the refresh token.
+	Id        string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
+	ClientId  string `protobuf:"bytes,2,opt,name=client_id,json=clientId" json:"client_id,omitempty"`
+	CreatedAt string `protobuf:"bytes,3,opt,name=created_at,json=createdAt" json:"created_at,omitempty"`
+	LastUsed  string `protobuf:"bytes,4,opt,name=last_used,json=lastUsed" json:"last_used,omitempty"`
+}
+
+func (m *RefreshTokenRef) Reset()                    { *m = RefreshTokenRef{} }
+func (m *RefreshTokenRef) String() string            { return proto.CompactTextString(m) }
+func (*RefreshTokenRef) ProtoMessage()               {}
+func (*RefreshTokenRef) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{16} }
+
+// ListRefreshReq is a request to enumerate the refresh tokens of a user.
+type ListRefreshReq struct {
+	// The "sub" claim returned in the ID Token.
+	UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId" json:"user_id,omitempty"`
+}
+
+func (m *ListRefreshReq) Reset()                    { *m = ListRefreshReq{} }
+func (m *ListRefreshReq) String() string            { return proto.CompactTextString(m) }
+func (*ListRefreshReq) ProtoMessage()               {}
+func (*ListRefreshReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{17} }
+
+// ListRefreshResp returns a list of refresh tokens for a user.
+type ListRefreshResp struct {
+	RefreshTokens []*RefreshTokenRef `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens" json:"refresh_tokens,omitempty"`
+}
+
+func (m *ListRefreshResp) Reset()                    { *m = ListRefreshResp{} }
+func (m *ListRefreshResp) String() string            { return proto.CompactTextString(m) }
+func (*ListRefreshResp) ProtoMessage()               {}
+func (*ListRefreshResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} }
+
+func (m *ListRefreshResp) GetRefreshTokens() []*RefreshTokenRef {
+	if m != nil {
+		return m.RefreshTokens
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterType((*Client)(nil), "api.Client")
 	proto.RegisterType((*CreateClientReq)(nil), "api.CreateClientReq")
@@ -270,6 +315,9 @@ func init() {
 	proto.RegisterType((*ListPasswordResp)(nil), "api.ListPasswordResp")
 	proto.RegisterType((*VersionReq)(nil), "api.VersionReq")
 	proto.RegisterType((*VersionResp)(nil), "api.VersionResp")
+	proto.RegisterType((*RefreshTokenRef)(nil), "api.RefreshTokenRef")
+	proto.RegisterType((*ListRefreshReq)(nil), "api.ListRefreshReq")
+	proto.RegisterType((*ListRefreshResp)(nil), "api.ListRefreshResp")
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -297,6 +345,8 @@ type DexClient interface {
 	ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, 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.
+	ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error)
 }
 
 type dexClient struct {
@@ -370,6 +420,15 @@ func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc
 	return out, nil
 }
 
+func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error) {
+	out := new(ListRefreshResp)
+	err := grpc.Invoke(ctx, "/api.Dex/ListRefresh", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // Server API for Dex service
 
 type DexServer interface {
@@ -387,6 +446,8 @@ type DexServer interface {
 	ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, 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.
+	ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error)
 }
 
 func RegisterDexServer(s *grpc.Server, srv DexServer) {
@@ -519,6 +580,24 @@ func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(inte
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListRefreshReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(DexServer).ListRefresh(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/api.Dex/ListRefresh",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(DexServer).ListRefresh(ctx, req.(*ListRefreshReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 var _Dex_serviceDesc = grpc.ServiceDesc{
 	ServiceName: "api.Dex",
 	HandlerType: (*DexServer)(nil),
@@ -551,6 +630,10 @@ var _Dex_serviceDesc = grpc.ServiceDesc{
 			MethodName: "GetVersion",
 			Handler:    _Dex_GetVersion_Handler,
 		},
+		{
+			MethodName: "ListRefresh",
+			Handler:    _Dex_ListRefresh_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: fileDescriptor0,
@@ -559,45 +642,52 @@ var _Dex_serviceDesc = grpc.ServiceDesc{
 func init() { proto.RegisterFile("api/api.proto", fileDescriptor0) }
 
 var fileDescriptor0 = []byte{
-	// 625 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x55, 0x4f, 0x4f, 0xdb, 0x4e,
-	0x10, 0x4d, 0x62, 0x48, 0x9c, 0x49, 0x42, 0x92, 0x11, 0x3f, 0x30, 0xf9, 0x5d, 0x60, 0x51, 0x25,
-	0x50, 0x25, 0x10, 0x54, 0x6a, 0x0f, 0x55, 0xe9, 0x01, 0xfa, 0x4f, 0xea, 0x01, 0x59, 0x4a, 0x8f,
-	0xb5, 0x4c, 0x3c, 0x85, 0x95, 0x8c, 0xbd, 0xdd, 0xb5, 0x1b, 0xfa, 0xf1, 0x7a, 0xe8, 0xf7, 0xaa,
-	0x76, 0xbd, 0x09, 0xb6, 0x43, 0x95, 0xde, 0xfc, 0xde, 0xce, 0xbc, 0xd9, 0x79, 0x33, 0x9b, 0xc0,
-	0x20, 0x14, 0xfc, 0x34, 0x14, 0xfc, 0x44, 0xc8, 0x34, 0x4b, 0xd1, 0x09, 0x05, 0x67, 0xbf, 0x9a,
-	0xd0, 0xbe, 0x8c, 0x39, 0x25, 0x19, 0x6e, 0x41, 0x8b, 0x47, 0x5e, 0x73, 0xbf, 0x79, 0xd4, 0xf5,
-	0x5b, 0x3c, 0xc2, 0x1d, 0x68, 0x2b, 0x9a, 0x49, 0xca, 0xbc, 0x96, 0xe1, 0x2c, 0xc2, 0x43, 0x18,
-	0x48, 0x8a, 0xb8, 0xa4, 0x59, 0x16, 0xe4, 0x92, 0x2b, 0xcf, 0xd9, 0x77, 0x8e, 0xba, 0x7e, 0x7f,
-	0x41, 0x4e, 0x25, 0x57, 0x3a, 0x28, 0x93, 0xb9, 0xca, 0x28, 0x0a, 0x04, 0x91, 0x54, 0xde, 0x46,
-	0x11, 0x64, 0xc9, 0x6b, 0xcd, 0xe9, 0x0a, 0x22, 0xbf, 0x89, 0xf9, 0xcc, 0xdb, 0xdc, 0x6f, 0x1e,
-	0xb9, 0xbe, 0x45, 0x88, 0xb0, 0x91, 0x84, 0xf7, 0xe4, 0xb5, 0x4d, 0x5d, 0xf3, 0x8d, 0x7b, 0xe0,
-	0xc6, 0xe9, 0x6d, 0x1a, 0xe4, 0x32, 0xf6, 0x3a, 0x86, 0xef, 0x68, 0x3c, 0x95, 0x31, 0x7b, 0x09,
-	0xc3, 0x4b, 0x49, 0x61, 0x46, 0x45, 0x23, 0x3e, 0x7d, 0xc7, 0x43, 0x68, 0xcf, 0x0c, 0x30, 0xfd,
-	0xf4, 0xce, 0x7b, 0x27, 0xba, 0x6f, 0x7b, 0x6e, 0x8f, 0xd8, 0x57, 0x18, 0x55, 0xf3, 0x94, 0xc0,
-	0x67, 0xb0, 0x15, 0xc6, 0x92, 0xc2, 0xe8, 0x67, 0x40, 0x0f, 0x5c, 0x65, 0xca, 0x08, 0xb8, 0xfe,
-	0xc0, 0xb2, 0xef, 0x0c, 0x59, 0xd2, 0x6f, 0xfd, 0x5d, 0xff, 0x00, 0x86, 0x57, 0x14, 0x53, 0xf9,
-	0x5e, 0x35, 0x8f, 0xd9, 0x29, 0x8c, 0xaa, 0x21, 0x4a, 0xe0, 0xff, 0xd0, 0x4d, 0xd2, 0x2c, 0xf8,
-	0x96, 0xe6, 0x49, 0x64, 0xab, 0xbb, 0x49, 0x9a, 0xbd, 0xd7, 0x98, 0x71, 0x70, 0xaf, 0x43, 0xa5,
-	0xe6, 0xa9, 0x8c, 0x70, 0x1b, 0x36, 0xe9, 0x3e, 0xe4, 0xb1, 0xd5, 0x2b, 0x80, 0x36, 0xef, 0x2e,
-	0x54, 0x77, 0xe6, 0x62, 0x7d, 0xdf, 0x7c, 0xe3, 0x04, 0xdc, 0x5c, 0x91, 0x34, 0xa6, 0x3a, 0x26,
-	0x78, 0x89, 0x71, 0x17, 0x3a, 0xfa, 0x3b, 0xe0, 0x91, 0xb7, 0x51, 0xcc, 0x59, 0xc3, 0x4f, 0x11,
-	0xbb, 0x80, 0x71, 0x61, 0xcf, 0xa2, 0xa0, 0x6e, 0xe0, 0x18, 0x5c, 0x61, 0xa1, 0xb5, 0x76, 0x60,
-	0x5a, 0x5f, 0xc6, 0x2c, 0x8f, 0xd9, 0x6b, 0xc0, 0x7a, 0xfe, 0x3f, 0x1b, 0xcc, 0x6e, 0x61, 0x3c,
-	0x15, 0x51, 0xad, 0xf8, 0xd3, 0x0d, 0xef, 0x81, 0x9b, 0xd0, 0x3c, 0x28, 0x35, 0xdd, 0x49, 0x68,
-	0xfe, 0x51, 0xf7, 0x7d, 0x00, 0x7d, 0x7d, 0x54, 0xeb, 0xbd, 0x97, 0xd0, 0x7c, 0x6a, 0x29, 0x76,
-	0x06, 0x58, 0x2f, 0xb4, 0x6e, 0x06, 0xc7, 0x30, 0x2e, 0x86, 0xb6, 0xf6, 0x6e, 0x5a, 0xbd, 0x1e,
-	0xba, 0x4e, 0x7d, 0x0c, 0xc3, 0xcf, 0x5c, 0x65, 0x25, 0x6d, 0xf6, 0x16, 0x46, 0x55, 0x4a, 0x09,
-	0x7c, 0x0e, 0xdd, 0x85, 0xd3, 0xda, 0x42, 0x67, 0x75, 0x12, 0x8f, 0xe7, 0xac, 0x0f, 0xf0, 0x85,
-	0xa4, 0xe2, 0x69, 0xa2, 0xe5, 0x5e, 0x41, 0x6f, 0x89, 0x94, 0x28, 0xde, 0xb9, 0xfc, 0x41, 0xd2,
-	0x5e, 0xdd, 0x22, 0x1c, 0x81, 0xfe, 0x85, 0x30, 0x96, 0x6e, 0xfa, 0xfa, 0xf3, 0xfc, 0xb7, 0x03,
-	0xce, 0x15, 0x3d, 0xe0, 0x1b, 0xe8, 0x97, 0x1f, 0x0e, 0x6e, 0x17, 0xdb, 0x5f, 0x7d, 0x83, 0x93,
-	0xff, 0x9e, 0x60, 0x95, 0x60, 0x0d, 0x9d, 0x5e, 0x5e, 0x7a, 0x9b, 0x5e, 0x7b, 0x2a, 0x36, 0xbd,
-	0xfe, 0x3a, 0x58, 0x03, 0x2f, 0x61, 0xab, 0xba, 0x57, 0xb8, 0x53, 0xaa, 0x54, 0xf2, 0x6d, 0xb2,
-	0xfb, 0x24, 0xbf, 0x10, 0xa9, 0x8e, 0xdd, 0x8a, 0xac, 0x2c, 0x9d, 0x15, 0x59, 0xdd, 0x91, 0x42,
-	0xa4, 0x3a, 0x5d, 0x2b, 0xb2, 0xb2, 0x1d, 0x56, 0x64, 0x75, 0x15, 0x58, 0x03, 0x2f, 0x60, 0x50,
-	0x1e, 0xae, 0xb2, 0x76, 0xd4, 0x76, 0xc0, 0xda, 0x51, 0x5f, 0x03, 0xd6, 0xc0, 0x33, 0x80, 0x0f,
-	0x94, 0xd9, 0x81, 0xe2, 0xd0, 0x84, 0x3d, 0x0e, 0x7b, 0x32, 0xaa, 0x12, 0x3a, 0xe5, 0xa6, 0x6d,
-	0xfe, 0x00, 0x5e, 0xfc, 0x09, 0x00, 0x00, 0xff, 0xff, 0x34, 0x14, 0x6b, 0x5d, 0x11, 0x06, 0x00,
-	0x00,
+	// 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,
 }
diff --git a/api/api.proto b/api/api.proto
index 23842fc875ea8a5e8ae4fc9078c953c347d960ea..82381d1ae63957a78ef9435316410ca3a4a8210c 100644
--- a/api/api.proto
+++ b/api/api.proto
@@ -83,7 +83,7 @@ message DeletePasswordResp {
 // ListPasswordReq is a request to enumerate passwords.
 message ListPasswordReq {}
 
-// ListPasswordResp returs a list of passwords.
+// ListPasswordResp returns a list of passwords.
 message ListPasswordResp {
   repeated Password passwords = 1;
 }
@@ -100,6 +100,26 @@ message VersionResp {
   int32 api = 2;
 }
 
+// RefreshTokenRef contains the metadata for a refresh token that is managed by the storage.
+message RefreshTokenRef {
+  // ID of the refresh token.
+  string id = 1;
+  string client_id = 2;
+  string created_at = 3;
+  string last_used = 4;
+}
+
+// ListRefreshReq is a request to enumerate the refresh tokens of a user.
+message ListRefreshReq {
+  // The "sub" claim returned in the ID Token.
+  string user_id = 1;
+}
+
+// ListRefreshResp returns a list of refresh tokens for a user.
+message ListRefreshResp {
+  repeated RefreshTokenRef refresh_tokens = 1;
+}
+
 // Dex represents the dex gRPC service.
 service Dex {
   // CreateClient creates a client.
@@ -116,4 +136,6 @@ service Dex {
   rpc ListPasswords(ListPasswordReq) returns (ListPasswordResp) {};
   // GetVersion returns version information of the server.
   rpc GetVersion(VersionReq) returns (VersionResp) {};
+  // ListRefresh lists all the refresh token entries for a particular user.
+  rpc ListRefresh(ListRefreshReq) returns (ListRefreshResp) {};
 }
diff --git a/server/api.go b/server/api.go
index 61a0e5759cf2c23cac32b7bb8dab961960267765..8dc2ea573aeada4aa1525c32a30c01dbdf6dbd97 100644
--- a/server/api.go
+++ b/server/api.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/coreos/dex/api"
+	"github.com/coreos/dex/server/internal"
 	"github.com/coreos/dex/storage"
 	"github.com/coreos/dex/version"
 )
@@ -198,3 +199,32 @@ func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*a
 	}, nil
 
 }
+
+func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, 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, fmt.Errorf("unmarshal ID Token subject: %v", 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)
+	}
+
+	var refreshTokenRefs []*api.RefreshTokenRef
+	for _, session := range offlineSessions.Refresh {
+		r := api.RefreshTokenRef{
+			Id:        session.ID,
+			ClientId:  session.ClientID,
+			CreatedAt: session.CreatedAt.String(),
+			LastUsed:  session.LastUsed.String(),
+		}
+		refreshTokenRefs = append(refreshTokenRefs, &r)
+	}
+
+	return &api.ListRefreshResp{
+		RefreshTokens: refreshTokenRefs,
+	}, nil
+}
diff --git a/server/handlers.go b/server/handlers.go
index a994b9892d39dc717c8a434c356468f4fa04179d..be1d148699c935bad0200e8697cb9fa5d3d1c283 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -510,7 +510,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 		case responseTypeIDToken:
 			implicitOrHybrid = true
 			var err error
-			idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken)
+			idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, authReq.ConnectorID)
 			if err != nil {
 				s.logger.Errorf("failed to create ID token: %v", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -632,7 +632,7 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 	}
 
 	accessToken := storage.NewID()
-	idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken)
+	idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ConnectorID)
 	if err != nil {
 		s.logger.Errorf("failed to create ID token: %v", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -866,7 +866,7 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
 	}
 
 	accessToken := storage.NewID()
-	idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, refresh.Nonce, accessToken)
+	idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, refresh.Nonce, accessToken, refresh.ConnectorID)
 	if err != nil {
 		s.logger.Errorf("failed to create ID token: %v", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
diff --git a/server/internal/types.pb.go b/server/internal/types.pb.go
index 791944f52603607380b7340abe2ddf4cc7e30f1e..0c3f3830955e8f0d9103d78c8bf2e11adf8e1e9e 100644
--- a/server/internal/types.pb.go
+++ b/server/internal/types.pb.go
@@ -12,6 +12,7 @@ It is generated from these files:
 
 It has these top-level messages:
 	RefreshToken
+	IDTokenSubject
 */
 package internal
 
@@ -41,19 +42,35 @@ func (m *RefreshToken) String() string            { return proto.CompactTextStri
 func (*RefreshToken) ProtoMessage()               {}
 func (*RefreshToken) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
 
+// IDTokenSubject represents both the userID and connID which is returned
+// as the "sub" claim in the ID Token.
+type IDTokenSubject struct {
+	UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId" json:"user_id,omitempty"`
+	ConnId string `protobuf:"bytes,2,opt,name=conn_id,json=connId" json:"conn_id,omitempty"`
+}
+
+func (m *IDTokenSubject) Reset()                    { *m = IDTokenSubject{} }
+func (m *IDTokenSubject) String() string            { return proto.CompactTextString(m) }
+func (*IDTokenSubject) ProtoMessage()               {}
+func (*IDTokenSubject) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
+
 func init() {
 	proto.RegisterType((*RefreshToken)(nil), "internal.RefreshToken")
+	proto.RegisterType((*IDTokenSubject)(nil), "internal.IDTokenSubject")
 }
 
 func init() { proto.RegisterFile("server/internal/types.proto", fileDescriptor0) }
 
 var fileDescriptor0 = []byte{
-	// 112 bytes of a gzipped FileDescriptorProto
+	// 157 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0x2e, 0x4e, 0x2d, 0x2a,
 	0x4b, 0x2d, 0xd2, 0xcf, 0xcc, 0x2b, 0x49, 0x2d, 0xca, 0x4b, 0xcc, 0xd1, 0x2f, 0xa9, 0x2c, 0x48,
 	0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0x89, 0x2a, 0x39, 0x73, 0xf1, 0x04,
 	0xa5, 0xa6, 0x15, 0xa5, 0x16, 0x67, 0x84, 0xe4, 0x67, 0xa7, 0xe6, 0x09, 0xc9, 0x72, 0x71, 0x15,
 	0x41, 0xf8, 0xf1, 0x99, 0x29, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x9c, 0x50, 0x11, 0xcf,
-	0x14, 0x21, 0x11, 0x2e, 0xd6, 0x12, 0x90, 0x3a, 0x09, 0x26, 0xb0, 0x0c, 0x84, 0x93, 0xc4, 0x06,
-	0x36, 0xd5, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x9b, 0xd0, 0x5a, 0x1d, 0x74, 0x00, 0x00, 0x00,
+	0x14, 0x21, 0x11, 0x2e, 0xd6, 0x12, 0x90, 0x3a, 0x09, 0x26, 0xb0, 0x0c, 0x84, 0xa3, 0xe4, 0xc4,
+	0xc5, 0xe7, 0xe9, 0x02, 0xd6, 0x1f, 0x5c, 0x9a, 0x94, 0x95, 0x9a, 0x5c, 0x22, 0x24, 0xce, 0xc5,
+	0x5e, 0x5a, 0x9c, 0x5a, 0x84, 0x30, 0x83, 0x0d, 0xc4, 0xf5, 0x4c, 0x01, 0x49, 0x24, 0xe7, 0xe7,
+	0xe5, 0x81, 0x24, 0x20, 0x46, 0xb0, 0x81, 0xb8, 0x9e, 0x29, 0x49, 0x6c, 0x60, 0x97, 0x19, 0x03,
+	0x02, 0x00, 0x00, 0xff, 0xff, 0x13, 0xfe, 0x01, 0x37, 0xb8, 0x00, 0x00, 0x00,
 }
diff --git a/server/internal/types.proto b/server/internal/types.proto
index 442dbd95c05b12d27d5c946ffa2119dd080ed99c..87e18f4c232e24cf6f3bd78007009d110131bc8f 100644
--- a/server/internal/types.proto
+++ b/server/internal/types.proto
@@ -8,3 +8,10 @@ message RefreshToken {
   string refresh_id = 1;
   string token = 2;
 }
+
+// IDTokenSubject represents both the userID and connID which is returned
+// as the "sub" claim in the ID Token.
+message IDTokenSubject {
+  string user_id = 1;
+  string conn_id = 2;
+}
diff --git a/server/oauth2.go b/server/oauth2.go
index 15b85ed2839b33719f3bfdb2f6822f28293c09b2..b10609c93ba6b66e9295c1393732b28ec871ef9b 100644
--- a/server/oauth2.go
+++ b/server/oauth2.go
@@ -21,6 +21,7 @@ import (
 	jose "gopkg.in/square/go-jose.v2"
 
 	"github.com/coreos/dex/connector"
+	"github.com/coreos/dex/server/internal"
 	"github.com/coreos/dex/storage"
 )
 
@@ -246,7 +247,7 @@ type idTokenClaims struct {
 	Name string `json:"name,omitempty"`
 }
 
-func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken string) (idToken string, expiry time.Time, err error) {
+func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, connID string) (idToken string, expiry time.Time, err error) {
 	keys, err := s.storage.GetKeys()
 	if err != nil {
 		s.logger.Errorf("Failed to get keys: %v", err)
@@ -265,9 +266,20 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 	issuedAt := s.now()
 	expiry = issuedAt.Add(s.idTokensValidFor)
 
+	sub := &internal.IDTokenSubject{
+		UserId: claims.UserID,
+		ConnId: connID,
+	}
+
+	subjectString, err := internal.Marshal(sub)
+	if err != nil {
+		s.logger.Errorf("failed to marshal offline session ID: %v", err)
+		return "", expiry, fmt.Errorf("failed to marshal offline session ID: %v", err)
+	}
+
 	tok := idTokenClaims{
 		Issuer:   s.issuerURL.String(),
-		Subject:  claims.UserID,
+		Subject:  subjectString,
 		Nonce:    nonce,
 		Expiry:   expiry.Unix(),
 		IssuedAt: issuedAt.Unix(),