From 26a7e6afca2e9ff1c3450d6b49cfaef8d62aaaf2 Mon Sep 17 00:00:00 2001
From: Nick Zelei <2420177+nickzelei@users.noreply.github.com>
Date: Mon, 18 Dec 2023 12:01:25 -0800
Subject: [PATCH] NEOS-470 Enable auth token refreshing in cli (#894)
---
.../gen/go/protos/mgmt/v1alpha1/auth.pb.go | 263 ++++++++++++++----
.../protos/mgmt/v1alpha1/auth.pb.validate.go | 235 ++++++++++++++++
.../mgmtv1alpha1connect/auth.connect.go | 30 ++
backend/internal/auth/client/client.go | 60 ++++
.../internal/cmds/mgmt/serve/connect/cmd.go | 1 +
backend/protos/mgmt/v1alpha1/auth.proto | 50 ++--
.../mgmt/v1alpha1/auth-service/service.go | 5 +
.../mgmt/v1alpha1/auth-service/tokens.go | 54 ++--
cli/internal/auth/tokens.go | 23 +-
cli/internal/cmds/neosync/login/login.go | 2 +-
docs/protos/data/proto_docs.json | 72 ++++-
docs/protos/mgmt/v1alpha1/auth.proto.mdx | 20 +-
.../src/client/mgmt/v1alpha1/auth_connect.ts | 14 +-
.../sdk/src/client/mgmt/v1alpha1/auth_pb.ts | 91 ++++++
14 files changed, 799 insertions(+), 121 deletions(-)
diff --git a/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.go b/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.go
index 038981f2e..795d90803 100644
--- a/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.go
+++ b/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.go
@@ -219,12 +219,19 @@ type AccessToken struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
+ // The access token that will be provided in subsequent requests to provide authenticated access to the Api
+ AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
+ // Token that can be used to retrieve a refreshed access token.
+ // Will not be provided if the offline_access scope is not provided in the initial login flow.
RefreshToken *string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3,oneof" json:"refresh_token,omitempty"`
- ExpiresIn int64 `protobuf:"varint,3,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"`
- Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"`
- IdToken *string `protobuf:"bytes,5,opt,name=id_token,json=idToken,proto3,oneof" json:"id_token,omitempty"`
- TokenType string `protobuf:"bytes,6,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"`
+ // Relative time in seconds that the access token will expire. Combine with the current time to get the expires_at time.
+ ExpiresIn int64 `protobuf:"varint,3,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"`
+ // The scopes that the access token have
+ Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"`
+ // The identity token of the authenticated user
+ IdToken *string `protobuf:"bytes,5,opt,name=id_token,json=idToken,proto3,oneof" json:"id_token,omitempty"`
+ // The token type. For JWTs, this will be `Bearer`
+ TokenType string `protobuf:"bytes,6,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"`
}
func (x *AccessToken) Reset() {
@@ -510,6 +517,102 @@ func (x *GetCliIssuerResponse) GetAudience() string {
return ""
}
+type RefreshCliRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The token used to retrieve a new access token.
+ RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
+}
+
+func (x *RefreshCliRequest) Reset() {
+ *x = RefreshCliRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_mgmt_v1alpha1_auth_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RefreshCliRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RefreshCliRequest) ProtoMessage() {}
+
+func (x *RefreshCliRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_mgmt_v1alpha1_auth_proto_msgTypes[9]
+ 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 RefreshCliRequest.ProtoReflect.Descriptor instead.
+func (*RefreshCliRequest) Descriptor() ([]byte, []int) {
+ return file_mgmt_v1alpha1_auth_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *RefreshCliRequest) GetRefreshToken() string {
+ if x != nil {
+ return x.RefreshToken
+ }
+ return ""
+}
+
+type RefreshCliResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // The access token that is returned on successful refresh
+ AccessToken *AccessToken `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
+}
+
+func (x *RefreshCliResponse) Reset() {
+ *x = RefreshCliResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_mgmt_v1alpha1_auth_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RefreshCliResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RefreshCliResponse) ProtoMessage() {}
+
+func (x *RefreshCliResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_mgmt_v1alpha1_auth_proto_msgTypes[10]
+ 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 RefreshCliResponse.ProtoReflect.Descriptor instead.
+func (*RefreshCliResponse) Descriptor() ([]byte, []int) {
+ return file_mgmt_v1alpha1_auth_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *RefreshCliResponse) GetAccessToken() *AccessToken {
+ if x != nil {
+ return x.AccessToken
+ }
+ return nil
+}
+
var File_mgmt_v1alpha1_auth_proto protoreflect.FileDescriptor
var file_mgmt_v1alpha1_auth_proto_rawDesc = []byte{
@@ -566,43 +669,58 @@ var file_mgmt_v1alpha1_auth_proto_rawDesc = []byte{
0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x73, 0x73, 0x75,
0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63,
- 0x65, 0x32, 0xf9, 0x02, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
- 0x65, 0x12, 0x4d, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x43, 0x6c, 0x69, 0x12, 0x1e, 0x2e,
- 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x6f,
- 0x67, 0x69, 0x6e, 0x43, 0x6c, 0x69, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e,
- 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x6f,
- 0x67, 0x69, 0x6e, 0x43, 0x6c, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
- 0x12, 0x59, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72,
- 0x12, 0x22, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
- 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c,
- 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x49, 0x73, 0x73, 0x75, 0x65,
- 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0f, 0x47,
- 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x25,
- 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47,
- 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x72, 0x6c, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61,
- 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
- 0x7a, 0x65, 0x55, 0x72, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
- 0x5c, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
- 0x12, 0x23, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
- 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61,
- 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61,
- 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xc5, 0x01,
- 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70,
- 0x68, 0x61, 0x31, 0x42, 0x09, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
- 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6e, 0x75, 0x63,
- 0x6c, 0x65, 0x75, 0x73, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6e, 0x65, 0x6f, 0x73, 0x79, 0x6e,
- 0x63, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f,
- 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6d, 0x67, 0x6d, 0x74, 0x2f, 0x76, 0x31, 0x61,
- 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x6d, 0x67, 0x6d, 0x74, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
- 0x61, 0x31, 0xa2, 0x02, 0x03, 0x4d, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x4d, 0x67, 0x6d, 0x74, 0x2e,
- 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x0d, 0x4d, 0x67, 0x6d, 0x74, 0x5c,
- 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, 0x19, 0x4d, 0x67, 0x6d, 0x74, 0x5c,
- 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61,
- 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x67, 0x6d, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x61,
- 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x65, 0x22, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x43, 0x6c, 0x69, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73,
+ 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba,
+ 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54,
+ 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x53, 0x0a, 0x12, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x43,
+ 0x6c, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0c, 0x61, 0x63,
+ 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
+ 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x0b, 0x61, 0x63,
+ 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xce, 0x03, 0x0a, 0x0b, 0x41, 0x75,
+ 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4d, 0x0a, 0x08, 0x4c, 0x6f, 0x67,
+ 0x69, 0x6e, 0x43, 0x6c, 0x69, 0x12, 0x1e, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61,
+ 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x43, 0x6c, 0x69, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61,
+ 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x43, 0x6c, 0x69, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x0a, 0x52, 0x65, 0x66, 0x72,
+ 0x65, 0x73, 0x68, 0x43, 0x6c, 0x69, 0x12, 0x20, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31,
+ 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x43, 0x6c,
+ 0x69, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e,
+ 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68,
+ 0x43, 0x6c, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a,
+ 0x0c, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x22, 0x2e,
+ 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65,
+ 0x74, 0x43, 0x6c, 0x69, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
+ 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41,
+ 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x25, 0x2e, 0x6d, 0x67,
+ 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41,
+ 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x72, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
+ 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55,
+ 0x72, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5c, 0x0a, 0x0d,
+ 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x2e,
+ 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65,
+ 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
+ 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xc5, 0x01, 0x0a, 0x11, 0x63,
+ 0x6f, 0x6d, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
+ 0x42, 0x09, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x50, 0x67,
+ 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6e, 0x75, 0x63, 0x6c, 0x65, 0x75,
+ 0x73, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6e, 0x65, 0x6f, 0x73, 0x79, 0x6e, 0x63, 0x2f, 0x62,
+ 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6d, 0x67, 0x6d, 0x74, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
+ 0x61, 0x31, 0x3b, 0x6d, 0x67, 0x6d, 0x74, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2,
+ 0x02, 0x03, 0x4d, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x4d, 0x67, 0x6d, 0x74, 0x2e, 0x56, 0x31, 0x61,
+ 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x0d, 0x4d, 0x67, 0x6d, 0x74, 0x5c, 0x56, 0x31, 0x61,
+ 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, 0x19, 0x4d, 0x67, 0x6d, 0x74, 0x5c, 0x56, 0x31, 0x61,
+ 0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+ 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x67, 0x6d, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68,
+ 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -617,7 +735,7 @@ func file_mgmt_v1alpha1_auth_proto_rawDescGZIP() []byte {
return file_mgmt_v1alpha1_auth_proto_rawDescData
}
-var file_mgmt_v1alpha1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
+var file_mgmt_v1alpha1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_mgmt_v1alpha1_auth_proto_goTypes = []interface{}{
(*LoginCliRequest)(nil), // 0: mgmt.v1alpha1.LoginCliRequest
(*LoginCliResponse)(nil), // 1: mgmt.v1alpha1.LoginCliResponse
@@ -628,22 +746,27 @@ var file_mgmt_v1alpha1_auth_proto_goTypes = []interface{}{
(*GetAuthorizeUrlResponse)(nil), // 6: mgmt.v1alpha1.GetAuthorizeUrlResponse
(*GetCliIssuerRequest)(nil), // 7: mgmt.v1alpha1.GetCliIssuerRequest
(*GetCliIssuerResponse)(nil), // 8: mgmt.v1alpha1.GetCliIssuerResponse
+ (*RefreshCliRequest)(nil), // 9: mgmt.v1alpha1.RefreshCliRequest
+ (*RefreshCliResponse)(nil), // 10: mgmt.v1alpha1.RefreshCliResponse
}
var file_mgmt_v1alpha1_auth_proto_depIdxs = []int32{
- 4, // 0: mgmt.v1alpha1.LoginCliResponse.access_token:type_name -> mgmt.v1alpha1.AccessToken
- 0, // 1: mgmt.v1alpha1.AuthService.LoginCli:input_type -> mgmt.v1alpha1.LoginCliRequest
- 7, // 2: mgmt.v1alpha1.AuthService.GetCliIssuer:input_type -> mgmt.v1alpha1.GetCliIssuerRequest
- 5, // 3: mgmt.v1alpha1.AuthService.GetAuthorizeUrl:input_type -> mgmt.v1alpha1.GetAuthorizeUrlRequest
- 2, // 4: mgmt.v1alpha1.AuthService.GetAuthStatus:input_type -> mgmt.v1alpha1.GetAuthStatusRequest
- 1, // 5: mgmt.v1alpha1.AuthService.LoginCli:output_type -> mgmt.v1alpha1.LoginCliResponse
- 8, // 6: mgmt.v1alpha1.AuthService.GetCliIssuer:output_type -> mgmt.v1alpha1.GetCliIssuerResponse
- 6, // 7: mgmt.v1alpha1.AuthService.GetAuthorizeUrl:output_type -> mgmt.v1alpha1.GetAuthorizeUrlResponse
- 3, // 8: mgmt.v1alpha1.AuthService.GetAuthStatus:output_type -> mgmt.v1alpha1.GetAuthStatusResponse
- 5, // [5:9] is the sub-list for method output_type
- 1, // [1:5] is the sub-list for method input_type
- 1, // [1:1] is the sub-list for extension type_name
- 1, // [1:1] is the sub-list for extension extendee
- 0, // [0:1] is the sub-list for field type_name
+ 4, // 0: mgmt.v1alpha1.LoginCliResponse.access_token:type_name -> mgmt.v1alpha1.AccessToken
+ 4, // 1: mgmt.v1alpha1.RefreshCliResponse.access_token:type_name -> mgmt.v1alpha1.AccessToken
+ 0, // 2: mgmt.v1alpha1.AuthService.LoginCli:input_type -> mgmt.v1alpha1.LoginCliRequest
+ 9, // 3: mgmt.v1alpha1.AuthService.RefreshCli:input_type -> mgmt.v1alpha1.RefreshCliRequest
+ 7, // 4: mgmt.v1alpha1.AuthService.GetCliIssuer:input_type -> mgmt.v1alpha1.GetCliIssuerRequest
+ 5, // 5: mgmt.v1alpha1.AuthService.GetAuthorizeUrl:input_type -> mgmt.v1alpha1.GetAuthorizeUrlRequest
+ 2, // 6: mgmt.v1alpha1.AuthService.GetAuthStatus:input_type -> mgmt.v1alpha1.GetAuthStatusRequest
+ 1, // 7: mgmt.v1alpha1.AuthService.LoginCli:output_type -> mgmt.v1alpha1.LoginCliResponse
+ 10, // 8: mgmt.v1alpha1.AuthService.RefreshCli:output_type -> mgmt.v1alpha1.RefreshCliResponse
+ 8, // 9: mgmt.v1alpha1.AuthService.GetCliIssuer:output_type -> mgmt.v1alpha1.GetCliIssuerResponse
+ 6, // 10: mgmt.v1alpha1.AuthService.GetAuthorizeUrl:output_type -> mgmt.v1alpha1.GetAuthorizeUrlResponse
+ 3, // 11: mgmt.v1alpha1.AuthService.GetAuthStatus:output_type -> mgmt.v1alpha1.GetAuthStatusResponse
+ 7, // [7:12] is the sub-list for method output_type
+ 2, // [2:7] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 2, // [2:2] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
}
func init() { file_mgmt_v1alpha1_auth_proto_init() }
@@ -760,6 +883,30 @@ func file_mgmt_v1alpha1_auth_proto_init() {
return nil
}
}
+ file_mgmt_v1alpha1_auth_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RefreshCliRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_mgmt_v1alpha1_auth_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RefreshCliResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
}
file_mgmt_v1alpha1_auth_proto_msgTypes[4].OneofWrappers = []interface{}{}
type x struct{}
@@ -768,7 +915,7 @@ func file_mgmt_v1alpha1_auth_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_mgmt_v1alpha1_auth_proto_rawDesc,
NumEnums: 0,
- NumMessages: 9,
+ NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.validate.go b/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.validate.go
index 66224a90c..d7d1d090d 100644
--- a/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.validate.go
+++ b/backend/gen/go/protos/mgmt/v1alpha1/auth.pb.validate.go
@@ -1008,3 +1008,238 @@ var _ interface {
Cause() error
ErrorName() string
} = GetCliIssuerResponseValidationError{}
+
+// Validate checks the field values on RefreshCliRequest with the rules defined
+// in the proto definition for this message. If any rules are violated, the
+// first error encountered is returned, or nil if there are no violations.
+func (m *RefreshCliRequest) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on RefreshCliRequest with the rules
+// defined in the proto definition for this message. If any rules are
+// violated, the result is a list of violation errors wrapped in
+// RefreshCliRequestMultiError, or nil if none found.
+func (m *RefreshCliRequest) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *RefreshCliRequest) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ // no validation rules for RefreshToken
+
+ if len(errors) > 0 {
+ return RefreshCliRequestMultiError(errors)
+ }
+
+ return nil
+}
+
+// RefreshCliRequestMultiError is an error wrapping multiple validation errors
+// returned by RefreshCliRequest.ValidateAll() if the designated constraints
+// aren't met.
+type RefreshCliRequestMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m RefreshCliRequestMultiError) Error() string {
+ var msgs []string
+ for _, err := range m {
+ msgs = append(msgs, err.Error())
+ }
+ return strings.Join(msgs, "; ")
+}
+
+// AllErrors returns a list of validation violation errors.
+func (m RefreshCliRequestMultiError) AllErrors() []error { return m }
+
+// RefreshCliRequestValidationError is the validation error returned by
+// RefreshCliRequest.Validate if the designated constraints aren't met.
+type RefreshCliRequestValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e RefreshCliRequestValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e RefreshCliRequestValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e RefreshCliRequestValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e RefreshCliRequestValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e RefreshCliRequestValidationError) ErrorName() string {
+ return "RefreshCliRequestValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e RefreshCliRequestValidationError) Error() string {
+ cause := ""
+ if e.cause != nil {
+ cause = fmt.Sprintf(" | caused by: %v", e.cause)
+ }
+
+ key := ""
+ if e.key {
+ key = "key for "
+ }
+
+ return fmt.Sprintf(
+ "invalid %sRefreshCliRequest.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = RefreshCliRequestValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = RefreshCliRequestValidationError{}
+
+// Validate checks the field values on RefreshCliResponse with the rules
+// defined in the proto definition for this message. If any rules are
+// violated, the first error encountered is returned, or nil if there are no violations.
+func (m *RefreshCliResponse) Validate() error {
+ return m.validate(false)
+}
+
+// ValidateAll checks the field values on RefreshCliResponse with the rules
+// defined in the proto definition for this message. If any rules are
+// violated, the result is a list of violation errors wrapped in
+// RefreshCliResponseMultiError, or nil if none found.
+func (m *RefreshCliResponse) ValidateAll() error {
+ return m.validate(true)
+}
+
+func (m *RefreshCliResponse) validate(all bool) error {
+ if m == nil {
+ return nil
+ }
+
+ var errors []error
+
+ if all {
+ switch v := interface{}(m.GetAccessToken()).(type) {
+ case interface{ ValidateAll() error }:
+ if err := v.ValidateAll(); err != nil {
+ errors = append(errors, RefreshCliResponseValidationError{
+ field: "AccessToken",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ case interface{ Validate() error }:
+ if err := v.Validate(); err != nil {
+ errors = append(errors, RefreshCliResponseValidationError{
+ field: "AccessToken",
+ reason: "embedded message failed validation",
+ cause: err,
+ })
+ }
+ }
+ } else if v, ok := interface{}(m.GetAccessToken()).(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return RefreshCliResponseValidationError{
+ field: "AccessToken",
+ reason: "embedded message failed validation",
+ cause: err,
+ }
+ }
+ }
+
+ if len(errors) > 0 {
+ return RefreshCliResponseMultiError(errors)
+ }
+
+ return nil
+}
+
+// RefreshCliResponseMultiError is an error wrapping multiple validation errors
+// returned by RefreshCliResponse.ValidateAll() if the designated constraints
+// aren't met.
+type RefreshCliResponseMultiError []error
+
+// Error returns a concatenation of all the error messages it wraps.
+func (m RefreshCliResponseMultiError) Error() string {
+ var msgs []string
+ for _, err := range m {
+ msgs = append(msgs, err.Error())
+ }
+ return strings.Join(msgs, "; ")
+}
+
+// AllErrors returns a list of validation violation errors.
+func (m RefreshCliResponseMultiError) AllErrors() []error { return m }
+
+// RefreshCliResponseValidationError is the validation error returned by
+// RefreshCliResponse.Validate if the designated constraints aren't met.
+type RefreshCliResponseValidationError struct {
+ field string
+ reason string
+ cause error
+ key bool
+}
+
+// Field function returns field value.
+func (e RefreshCliResponseValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e RefreshCliResponseValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e RefreshCliResponseValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e RefreshCliResponseValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e RefreshCliResponseValidationError) ErrorName() string {
+ return "RefreshCliResponseValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e RefreshCliResponseValidationError) Error() string {
+ cause := ""
+ if e.cause != nil {
+ cause = fmt.Sprintf(" | caused by: %v", e.cause)
+ }
+
+ key := ""
+ if e.key {
+ key = "key for "
+ }
+
+ return fmt.Sprintf(
+ "invalid %sRefreshCliResponse.%s: %s%s",
+ key,
+ e.field,
+ e.reason,
+ cause)
+}
+
+var _ error = RefreshCliResponseValidationError{}
+
+var _ interface {
+ Field() string
+ Reason() string
+ Key() bool
+ Cause() error
+ ErrorName() string
+} = RefreshCliResponseValidationError{}
diff --git a/backend/gen/go/protos/mgmt/v1alpha1/mgmtv1alpha1connect/auth.connect.go b/backend/gen/go/protos/mgmt/v1alpha1/mgmtv1alpha1connect/auth.connect.go
index 209b2ff69..fcbffcd1b 100644
--- a/backend/gen/go/protos/mgmt/v1alpha1/mgmtv1alpha1connect/auth.connect.go
+++ b/backend/gen/go/protos/mgmt/v1alpha1/mgmtv1alpha1connect/auth.connect.go
@@ -35,6 +35,8 @@ const (
const (
// AuthServiceLoginCliProcedure is the fully-qualified name of the AuthService's LoginCli RPC.
AuthServiceLoginCliProcedure = "/mgmt.v1alpha1.AuthService/LoginCli"
+ // AuthServiceRefreshCliProcedure is the fully-qualified name of the AuthService's RefreshCli RPC.
+ AuthServiceRefreshCliProcedure = "/mgmt.v1alpha1.AuthService/RefreshCli"
// AuthServiceGetCliIssuerProcedure is the fully-qualified name of the AuthService's GetCliIssuer
// RPC.
AuthServiceGetCliIssuerProcedure = "/mgmt.v1alpha1.AuthService/GetCliIssuer"
@@ -50,6 +52,9 @@ const (
type AuthServiceClient interface {
// Used by the CLI to login to Neosync with OAuth.
LoginCli(context.Context, *connect.Request[v1alpha1.LoginCliRequest]) (*connect.Response[v1alpha1.LoginCliResponse], error)
+ // Used by the CLI to refresh an expired Neosync accesss token.
+ // This should only be used if an access token was previously retrieved from the `LoginCli` or `RefreshCli` methods.
+ RefreshCli(context.Context, *connect.Request[v1alpha1.RefreshCliRequest]) (*connect.Response[v1alpha1.RefreshCliResponse], error)
// Used by the CLI to retrieve Auth Issuer information
GetCliIssuer(context.Context, *connect.Request[v1alpha1.GetCliIssuerRequest]) (*connect.Response[v1alpha1.GetCliIssuerResponse], error)
// Used by the CLI to retrieve an Authorize URL for use with OAuth login.
@@ -74,6 +79,11 @@ func NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
baseURL+AuthServiceLoginCliProcedure,
opts...,
),
+ refreshCli: connect.NewClient[v1alpha1.RefreshCliRequest, v1alpha1.RefreshCliResponse](
+ httpClient,
+ baseURL+AuthServiceRefreshCliProcedure,
+ opts...,
+ ),
getCliIssuer: connect.NewClient[v1alpha1.GetCliIssuerRequest, v1alpha1.GetCliIssuerResponse](
httpClient,
baseURL+AuthServiceGetCliIssuerProcedure,
@@ -95,6 +105,7 @@ func NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
// authServiceClient implements AuthServiceClient.
type authServiceClient struct {
loginCli *connect.Client[v1alpha1.LoginCliRequest, v1alpha1.LoginCliResponse]
+ refreshCli *connect.Client[v1alpha1.RefreshCliRequest, v1alpha1.RefreshCliResponse]
getCliIssuer *connect.Client[v1alpha1.GetCliIssuerRequest, v1alpha1.GetCliIssuerResponse]
getAuthorizeUrl *connect.Client[v1alpha1.GetAuthorizeUrlRequest, v1alpha1.GetAuthorizeUrlResponse]
getAuthStatus *connect.Client[v1alpha1.GetAuthStatusRequest, v1alpha1.GetAuthStatusResponse]
@@ -105,6 +116,11 @@ func (c *authServiceClient) LoginCli(ctx context.Context, req *connect.Request[v
return c.loginCli.CallUnary(ctx, req)
}
+// RefreshCli calls mgmt.v1alpha1.AuthService.RefreshCli.
+func (c *authServiceClient) RefreshCli(ctx context.Context, req *connect.Request[v1alpha1.RefreshCliRequest]) (*connect.Response[v1alpha1.RefreshCliResponse], error) {
+ return c.refreshCli.CallUnary(ctx, req)
+}
+
// GetCliIssuer calls mgmt.v1alpha1.AuthService.GetCliIssuer.
func (c *authServiceClient) GetCliIssuer(ctx context.Context, req *connect.Request[v1alpha1.GetCliIssuerRequest]) (*connect.Response[v1alpha1.GetCliIssuerResponse], error) {
return c.getCliIssuer.CallUnary(ctx, req)
@@ -124,6 +140,9 @@ func (c *authServiceClient) GetAuthStatus(ctx context.Context, req *connect.Requ
type AuthServiceHandler interface {
// Used by the CLI to login to Neosync with OAuth.
LoginCli(context.Context, *connect.Request[v1alpha1.LoginCliRequest]) (*connect.Response[v1alpha1.LoginCliResponse], error)
+ // Used by the CLI to refresh an expired Neosync accesss token.
+ // This should only be used if an access token was previously retrieved from the `LoginCli` or `RefreshCli` methods.
+ RefreshCli(context.Context, *connect.Request[v1alpha1.RefreshCliRequest]) (*connect.Response[v1alpha1.RefreshCliResponse], error)
// Used by the CLI to retrieve Auth Issuer information
GetCliIssuer(context.Context, *connect.Request[v1alpha1.GetCliIssuerRequest]) (*connect.Response[v1alpha1.GetCliIssuerResponse], error)
// Used by the CLI to retrieve an Authorize URL for use with OAuth login.
@@ -144,6 +163,11 @@ func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption
svc.LoginCli,
opts...,
)
+ authServiceRefreshCliHandler := connect.NewUnaryHandler(
+ AuthServiceRefreshCliProcedure,
+ svc.RefreshCli,
+ opts...,
+ )
authServiceGetCliIssuerHandler := connect.NewUnaryHandler(
AuthServiceGetCliIssuerProcedure,
svc.GetCliIssuer,
@@ -163,6 +187,8 @@ func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption
switch r.URL.Path {
case AuthServiceLoginCliProcedure:
authServiceLoginCliHandler.ServeHTTP(w, r)
+ case AuthServiceRefreshCliProcedure:
+ authServiceRefreshCliHandler.ServeHTTP(w, r)
case AuthServiceGetCliIssuerProcedure:
authServiceGetCliIssuerHandler.ServeHTTP(w, r)
case AuthServiceGetAuthorizeUrlProcedure:
@@ -182,6 +208,10 @@ func (UnimplementedAuthServiceHandler) LoginCli(context.Context, *connect.Reques
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgmt.v1alpha1.AuthService.LoginCli is not implemented"))
}
+func (UnimplementedAuthServiceHandler) RefreshCli(context.Context, *connect.Request[v1alpha1.RefreshCliRequest]) (*connect.Response[v1alpha1.RefreshCliResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgmt.v1alpha1.AuthService.RefreshCli is not implemented"))
+}
+
func (UnimplementedAuthServiceHandler) GetCliIssuer(context.Context, *connect.Request[v1alpha1.GetCliIssuerRequest]) (*connect.Response[v1alpha1.GetCliIssuerResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgmt.v1alpha1.AuthService.GetCliIssuer is not implemented"))
}
diff --git a/backend/internal/auth/client/client.go b/backend/internal/auth/client/client.go
index 5b6be0b25..247c5854e 100644
--- a/backend/internal/auth/client/client.go
+++ b/backend/internal/auth/client/client.go
@@ -112,3 +112,63 @@ func (c *Client) GetTokenResponse(
Error: nil,
}, nil
}
+
+func (c *Client) GetRefreshedAccessToken(
+ ctx context.Context,
+ clientId string,
+ refreshToken string,
+) (*AuthTokenResponse, error) {
+ if _, ok := c.clientIdSecretMap[clientId]; !ok {
+ return nil, errors.New("unknown client id, requested client was not in safelist")
+ }
+
+ clientSecret := c.clientIdSecretMap[clientId]
+ payload := strings.NewReader(
+ fmt.Sprintf(
+ "grant_type=refresh_token&client_id=%s&client_secret=%s&refresh_token=%s", clientId, clientSecret, refreshToken,
+ ),
+ )
+ req, err := http.NewRequestWithContext(ctx, "POST", c.tokenurl, payload)
+
+ if err != nil {
+ return nil, fmt.Errorf("unable to initiate refresh token request: %w", err)
+ }
+
+ req.Header.Add("content-type", "application/x-www-form-urlencoded")
+
+ res, err := getHttpClient().Do(req)
+
+ if err != nil {
+ return nil, fmt.Errorf("unable to fulfill refresh token request: %w", err)
+ }
+
+ defer res.Body.Close()
+ body, err := io.ReadAll(res.Body)
+
+ if err != nil {
+ return nil, fmt.Errorf("unable to read body from refresh token request: %w", err)
+ }
+
+ var tokenResponse *AuthTokenResponseData
+ err = json.Unmarshal(body, &tokenResponse)
+
+ if err != nil {
+ return nil, fmt.Errorf("unable to unmarshal token response from refresh token request: %w", err)
+ }
+
+ if tokenResponse.AccessToken == "" {
+ var errorResponse AuthTokenErrorData
+ err = json.Unmarshal(body, &errorResponse)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unmarshal error response from refresh token request: %w", err)
+ }
+ return &AuthTokenResponse{
+ Result: nil,
+ Error: &errorResponse,
+ }, nil
+ }
+ return &AuthTokenResponse{
+ Result: tokenResponse,
+ Error: nil,
+ }, nil
+}
diff --git a/backend/internal/cmds/mgmt/serve/connect/cmd.go b/backend/internal/cmds/mgmt/serve/connect/cmd.go
index 93541e52b..636fc7f06 100644
--- a/backend/internal/cmds/mgmt/serve/connect/cmd.go
+++ b/backend/internal/cmds/mgmt/serve/connect/cmd.go
@@ -176,6 +176,7 @@ func serve(ctx context.Context) error {
mgmtv1alpha1connect.AuthServiceGetAuthorizeUrlProcedure,
mgmtv1alpha1connect.AuthServiceGetCliIssuerProcedure,
mgmtv1alpha1connect.AuthServiceLoginCliProcedure,
+ mgmtv1alpha1connect.AuthServiceRefreshCliProcedure,
},
),
)
diff --git a/backend/protos/mgmt/v1alpha1/auth.proto b/backend/protos/mgmt/v1alpha1/auth.proto
index 181456e6d..b6e2b86b8 100644
--- a/backend/protos/mgmt/v1alpha1/auth.proto
+++ b/backend/protos/mgmt/v1alpha1/auth.proto
@@ -4,35 +4,6 @@ package mgmt.v1alpha1;
import "buf/validate/validate.proto";
-// message RefreshAccessTokenRequest {
-// string refresh_token = 1 [(buf.validate.field).string.min_len = 1];
-// optional string client_id = 2 [(buf.validate.field).string.min_len = 1];
-// }
-
-// message RefreshAccessTokenResponse {
-// string access_token = 1;
-// string refresh_token = 2;
-// int64 expires_in = 3;
-// string scope = 4;
-// string id_token = 5;
-// string token_type = 6;
-// }
-
-// message GetAccessTokenRequest {
-// string code = 1 [(buf.validate.field).string.min_len = 1];
-// optional string client_id = 2 [(buf.validate.field).string.min_len = 1];
-// optional string redirect_uri = 3 [(buf.validate.field).string.min_len = 1];
-// }
-
-// message GetAccessTokenResponse {
-// string access_token = 1;
-// string refresh_token = 2;
-// int64 expires_in = 3;
-// string scope = 4;
-// string id_token = 5;
-// string token_type = 6;
-// }
-
message LoginCliRequest {
// The oauth code
string code = 1 [(buf.validate.field).string.min_len = 1];
@@ -53,11 +24,18 @@ message GetAuthStatusResponse {
// A decoded representation of an Access token from the backing auth server
message AccessToken {
+ // The access token that will be provided in subsequent requests to provide authenticated access to the Api
string access_token = 1;
+ // Token that can be used to retrieve a refreshed access token.
+ // Will not be provided if the offline_access scope is not provided in the initial login flow.
optional string refresh_token = 2;
+ // Relative time in seconds that the access token will expire. Combine with the current time to get the expires_at time.
int64 expires_in = 3;
+ // The scopes that the access token have
string scope = 4;
+ // The identity token of the authenticated user
optional string id_token = 5;
+ // The token type. For JWTs, this will be `Bearer`
string token_type = 6;
}
@@ -82,17 +60,27 @@ message GetCliIssuerResponse {
string audience = 2;
}
+message RefreshCliRequest {
+ // The token used to retrieve a new access token.
+ string refresh_token = 1 [(buf.validate.field).string.min_len = 1];
+}
+message RefreshCliResponse {
+ // The access token that is returned on successful refresh
+ AccessToken access_token = 1;
+}
+
// Service that handles generic Authentication for Neosync
// Today this is mostly used by the CLI to receive authentication information
service AuthService {
// Used by the CLI to login to Neosync with OAuth.
rpc LoginCli(LoginCliRequest) returns (LoginCliResponse) {}
+ // Used by the CLI to refresh an expired Neosync accesss token.
+ // This should only be used if an access token was previously retrieved from the `LoginCli` or `RefreshCli` methods.
+ rpc RefreshCli(RefreshCliRequest) returns (RefreshCliResponse) {}
// Used by the CLI to retrieve Auth Issuer information
rpc GetCliIssuer(GetCliIssuerRequest) returns (GetCliIssuerResponse) {}
// Used by the CLI to retrieve an Authorize URL for use with OAuth login.
rpc GetAuthorizeUrl(GetAuthorizeUrlRequest) returns (GetAuthorizeUrlResponse) {}
- // rpc GetAccessToken(GetAccessTokenRequest) returns (GetAccessTokenResponse) {}
- // rpc RefreshAccessToken(RefreshAccessTokenRequest) returns (RefreshAccessTokenResponse) {}
// Returns the auth status of the API server. Whether or not the backend has authentication enabled.
// This is used by clients to make decisions on whether or not they should send access tokens to the API.
diff --git a/backend/services/mgmt/v1alpha1/auth-service/service.go b/backend/services/mgmt/v1alpha1/auth-service/service.go
index 83751b004..bba71f829 100644
--- a/backend/services/mgmt/v1alpha1/auth-service/service.go
+++ b/backend/services/mgmt/v1alpha1/auth-service/service.go
@@ -27,6 +27,11 @@ type AuthClient interface {
code string,
redirecturi string,
) (*auth_client.AuthTokenResponse, error)
+ GetRefreshedAccessToken(
+ ctx context.Context,
+ clientId string,
+ refreshToken string,
+ ) (*auth_client.AuthTokenResponse, error)
}
func New(
diff --git a/backend/services/mgmt/v1alpha1/auth-service/tokens.go b/backend/services/mgmt/v1alpha1/auth-service/tokens.go
index ee59748e8..6048a20d3 100644
--- a/backend/services/mgmt/v1alpha1/auth-service/tokens.go
+++ b/backend/services/mgmt/v1alpha1/auth-service/tokens.go
@@ -6,10 +6,9 @@ import (
"net/url"
"connectrpc.com/connect"
- "github.com/gogo/status"
mgmtv1alpha1 "github.com/nucleuscloud/neosync/backend/gen/go/protos/mgmt/v1alpha1"
logger_interceptor "github.com/nucleuscloud/neosync/backend/internal/connect/interceptors/logger"
- "google.golang.org/grpc/codes"
+ nucleuserrors "github.com/nucleuscloud/neosync/backend/internal/errors"
)
func (s *Service) GetAuthStatus(
@@ -34,7 +33,7 @@ func (s *Service) LoginCli(
logger.Error(
fmt.Sprintf("Unable to get access token. Title: %s -- Description: %s", resp.Error.Error, resp.Error.ErrorDescription),
)
- return nil, status.Errorf(codes.Unauthenticated, "Request unauthenticated")
+ return nil, nucleuserrors.NewUnauthenticated("Request unauthenticated")
}
var refreshToken *string
if resp.Result.RefreshToken != "" {
@@ -56,6 +55,41 @@ func (s *Service) LoginCli(
}), nil
}
+func (s *Service) RefreshCli(
+ ctx context.Context,
+ req *connect.Request[mgmtv1alpha1.RefreshCliRequest],
+) (*connect.Response[mgmtv1alpha1.RefreshCliResponse], error) {
+ logger := logger_interceptor.GetLoggerFromContextOrDefault(ctx)
+ resp, err := s.authclient.GetRefreshedAccessToken(ctx, s.cfg.CliClientId, req.Msg.RefreshToken)
+ if err != nil {
+ return nil, err
+ }
+ if resp.Error != nil {
+ logger.Error(
+ fmt.Sprintf("Unable to get refreshed token. Title: %s -- Description: %s", resp.Error.Error, resp.Error.ErrorDescription),
+ )
+ return nil, nucleuserrors.NewUnauthenticated("Unable to refresh access token")
+ }
+ var refreshToken *string
+ if resp.Result.RefreshToken != "" {
+ refreshToken = &resp.Result.RefreshToken
+ }
+ var idToken *string
+ if resp.Result.IdToken != "" {
+ idToken = &resp.Result.IdToken
+ }
+ return connect.NewResponse(&mgmtv1alpha1.RefreshCliResponse{
+ AccessToken: &mgmtv1alpha1.AccessToken{
+ AccessToken: resp.Result.AccessToken,
+ RefreshToken: refreshToken,
+ ExpiresIn: int64(resp.Result.ExpiresIn),
+ Scope: resp.Result.Scope,
+ IdToken: idToken,
+ TokenType: resp.Result.TokenType,
+ },
+ }), nil
+}
+
func (s *Service) GetAuthorizeUrl(
ctx context.Context,
req *connect.Request[mgmtv1alpha1.GetAuthorizeUrlRequest],
@@ -83,17 +117,3 @@ func (s *Service) GetCliIssuer(
IssuerUrl: s.cfg.IssuerUrl,
}), nil
}
-
-// func (s *Service) GetAccessToken(
-// ctx context.Context,
-// req *connect.Request[mgmtv1alpha1.GetAccessTokenRequest],
-// ) (*connect.Response[mgmtv1alpha1.GetAccessTokenResponse], error) {
-// return nil, nucleuserrors.NewNotImplemented("method is not yet implemented")
-// }
-
-// func (s *Service) RefreshAccessToken(
-// ctx context.Context,
-// req *connect.Request[mgmtv1alpha1.RefreshAccessTokenRequest],
-// ) (*connect.Response[mgmtv1alpha1.RefreshAccessTokenResponse], error) {
-// return nil, nucleuserrors.NewNotImplemented("method is not yet implemented")
-// }
diff --git a/cli/internal/auth/tokens.go b/cli/internal/auth/tokens.go
index a93e8e63f..9ed0fc34b 100644
--- a/cli/internal/auth/tokens.go
+++ b/cli/internal/auth/tokens.go
@@ -35,7 +35,7 @@ func GetAuthHeaderTokenFn(
func getAuthHeaderToken(ctx context.Context) (string, error) {
token, err := getToken(ctx)
if err != nil {
- return "", err
+ return "", fmt.Errorf("unable to get access token, try running neosync login again or provide an API Key: %w", err)
}
return fmt.Sprintf("Bearer %s", token), nil
}
@@ -66,10 +66,27 @@ func getToken(ctx context.Context) (string, error) {
slog.Info("access token is no longer valid. attempting to refresh...")
refreshtoken, err := userconfig.GetRefreshToken()
if err != nil {
+ slog.Info("unable to find refresh token")
+ return "", err
+ }
+ refreshResp, err := authclient.RefreshCli(ctx, connect.NewRequest(&mgmtv1alpha1.RefreshCliRequest{
+ RefreshToken: refreshtoken,
+ }))
+ if err != nil {
+ slog.Info("unable to refresh token")
return "", err
}
- _ = refreshtoken
- // todo
+ err = userconfig.SetAccessToken(refreshResp.Msg.AccessToken.AccessToken)
+ if err != nil {
+ slog.Warn("unable to write refreshed access token back to user config", "error", err.Error())
+ }
+ if refreshResp.Msg.AccessToken.RefreshToken != nil {
+ err = userconfig.SetRefreshToken(*refreshResp.Msg.AccessToken.RefreshToken)
+ if err != nil {
+ slog.Warn("unable to write refreshed refresh token back to user config", "error", err.Error())
+ }
+ }
+ return refreshResp.Msg.AccessToken.AccessToken, nil
}
return accessToken, nil
}
diff --git a/cli/internal/cmds/neosync/login/login.go b/cli/internal/cmds/neosync/login/login.go
index bf30145c6..013943d31 100644
--- a/cli/internal/cmds/neosync/login/login.go
+++ b/cli/internal/cmds/neosync/login/login.go
@@ -114,7 +114,7 @@ func oAuthLogin(
authorizeurlResp, err := authclient.GetAuthorizeUrl(ctx, connect.NewRequest(&mgmtv1alpha1.GetAuthorizeUrlRequest{
State: state,
RedirectUri: redirectUri,
- Scope: "openid profile",
+ Scope: "openid profile offline_access",
}))
if err != nil {
return err
diff --git a/docs/protos/data/proto_docs.json b/docs/protos/data/proto_docs.json
index 3efa60cc3..fb118955b 100644
--- a/docs/protos/data/proto_docs.json
+++ b/docs/protos/data/proto_docs.json
@@ -501,7 +501,7 @@
"fields": [
{
"name": "access_token",
- "description": "",
+ "description": "The access token that will be provided in subsequent requests to provide authenticated access to the Api",
"label": "",
"type": "string",
"longType": "string",
@@ -513,7 +513,7 @@
},
{
"name": "refresh_token",
- "description": "",
+ "description": "Token that can be used to retrieve a refreshed access token.\nWill not be provided if the offline_access scope is not provided in the initial login flow.",
"label": "optional",
"type": "string",
"longType": "string",
@@ -525,7 +525,7 @@
},
{
"name": "expires_in",
- "description": "",
+ "description": "Relative time in seconds that the access token will expire. Combine with the current time to get the expires_at time.",
"label": "",
"type": "int64",
"longType": "int64",
@@ -537,7 +537,7 @@
},
{
"name": "scope",
- "description": "",
+ "description": "The scopes that the access token have",
"label": "",
"type": "string",
"longType": "string",
@@ -549,7 +549,7 @@
},
{
"name": "id_token",
- "description": "",
+ "description": "The identity token of the authenticated user",
"label": "optional",
"type": "string",
"longType": "string",
@@ -561,7 +561,7 @@
},
{
"name": "token_type",
- "description": "",
+ "description": "The token type. For JWTs, this will be `Bearer`",
"label": "",
"type": "string",
"longType": "string",
@@ -786,6 +786,54 @@
"defaultValue": ""
}
]
+ },
+ {
+ "name": "RefreshCliRequest",
+ "longName": "RefreshCliRequest",
+ "fullName": "mgmt.v1alpha1.RefreshCliRequest",
+ "description": "",
+ "hasExtensions": false,
+ "hasFields": true,
+ "hasOneofs": false,
+ "extensions": [],
+ "fields": [
+ {
+ "name": "refresh_token",
+ "description": "The token used to retrieve a new access token.",
+ "label": "",
+ "type": "string",
+ "longType": "string",
+ "fullType": "string",
+ "ismap": false,
+ "isoneof": false,
+ "oneofdecl": "",
+ "defaultValue": ""
+ }
+ ]
+ },
+ {
+ "name": "RefreshCliResponse",
+ "longName": "RefreshCliResponse",
+ "fullName": "mgmt.v1alpha1.RefreshCliResponse",
+ "description": "",
+ "hasExtensions": false,
+ "hasFields": true,
+ "hasOneofs": false,
+ "extensions": [],
+ "fields": [
+ {
+ "name": "access_token",
+ "description": "The access token that is returned on successful refresh",
+ "label": "",
+ "type": "AccessToken",
+ "longType": "AccessToken",
+ "fullType": "mgmt.v1alpha1.AccessToken",
+ "ismap": false,
+ "isoneof": false,
+ "oneofdecl": "",
+ "defaultValue": ""
+ }
+ ]
}
],
"services": [
@@ -807,6 +855,18 @@
"responseFullType": "mgmt.v1alpha1.LoginCliResponse",
"responseStreaming": false
},
+ {
+ "name": "RefreshCli",
+ "description": "Used by the CLI to refresh an expired Neosync accesss token.\nThis should only be used if an access token was previously retrieved from the `LoginCli` or `RefreshCli` methods.",
+ "requestType": "RefreshCliRequest",
+ "requestLongType": "RefreshCliRequest",
+ "requestFullType": "mgmt.v1alpha1.RefreshCliRequest",
+ "requestStreaming": false,
+ "responseType": "RefreshCliResponse",
+ "responseLongType": "RefreshCliResponse",
+ "responseFullType": "mgmt.v1alpha1.RefreshCliResponse",
+ "responseStreaming": false
+ },
{
"name": "GetCliIssuer",
"description": "Used by the CLI to retrieve Auth Issuer information",
diff --git a/docs/protos/mgmt/v1alpha1/auth.proto.mdx b/docs/protos/mgmt/v1alpha1/auth.proto.mdx
index 34c891cc9..801c2a15d 100644
--- a/docs/protos/mgmt/v1alpha1/auth.proto.mdx
+++ b/docs/protos/mgmt/v1alpha1/auth.proto.mdx
@@ -18,7 +18,7 @@ _**package** mgmt.v1alpha1_
### `AccessToken`
-
+
### `GetAuthStatusRequest`
@@ -52,6 +52,14 @@ _**package** mgmt.v1alpha1_
### `LoginCliResponse`
+
+### `RefreshCliRequest`
+
+
+
+### `RefreshCliResponse`
+
+
---
## Services
@@ -66,16 +74,20 @@ Today this is mostly used by the CLI to receive authentication information
+#### `RefreshCli`
+
+
+
#### `GetCliIssuer`
-
+
#### `GetAuthorizeUrl`
-
+
#### `GetAuthStatus`
-
+
---
diff --git a/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_connect.ts b/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_connect.ts
index 4236b0556..83b88f09a 100644
--- a/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_connect.ts
+++ b/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_connect.ts
@@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
-import { GetAuthorizeUrlRequest, GetAuthorizeUrlResponse, GetAuthStatusRequest, GetAuthStatusResponse, GetCliIssuerRequest, GetCliIssuerResponse, LoginCliRequest, LoginCliResponse } from "./auth_pb.js";
+import { GetAuthorizeUrlRequest, GetAuthorizeUrlResponse, GetAuthStatusRequest, GetAuthStatusResponse, GetCliIssuerRequest, GetCliIssuerResponse, LoginCliRequest, LoginCliResponse, RefreshCliRequest, RefreshCliResponse } from "./auth_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
@@ -26,6 +26,18 @@ export const AuthService = {
O: LoginCliResponse,
kind: MethodKind.Unary,
},
+ /**
+ * Used by the CLI to refresh an expired Neosync accesss token.
+ * This should only be used if an access token was previously retrieved from the `LoginCli` or `RefreshCli` methods.
+ *
+ * @generated from rpc mgmt.v1alpha1.AuthService.RefreshCli
+ */
+ refreshCli: {
+ name: "RefreshCli",
+ I: RefreshCliRequest,
+ O: RefreshCliResponse,
+ kind: MethodKind.Unary,
+ },
/**
* Used by the CLI to retrieve Auth Issuer information
*
diff --git a/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_pb.ts b/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_pb.ts
index c08f4277b..37b09f5c8 100644
--- a/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_pb.ts
+++ b/frontend/packages/sdk/src/client/mgmt/v1alpha1/auth_pb.ts
@@ -170,31 +170,44 @@ export class GetAuthStatusResponse extends Message {
*/
export class AccessToken extends Message {
/**
+ * The access token that will be provided in subsequent requests to provide authenticated access to the Api
+ *
* @generated from field: string access_token = 1;
*/
accessToken = "";
/**
+ * Token that can be used to retrieve a refreshed access token.
+ * Will not be provided if the offline_access scope is not provided in the initial login flow.
+ *
* @generated from field: optional string refresh_token = 2;
*/
refreshToken?: string;
/**
+ * Relative time in seconds that the access token will expire. Combine with the current time to get the expires_at time.
+ *
* @generated from field: int64 expires_in = 3;
*/
expiresIn = protoInt64.zero;
/**
+ * The scopes that the access token have
+ *
* @generated from field: string scope = 4;
*/
scope = "";
/**
+ * The identity token of the authenticated user
+ *
* @generated from field: optional string id_token = 5;
*/
idToken?: string;
/**
+ * The token type. For JWTs, this will be `Bearer`
+ *
* @generated from field: string token_type = 6;
*/
tokenType = "";
@@ -404,3 +417,81 @@ export class GetCliIssuerResponse extends Message {
}
}
+/**
+ * @generated from message mgmt.v1alpha1.RefreshCliRequest
+ */
+export class RefreshCliRequest extends Message {
+ /**
+ * The token used to retrieve a new access token.
+ *
+ * @generated from field: string refresh_token = 1;
+ */
+ refreshToken = "";
+
+ constructor(data?: PartialMessage) {
+ super();
+ proto3.util.initPartial(data, this);
+ }
+
+ static readonly runtime: typeof proto3 = proto3;
+ static readonly typeName = "mgmt.v1alpha1.RefreshCliRequest";
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
+ { no: 1, name: "refresh_token", kind: "scalar", T: 9 /* ScalarType.STRING */ },
+ ]);
+
+ static fromBinary(bytes: Uint8Array, options?: Partial): RefreshCliRequest {
+ return new RefreshCliRequest().fromBinary(bytes, options);
+ }
+
+ static fromJson(jsonValue: JsonValue, options?: Partial): RefreshCliRequest {
+ return new RefreshCliRequest().fromJson(jsonValue, options);
+ }
+
+ static fromJsonString(jsonString: string, options?: Partial): RefreshCliRequest {
+ return new RefreshCliRequest().fromJsonString(jsonString, options);
+ }
+
+ static equals(a: RefreshCliRequest | PlainMessage | undefined, b: RefreshCliRequest | PlainMessage | undefined): boolean {
+ return proto3.util.equals(RefreshCliRequest, a, b);
+ }
+}
+
+/**
+ * @generated from message mgmt.v1alpha1.RefreshCliResponse
+ */
+export class RefreshCliResponse extends Message {
+ /**
+ * The access token that is returned on successful refresh
+ *
+ * @generated from field: mgmt.v1alpha1.AccessToken access_token = 1;
+ */
+ accessToken?: AccessToken;
+
+ constructor(data?: PartialMessage) {
+ super();
+ proto3.util.initPartial(data, this);
+ }
+
+ static readonly runtime: typeof proto3 = proto3;
+ static readonly typeName = "mgmt.v1alpha1.RefreshCliResponse";
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
+ { no: 1, name: "access_token", kind: "message", T: AccessToken },
+ ]);
+
+ static fromBinary(bytes: Uint8Array, options?: Partial): RefreshCliResponse {
+ return new RefreshCliResponse().fromBinary(bytes, options);
+ }
+
+ static fromJson(jsonValue: JsonValue, options?: Partial): RefreshCliResponse {
+ return new RefreshCliResponse().fromJson(jsonValue, options);
+ }
+
+ static fromJsonString(jsonString: string, options?: Partial): RefreshCliResponse {
+ return new RefreshCliResponse().fromJsonString(jsonString, options);
+ }
+
+ static equals(a: RefreshCliResponse | PlainMessage | undefined, b: RefreshCliResponse | PlainMessage | undefined): boolean {
+ return proto3.util.equals(RefreshCliResponse, a, b);
+ }
+}
+