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); + } +} +