From d6215481206f4c6e252794aa374339e08c9e904e Mon Sep 17 00:00:00 2001 From: ben-toogood Date: Mon, 3 Feb 2020 08:16:02 +0000 Subject: [PATCH] Auth (#1147) Implement the Auth interface, with JWT and service implementations. * Update Auth Interface * Define Auth Service Implementation * Support Service Auth * Add Auth Service Proto * Remove erronious files * Implement Auth Service Package * Update Auth Interface * Update Auth Interface. Add Validate, remove Add/Remove roles * Make Revoke interface more explicit * Refactor serializing and deserializing service accounts * Fix srv name & update interface to be more explicit * Require jwt public key for auth * Rename Variables (Resource.ID => Resource.Name & ServiceAccount => Account) * Implement JWT Auth Package * Remove parent, add ID * Update auth imports to v2. Add String() to auth interface --- auth/auth.go | 48 +-- auth/default.go | 34 ++ auth/jwt/jwt.go | 106 +++++++ auth/options.go | 57 ++++ auth/service/proto/auth.pb.go | 466 ++++++++++++++++++++++++++++ auth/service/proto/auth.pb.micro.go | 125 ++++++++ auth/service/proto/auth.proto | 50 +++ auth/service/service.go | 128 ++++++++ config/cmd/cmd.go | 49 +++ config/cmd/options.go | 16 + go.mod | 1 + options.go | 8 + server/options.go | 9 + store/memory/memory.go | 21 +- store/options.go | 9 + store/store.go | 2 + 16 files changed, 1103 insertions(+), 26 deletions(-) create mode 100644 auth/default.go create mode 100644 auth/jwt/jwt.go create mode 100644 auth/options.go create mode 100644 auth/service/proto/auth.pb.go create mode 100644 auth/service/proto/auth.pb.micro.go create mode 100644 auth/service/proto/auth.proto create mode 100644 auth/service/service.go diff --git a/auth/auth.go b/auth/auth.go index a7750745..69f22c91 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -7,34 +7,44 @@ import ( // Auth providers authentication and authorization type Auth interface { - // Generate a new auth token - Generate(string) (*Token, error) - // Revoke an authorization token - Revoke(*Token) error - // Grant access to a resource - Grant(*Token, *Service) error - // Verify a token can access a resource - Verify(*Token, *Service) error + // String to identify the package + String() string + // Init the auth package + Init(opts ...Option) error + // Generate a new auth Account + Generate(id string, opts ...GenerateOption) (*Account, error) + // Revoke an authorization Account + Revoke(token string) error + // Validate an account token + Validate(token string) (*Account, error) } -// Service is some thing to provide access to -type Service struct { +// Resource is an entity such as a user or +type Resource struct { // Name of the resource Name string - // Endpoint is the specific endpoint - Endpoint string + // Type of resource, e.g. + Type string } -// Token providers by an auth provider -type Token struct { - // Unique token id +// Role an account has +type Role struct { + Name string + Resource *Resource +} + +// Account provided by an auth provider +type Account struct { + // ID of the account (UUID or email) Id string `json: "id"` - // Time of token creation + // Token used to authenticate + Token string `json: "token"` + // Time of Account creation Created time.Time `json:"created"` - // Time of token expiry + // Time of Account expiry Expiry time.Time `json:"expiry"` - // Roles associated with the token - Roles []string `json:"roles"` + // Roles associated with the Account + Roles []*Role `json:"roles"` // Any other associated metadata Metadata map[string]string `json:"metadata"` } diff --git a/auth/default.go b/auth/default.go new file mode 100644 index 00000000..f463a003 --- /dev/null +++ b/auth/default.go @@ -0,0 +1,34 @@ +package auth + +var ( + DefaultAuth Auth = new(noop) +) + +type noop struct { + options Options +} + +// String name of implementation +func (a *noop) String() string { + return "noop" +} + +// Init the svc +func (a *noop) Init(...Option) error { + return nil +} + +// Generate a new auth Account +func (a *noop) Generate(id string, ops ...GenerateOption) (*Account, error) { + return nil, nil +} + +// Revoke an authorization Account +func (a *noop) Revoke(token string) error { + return nil +} + +// Validate a account token +func (a *noop) Validate(token string) (*Account, error) { + return nil, nil +} diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go new file mode 100644 index 00000000..47498001 --- /dev/null +++ b/auth/jwt/jwt.go @@ -0,0 +1,106 @@ +package jwt + +import ( + "errors" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/micro/go-micro/v2/auth" +) + +// ErrInvalidPrivateKey is returned when the service provided an invalid private key +var ErrInvalidPrivateKey = errors.New("An invalid private key was provided") + +// ErrEncodingToken is returned when the service encounters an error during encoding +var ErrEncodingToken = errors.New("An error occured while encoding the JWT") + +// ErrInvalidToken is returned when the token provided is not valid +var ErrInvalidToken = errors.New("An invalid token was provided") + +// NewAuth returns a new instance of the Auth service +func NewAuth(opts ...auth.Option) auth.Auth { + svc := new(svc) + svc.Init(opts...) + return svc +} + +// svc is the JWT implementation of the Auth interface +type svc struct { + options auth.Options +} + +func (s *svc) String() string { + return "jwt" +} + +func (s *svc) Init(opts ...auth.Option) error { + for _, o := range opts { + o(&s.options) + } + + return nil +} + +// AuthClaims to be encoded in the JWT +type AuthClaims struct { + Id string `json:"id"` + Roles []*auth.Role `json:"roles"` + Metadata map[string]string `json:"metadata"` + + jwt.StandardClaims +} + +// Generate a new JWT +func (s *svc) Generate(id string, ops ...auth.GenerateOption) (*auth.Account, error) { + key, err := jwt.ParseRSAPrivateKeyFromPEM(s.options.PrivateKey) + if err != nil { + return nil, ErrEncodingToken + } + + options := auth.NewGenerateOptions(ops...) + account := jwt.NewWithClaims(jwt.SigningMethodRS256, AuthClaims{ + id, options.Roles, options.Metadata, jwt.StandardClaims{ + Subject: "TODO", + ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), + }, + }) + + token, err := account.SignedString(key) + if err != nil { + return nil, err + } + + return &auth.Account{ + Id: id, + Token: token, + Roles: options.Roles, + Metadata: options.Metadata, + }, nil +} + +// Revoke an authorization account +func (s *svc) Revoke(token string) error { + return nil +} + +// Validate a JWT +func (s *svc) Validate(token string) (*auth.Account, error) { + res, err := jwt.ParseWithClaims(token, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { + return jwt.ParseRSAPublicKeyFromPEM(s.options.PublicKey) + }) + if err != nil { + return nil, err + } + + if !res.Valid { + return nil, ErrInvalidToken + } + + claims := res.Claims.(*AuthClaims) + + return &auth.Account{ + Id: claims.Id, + Metadata: claims.Metadata, + Roles: claims.Roles, + }, nil +} diff --git a/auth/options.go b/auth/options.go new file mode 100644 index 00000000..3488c03c --- /dev/null +++ b/auth/options.go @@ -0,0 +1,57 @@ +package auth + +import ( + b64 "encoding/base64" +) + +type Options struct { + PublicKey []byte + PrivateKey []byte +} + +type Option func(o *Options) + +// PublicKey is the JWT public key +func PublicKey(key string) Option { + return func(o *Options) { + o.PublicKey, _ = b64.StdEncoding.DecodeString(key) + } +} + +// PrivateKey is the JWT private key +func PrivateKey(key string) Option { + return func(o *Options) { + o.PrivateKey, _ = b64.StdEncoding.DecodeString(key) + } +} + +type GenerateOptions struct { + Metadata map[string]string + Roles []*Role +} + +type GenerateOption func(o *GenerateOptions) + +// Metadata for the generated account +func Metadata(md map[string]string) func(o *GenerateOptions) { + return func(o *GenerateOptions) { + o.Metadata = md + } +} + +// Roles for the generated account +func Roles(rs []*Role) func(o *GenerateOptions) { + return func(o *GenerateOptions) { + o.Roles = rs + } +} + +// NewGenerateOptions from a slice of options +func NewGenerateOptions(opts ...GenerateOption) GenerateOptions { + var options GenerateOptions + for _, o := range opts { + o(&options) + } + + return options +} diff --git a/auth/service/proto/auth.pb.go b/auth/service/proto/auth.pb.go new file mode 100644 index 00000000..de3d6b28 --- /dev/null +++ b/auth/service/proto/auth.pb.go @@ -0,0 +1,466 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: auth/service/proto/auth.proto + +package go_micro_auth + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Account struct { + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + Created int64 `protobuf:"varint,3,opt,name=created,proto3" json:"created,omitempty"` + Expiry int64 `protobuf:"varint,4,opt,name=expiry,proto3" json:"expiry,omitempty"` + Roles []*Role `protobuf:"bytes,5,rep,name=roles,proto3" json:"roles,omitempty"` + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Account) Reset() { *m = Account{} } +func (m *Account) String() string { return proto.CompactTextString(m) } +func (*Account) ProtoMessage() {} +func (*Account) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{0} +} + +func (m *Account) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Account.Unmarshal(m, b) +} +func (m *Account) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Account.Marshal(b, m, deterministic) +} +func (m *Account) XXX_Merge(src proto.Message) { + xxx_messageInfo_Account.Merge(m, src) +} +func (m *Account) XXX_Size() int { + return xxx_messageInfo_Account.Size(m) +} +func (m *Account) XXX_DiscardUnknown() { + xxx_messageInfo_Account.DiscardUnknown(m) +} + +var xxx_messageInfo_Account proto.InternalMessageInfo + +func (m *Account) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *Account) GetToken() string { + if m != nil { + return m.Token + } + return "" +} + +func (m *Account) GetCreated() int64 { + if m != nil { + return m.Created + } + return 0 +} + +func (m *Account) GetExpiry() int64 { + if m != nil { + return m.Expiry + } + return 0 +} + +func (m *Account) GetRoles() []*Role { + if m != nil { + return m.Roles + } + return nil +} + +func (m *Account) GetMetadata() map[string]string { + if m != nil { + return m.Metadata + } + return nil +} + +type Role struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Resource *Resource `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Role) Reset() { *m = Role{} } +func (m *Role) String() string { return proto.CompactTextString(m) } +func (*Role) ProtoMessage() {} +func (*Role) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{1} +} + +func (m *Role) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Role.Unmarshal(m, b) +} +func (m *Role) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Role.Marshal(b, m, deterministic) +} +func (m *Role) XXX_Merge(src proto.Message) { + xxx_messageInfo_Role.Merge(m, src) +} +func (m *Role) XXX_Size() int { + return xxx_messageInfo_Role.Size(m) +} +func (m *Role) XXX_DiscardUnknown() { + xxx_messageInfo_Role.DiscardUnknown(m) +} + +var xxx_messageInfo_Role proto.InternalMessageInfo + +func (m *Role) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Role) GetResource() *Resource { + if m != nil { + return m.Resource + } + return nil +} + +type Resource struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Resource) Reset() { *m = Resource{} } +func (m *Resource) String() string { return proto.CompactTextString(m) } +func (*Resource) ProtoMessage() {} +func (*Resource) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{2} +} + +func (m *Resource) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Resource.Unmarshal(m, b) +} +func (m *Resource) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Resource.Marshal(b, m, deterministic) +} +func (m *Resource) XXX_Merge(src proto.Message) { + xxx_messageInfo_Resource.Merge(m, src) +} +func (m *Resource) XXX_Size() int { + return xxx_messageInfo_Resource.Size(m) +} +func (m *Resource) XXX_DiscardUnknown() { + xxx_messageInfo_Resource.DiscardUnknown(m) +} + +var xxx_messageInfo_Resource proto.InternalMessageInfo + +func (m *Resource) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Resource) GetType() string { + if m != nil { + return m.Type + } + return "" +} + +type GenerateRequest struct { + Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GenerateRequest) Reset() { *m = GenerateRequest{} } +func (m *GenerateRequest) String() string { return proto.CompactTextString(m) } +func (*GenerateRequest) ProtoMessage() {} +func (*GenerateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{3} +} + +func (m *GenerateRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GenerateRequest.Unmarshal(m, b) +} +func (m *GenerateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GenerateRequest.Marshal(b, m, deterministic) +} +func (m *GenerateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenerateRequest.Merge(m, src) +} +func (m *GenerateRequest) XXX_Size() int { + return xxx_messageInfo_GenerateRequest.Size(m) +} +func (m *GenerateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GenerateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GenerateRequest proto.InternalMessageInfo + +func (m *GenerateRequest) GetAccount() *Account { + if m != nil { + return m.Account + } + return nil +} + +type GenerateResponse struct { + Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GenerateResponse) Reset() { *m = GenerateResponse{} } +func (m *GenerateResponse) String() string { return proto.CompactTextString(m) } +func (*GenerateResponse) ProtoMessage() {} +func (*GenerateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{4} +} + +func (m *GenerateResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GenerateResponse.Unmarshal(m, b) +} +func (m *GenerateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GenerateResponse.Marshal(b, m, deterministic) +} +func (m *GenerateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenerateResponse.Merge(m, src) +} +func (m *GenerateResponse) XXX_Size() int { + return xxx_messageInfo_GenerateResponse.Size(m) +} +func (m *GenerateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GenerateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GenerateResponse proto.InternalMessageInfo + +func (m *GenerateResponse) GetAccount() *Account { + if m != nil { + return m.Account + } + return nil +} + +type ValidateRequest struct { + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ValidateRequest) Reset() { *m = ValidateRequest{} } +func (m *ValidateRequest) String() string { return proto.CompactTextString(m) } +func (*ValidateRequest) ProtoMessage() {} +func (*ValidateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{5} +} + +func (m *ValidateRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ValidateRequest.Unmarshal(m, b) +} +func (m *ValidateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ValidateRequest.Marshal(b, m, deterministic) +} +func (m *ValidateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ValidateRequest.Merge(m, src) +} +func (m *ValidateRequest) XXX_Size() int { + return xxx_messageInfo_ValidateRequest.Size(m) +} +func (m *ValidateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ValidateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ValidateRequest proto.InternalMessageInfo + +func (m *ValidateRequest) GetToken() string { + if m != nil { + return m.Token + } + return "" +} + +type ValidateResponse struct { + Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ValidateResponse) Reset() { *m = ValidateResponse{} } +func (m *ValidateResponse) String() string { return proto.CompactTextString(m) } +func (*ValidateResponse) ProtoMessage() {} +func (*ValidateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{6} +} + +func (m *ValidateResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ValidateResponse.Unmarshal(m, b) +} +func (m *ValidateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ValidateResponse.Marshal(b, m, deterministic) +} +func (m *ValidateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ValidateResponse.Merge(m, src) +} +func (m *ValidateResponse) XXX_Size() int { + return xxx_messageInfo_ValidateResponse.Size(m) +} +func (m *ValidateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ValidateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ValidateResponse proto.InternalMessageInfo + +func (m *ValidateResponse) GetAccount() *Account { + if m != nil { + return m.Account + } + return nil +} + +type RevokeRequest struct { + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RevokeRequest) Reset() { *m = RevokeRequest{} } +func (m *RevokeRequest) String() string { return proto.CompactTextString(m) } +func (*RevokeRequest) ProtoMessage() {} +func (*RevokeRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{7} +} + +func (m *RevokeRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RevokeRequest.Unmarshal(m, b) +} +func (m *RevokeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RevokeRequest.Marshal(b, m, deterministic) +} +func (m *RevokeRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RevokeRequest.Merge(m, src) +} +func (m *RevokeRequest) XXX_Size() int { + return xxx_messageInfo_RevokeRequest.Size(m) +} +func (m *RevokeRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RevokeRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RevokeRequest proto.InternalMessageInfo + +func (m *RevokeRequest) GetToken() string { + if m != nil { + return m.Token + } + return "" +} + +type RevokeResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RevokeResponse) Reset() { *m = RevokeResponse{} } +func (m *RevokeResponse) String() string { return proto.CompactTextString(m) } +func (*RevokeResponse) ProtoMessage() {} +func (*RevokeResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_21300bfacc51fc2a, []int{8} +} + +func (m *RevokeResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RevokeResponse.Unmarshal(m, b) +} +func (m *RevokeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RevokeResponse.Marshal(b, m, deterministic) +} +func (m *RevokeResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_RevokeResponse.Merge(m, src) +} +func (m *RevokeResponse) XXX_Size() int { + return xxx_messageInfo_RevokeResponse.Size(m) +} +func (m *RevokeResponse) XXX_DiscardUnknown() { + xxx_messageInfo_RevokeResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_RevokeResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*Account)(nil), "go.micro.auth.Account") + proto.RegisterMapType((map[string]string)(nil), "go.micro.auth.Account.MetadataEntry") + proto.RegisterType((*Role)(nil), "go.micro.auth.Role") + proto.RegisterType((*Resource)(nil), "go.micro.auth.Resource") + proto.RegisterType((*GenerateRequest)(nil), "go.micro.auth.GenerateRequest") + proto.RegisterType((*GenerateResponse)(nil), "go.micro.auth.GenerateResponse") + proto.RegisterType((*ValidateRequest)(nil), "go.micro.auth.ValidateRequest") + proto.RegisterType((*ValidateResponse)(nil), "go.micro.auth.ValidateResponse") + proto.RegisterType((*RevokeRequest)(nil), "go.micro.auth.RevokeRequest") + proto.RegisterType((*RevokeResponse)(nil), "go.micro.auth.RevokeResponse") +} + +func init() { proto.RegisterFile("auth/service/proto/auth.proto", fileDescriptor_21300bfacc51fc2a) } + +var fileDescriptor_21300bfacc51fc2a = []byte{ + // 429 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0x4d, 0x6f, 0xd3, 0x40, + 0x10, 0xad, 0x3f, 0xe2, 0x98, 0x89, 0xd2, 0x46, 0x03, 0x2a, 0x56, 0x44, 0x21, 0xb2, 0x40, 0x84, + 0x8b, 0x83, 0xdc, 0x0b, 0x82, 0x0b, 0x15, 0xa0, 0x9e, 0x2a, 0xa4, 0x3d, 0x70, 0x5f, 0xec, 0x11, + 0xb5, 0xe2, 0x78, 0xcd, 0x7a, 0x1d, 0xe1, 0xdf, 0xc0, 0x6f, 0xe5, 0x3f, 0x20, 0xaf, 0xbd, 0x69, + 0xea, 0xb4, 0xaa, 0xd4, 0xdb, 0x7c, 0xbc, 0x79, 0xf3, 0xde, 0x68, 0x17, 0xce, 0x78, 0xad, 0xae, + 0x57, 0x15, 0xc9, 0x6d, 0x96, 0xd0, 0xaa, 0x94, 0x42, 0x89, 0x55, 0x5b, 0x8a, 0x74, 0x88, 0xd3, + 0x5f, 0x22, 0xda, 0x64, 0x89, 0x14, 0x51, 0x5b, 0x0c, 0xff, 0xda, 0x30, 0xbe, 0x48, 0x12, 0x51, + 0x17, 0x0a, 0x8f, 0xc1, 0xce, 0xd2, 0xc0, 0x5a, 0x58, 0xcb, 0x27, 0xcc, 0xce, 0x52, 0x7c, 0x06, + 0x23, 0x25, 0xd6, 0x54, 0x04, 0xb6, 0x2e, 0x75, 0x09, 0x06, 0x30, 0x4e, 0x24, 0x71, 0x45, 0x69, + 0xe0, 0x2c, 0xac, 0xa5, 0xc3, 0x4c, 0x8a, 0xa7, 0xe0, 0xd1, 0x9f, 0x32, 0x93, 0x4d, 0xe0, 0xea, + 0x46, 0x9f, 0xe1, 0x3b, 0x18, 0x49, 0x91, 0x53, 0x15, 0x8c, 0x16, 0xce, 0x72, 0x12, 0x3f, 0x8d, + 0x6e, 0x49, 0x88, 0x98, 0xc8, 0x89, 0x75, 0x08, 0xfc, 0x0c, 0xfe, 0x86, 0x14, 0x4f, 0xb9, 0xe2, + 0x81, 0xa7, 0xd1, 0xaf, 0x07, 0xe8, 0x5e, 0x6c, 0x74, 0xd5, 0xc3, 0xbe, 0x15, 0x4a, 0x36, 0x6c, + 0x37, 0x35, 0xff, 0x04, 0xd3, 0x5b, 0x2d, 0x9c, 0x81, 0xb3, 0xa6, 0xa6, 0xb7, 0xd5, 0x86, 0xad, + 0xaf, 0x2d, 0xcf, 0x6b, 0x32, 0xbe, 0x74, 0xf2, 0xd1, 0xfe, 0x60, 0x85, 0xdf, 0xc1, 0x6d, 0xd5, + 0x20, 0x82, 0x5b, 0xf0, 0x0d, 0xf5, 0x43, 0x3a, 0xc6, 0x73, 0xf0, 0x25, 0x55, 0xa2, 0x96, 0x49, + 0x37, 0x38, 0x89, 0x9f, 0x0f, 0x8d, 0xf4, 0x6d, 0xb6, 0x03, 0x86, 0x31, 0xf8, 0xa6, 0x7a, 0x27, + 0x29, 0x82, 0xab, 0x9a, 0xd2, 0x28, 0xd1, 0x71, 0xf8, 0x05, 0x4e, 0x2e, 0xa9, 0x20, 0xc9, 0x15, + 0x31, 0xfa, 0x5d, 0x53, 0xa5, 0xf0, 0x3d, 0x8c, 0x79, 0xe7, 0x5b, 0x4f, 0x4f, 0xe2, 0xd3, 0xbb, + 0xaf, 0xc2, 0x0c, 0x2c, 0xfc, 0x0a, 0xb3, 0x1b, 0x92, 0xaa, 0x14, 0x45, 0x45, 0x8f, 0x60, 0x79, + 0x0b, 0x27, 0x3f, 0x78, 0x9e, 0xa5, 0x7b, 0x52, 0x76, 0x8f, 0xc2, 0xda, 0x7b, 0x14, 0xed, 0xba, + 0x1b, 0xe0, 0xa3, 0xd7, 0xbd, 0x81, 0x29, 0xa3, 0xad, 0x58, 0x3f, 0xb0, 0x6c, 0x06, 0xc7, 0x06, + 0xd6, 0xad, 0x8a, 0xff, 0x59, 0xe0, 0x5e, 0xd4, 0xea, 0x1a, 0xaf, 0xc0, 0x37, 0xb6, 0xf1, 0xe5, + 0x60, 0xdd, 0xe0, 0xa8, 0xf3, 0x57, 0xf7, 0xf6, 0x3b, 0xd6, 0xf0, 0xa8, 0xa5, 0x33, 0xb6, 0x0e, + 0xe8, 0x06, 0x87, 0x39, 0xa0, 0x1b, 0xde, 0x23, 0x3c, 0xc2, 0x4b, 0xf0, 0x3a, 0xe1, 0xf8, 0xe2, + 0xe0, 0xe9, 0xec, 0xd9, 0x9e, 0x9f, 0xdd, 0xd3, 0x35, 0x44, 0x3f, 0x3d, 0xfd, 0x97, 0xcf, 0xff, + 0x07, 0x00, 0x00, 0xff, 0xff, 0x79, 0x35, 0xb2, 0x7e, 0xec, 0x03, 0x00, 0x00, +} diff --git a/auth/service/proto/auth.pb.micro.go b/auth/service/proto/auth.pb.micro.go new file mode 100644 index 00000000..86ccd75d --- /dev/null +++ b/auth/service/proto/auth.pb.micro.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: auth/service/proto/auth.proto + +package go_micro_auth + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +import ( + context "context" + client "github.com/micro/go-micro/client" + server "github.com/micro/go-micro/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ client.Option +var _ server.Option + +// Client API for Auth service + +type AuthService interface { + Generate(ctx context.Context, in *GenerateRequest, opts ...client.CallOption) (*GenerateResponse, error) + Validate(ctx context.Context, in *ValidateRequest, opts ...client.CallOption) (*ValidateResponse, error) + Revoke(ctx context.Context, in *RevokeRequest, opts ...client.CallOption) (*RevokeResponse, error) +} + +type authService struct { + c client.Client + name string +} + +func NewAuthService(name string, c client.Client) AuthService { + if c == nil { + c = client.NewClient() + } + if len(name) == 0 { + name = "go.micro.auth" + } + return &authService{ + c: c, + name: name, + } +} + +func (c *authService) Generate(ctx context.Context, in *GenerateRequest, opts ...client.CallOption) (*GenerateResponse, error) { + req := c.c.NewRequest(c.name, "Auth.Generate", in) + out := new(GenerateResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authService) Validate(ctx context.Context, in *ValidateRequest, opts ...client.CallOption) (*ValidateResponse, error) { + req := c.c.NewRequest(c.name, "Auth.Validate", in) + out := new(ValidateResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authService) Revoke(ctx context.Context, in *RevokeRequest, opts ...client.CallOption) (*RevokeResponse, error) { + req := c.c.NewRequest(c.name, "Auth.Revoke", in) + out := new(RevokeResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Auth service + +type AuthHandler interface { + Generate(context.Context, *GenerateRequest, *GenerateResponse) error + Validate(context.Context, *ValidateRequest, *ValidateResponse) error + Revoke(context.Context, *RevokeRequest, *RevokeResponse) error +} + +func RegisterAuthHandler(s server.Server, hdlr AuthHandler, opts ...server.HandlerOption) error { + type auth interface { + Generate(ctx context.Context, in *GenerateRequest, out *GenerateResponse) error + Validate(ctx context.Context, in *ValidateRequest, out *ValidateResponse) error + Revoke(ctx context.Context, in *RevokeRequest, out *RevokeResponse) error + } + type Auth struct { + auth + } + h := &authHandler{hdlr} + return s.Handle(s.NewHandler(&Auth{h}, opts...)) +} + +type authHandler struct { + AuthHandler +} + +func (h *authHandler) Generate(ctx context.Context, in *GenerateRequest, out *GenerateResponse) error { + return h.AuthHandler.Generate(ctx, in, out) +} + +func (h *authHandler) Validate(ctx context.Context, in *ValidateRequest, out *ValidateResponse) error { + return h.AuthHandler.Validate(ctx, in, out) +} + +func (h *authHandler) Revoke(ctx context.Context, in *RevokeRequest, out *RevokeResponse) error { + return h.AuthHandler.Revoke(ctx, in, out) +} diff --git a/auth/service/proto/auth.proto b/auth/service/proto/auth.proto new file mode 100644 index 00000000..03f8ca76 --- /dev/null +++ b/auth/service/proto/auth.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +package go.micro.auth; + +service Auth { + rpc Generate(GenerateRequest) returns (GenerateResponse) {}; + rpc Validate(ValidateRequest) returns (ValidateResponse) {}; + rpc Revoke(RevokeRequest) returns (RevokeResponse) {}; +} + +message Account{ + string id = 1; + string token = 2; + int64 created = 3; + int64 expiry = 4; + repeated Role roles = 5; + map metadata = 6; +} + +message Role { + string name = 1; + Resource resource = 2; +} + +message Resource{ + string name = 1; + string type = 2; +} + +message GenerateRequest { + Account account = 1; +} + +message GenerateResponse { + Account account = 1; +} + +message ValidateRequest { + string token = 1; +} + +message ValidateResponse { + Account account = 1; +} + +message RevokeRequest { + string token = 1; +} + +message RevokeResponse {} diff --git a/auth/service/service.go b/auth/service/service.go new file mode 100644 index 00000000..58fb572f --- /dev/null +++ b/auth/service/service.go @@ -0,0 +1,128 @@ +package service + +import ( + "context" + "time" + + "github.com/micro/go-micro/client" + "github.com/micro/go-micro/v2/auth" + pb "github.com/micro/go-micro/v2/auth/service/proto" +) + +// NewAuth returns a new instance of the Auth service +func NewAuth(opts ...auth.Option) auth.Auth { + svc := new(svc) + svc.Init(opts...) + return svc +} + +// svc is the service implementation of the Auth interface +type svc struct { + options auth.Options + auth pb.AuthService +} + +func (s *svc) String() string { + return "service" +} + +func (s *svc) Init(opts ...auth.Option) error { + for _, o := range opts { + o(&s.options) + } + + dc := client.DefaultClient + s.auth = pb.NewAuthService("go.micro.auth", dc) + + return nil +} + +// Generate a new auth account +func (s *svc) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) { + // construct the request + options := auth.NewGenerateOptions(opts...) + sa := &auth.Account{ + Id: id, + Roles: options.Roles, + Metadata: options.Metadata, + } + req := &pb.GenerateRequest{Account: serializeAccount(sa)} + + // execute the request + resp, err := s.auth.Generate(context.Background(), req) + if err != nil { + return nil, err + } + + // format the response + return deserializeAccount(resp.Account), nil +} + +// Revoke an authorization account +func (s *svc) Revoke(token string) error { + // contruct the request + req := &pb.RevokeRequest{Token: token} + + // execute the request + _, err := s.auth.Revoke(context.Background(), req) + return err +} + +// Validate an account token +func (s *svc) Validate(token string) (*auth.Account, error) { + resp, err := s.auth.Validate(context.Background(), &pb.ValidateRequest{Token: token}) + if err != nil { + return nil, err + } + + return deserializeAccount(resp.Account), nil +} + +func serializeAccount(sa *auth.Account) *pb.Account { + roles := make([]*pb.Role, len(sa.Roles)) + for i, r := range sa.Roles { + roles[i] = &pb.Role{ + Name: r.Name, + } + + if r.Resource != nil { + roles[i].Resource = &pb.Resource{ + Name: r.Resource.Name, + Type: r.Resource.Type, + } + } + } + + return &pb.Account{ + Id: sa.Id, + Roles: roles, + Metadata: sa.Metadata, + } +} + +func deserializeAccount(a *pb.Account) *auth.Account { + // format the response + sa := &auth.Account{ + Id: a.Id, + Token: a.Token, + Created: time.Unix(a.Created, 0), + Expiry: time.Unix(a.Expiry, 0), + Metadata: a.Metadata, + } + + sa.Roles = make([]*auth.Role, len(a.Roles)) + for i, r := range a.Roles { + sa.Roles[i] = &auth.Role{ + Name: r.Name, + } + + if r.Resource != nil { + sa.Roles[i].Resource = &auth.Resource{ + Name: r.Resource.Name, + Type: r.Resource.Type, + } + } + } + + return sa +} diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 6bbaacd4..04fa0d23 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/broker" "github.com/micro/go-micro/v2/client" "github.com/micro/go-micro/v2/client/selector" @@ -55,6 +56,10 @@ import ( // tracers // jTracer "github.com/micro/go-micro/v2/debug/trace/jaeger" memTracer "github.com/micro/go-micro/v2/debug/trace/memory" + + // auth + jwtAuth "github.com/micro/go-micro/v2/auth/jwt" + sAuth "github.com/micro/go-micro/v2/auth/service" ) type Cmd interface { @@ -223,6 +228,21 @@ var ( EnvVars: []string{"MICRO_TRACER_ADDRESS"}, Usage: "Comma-separated list of tracer addresses", }, + &cli.StringFlag{ + Name: "auth", + EnvVars: []string{"MICRO_AUTH"}, + Usage: "Auth for role based access control, e.g. service", + }, + &cli.StringFlag{ + Name: "auth_public_key", + EnvVars: []string{"MICRO_AUTH_PUBLIC_KEY"}, + Usage: "Public key for JWT auth (base64 encoded PEM)", + }, + &cli.StringFlag{ + Name: "auth_private_key", + EnvVars: []string{"MICRO_AUTH_PRIVATE_KEY"}, + Usage: "Private key for JWT auth (base64 encoded PEM)", + }, } DefaultBrokers = map[string]func(...broker.Option) broker.Broker{ @@ -274,6 +294,11 @@ var ( // "jaeger": jTracer.NewTracer, } + DefaultAuths = map[string]func(...auth.Option) auth.Auth{ + "service": sAuth.NewAuth, + "jwt": jwtAuth.NewAuth, + } + // used for default selection as the fall back defaultClient = "grpc" defaultServer = "grpc" @@ -300,6 +325,7 @@ func newCmd(opts ...Option) Cmd { Runtime: &runtime.DefaultRuntime, Store: &store.DefaultStore, Tracer: &trace.DefaultTracer, + Auth: &auth.DefaultAuth, Brokers: DefaultBrokers, Clients: DefaultClients, @@ -310,6 +336,7 @@ func newCmd(opts ...Option) Cmd { Runtimes: DefaultRuntimes, Stores: DefaultStores, Tracers: DefaultTracers, + Auths: DefaultAuths, } for _, o := range opts { @@ -382,6 +409,16 @@ func (c *cmd) Before(ctx *cli.Context) error { *c.opts.Tracer = r() } + // Set the auth + if name := ctx.String("auth"); len(name) > 0 { + r, ok := c.opts.Auths[name] + if !ok { + return fmt.Errorf("Unsupported auth: %s", name) + } + + *c.opts.Auth = r() + } + // Set the client if name := ctx.String("client"); len(name) > 0 { // only change if we have the client and type differs @@ -531,6 +568,18 @@ func (c *cmd) Before(ctx *cli.Context) error { serverOpts = append(serverOpts, server.RegisterInterval(val*time.Second)) } + if len(ctx.String("auth_public_key")) > 0 { + if err := (*c.opts.Auth).Init(auth.PublicKey(ctx.String("auth_public_key"))); err != nil { + log.Fatalf("Error configuring auth: %v", err) + } + } + + if len(ctx.String("auth_private_key")) > 0 { + if err := (*c.opts.Auth).Init(auth.PrivateKey(ctx.String("auth_private_key"))); err != nil { + log.Fatalf("Error configuring auth: %v", err) + } + } + // client opts if r := ctx.Int("client_retries"); r >= 0 { clientOpts = append(clientOpts, client.Retries(r)) diff --git a/config/cmd/options.go b/config/cmd/options.go index 695e8ccf..cf387228 100644 --- a/config/cmd/options.go +++ b/config/cmd/options.go @@ -3,6 +3,7 @@ package cmd import ( "context" + "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/broker" "github.com/micro/go-micro/v2/client" "github.com/micro/go-micro/v2/client/selector" @@ -30,6 +31,7 @@ type Options struct { Runtime *runtime.Runtime Store *store.Store Tracer *trace.Tracer + Auth *auth.Auth Brokers map[string]func(...broker.Option) broker.Broker Clients map[string]func(...client.Option) client.Client @@ -40,6 +42,7 @@ type Options struct { Runtimes map[string]func(...runtime.Option) runtime.Runtime Stores map[string]func(...store.Option) store.Store Tracers map[string]func(...trace.Option) trace.Tracer + Auths map[string]func(...auth.Option) auth.Auth // Other options for implementations of the interface // can be stored in a context @@ -109,6 +112,12 @@ func Tracer(t *trace.Tracer) Option { } } +func Auth(a *auth.Auth) Option { + return func(o *Options) { + o.Auth = a + } +} + // New broker func func NewBroker(name string, b func(...broker.Option) broker.Broker) Option { return func(o *Options) { @@ -164,3 +173,10 @@ func NewTracer(name string, t func(...trace.Option) trace.Tracer) Option { o.Tracers[name] = t } } + +// New auth func +func NewAuth(name string, t func(...auth.Option) auth.Auth) Option { + return func(o *Options) { + o.Auths[name] = t + } +} diff --git a/go.mod b/go.mod index eb869453..0b8d74dd 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/bitly/go-simplejson v0.5.0 github.com/bwmarrin/discordgo v0.20.2 github.com/coreos/etcd v3.3.18+incompatible + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/forestgiant/sliceutil v0.0.0-20160425183142-94783f95db6c github.com/fsnotify/fsnotify v1.4.7 github.com/fsouza/go-dockerclient v1.6.0 diff --git a/options.go b/options.go index d616115e..22207223 100644 --- a/options.go +++ b/options.go @@ -5,6 +5,7 @@ import ( "time" "github.com/micro/cli/v2" + "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/broker" "github.com/micro/go-micro/v2/client" "github.com/micro/go-micro/v2/client/selector" @@ -120,6 +121,13 @@ func Tracer(t trace.Tracer) Option { } } +// Auth sets the auth for the service +func Auth(a auth.Auth) Option { + return func(o *Options) { + o.Server.Init(server.Auth(a)) + } +} + // Selector sets the selector for the service client func Selector(s selector.Selector) Option { return func(o *Options) { diff --git a/server/options.go b/server/options.go index d7b03d4b..53424548 100644 --- a/server/options.go +++ b/server/options.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/broker" "github.com/micro/go-micro/v2/codec" "github.com/micro/go-micro/v2/debug/trace" @@ -17,6 +18,7 @@ type Options struct { Broker broker.Broker Registry registry.Registry Tracer trace.Tracer + Auth auth.Auth Transport transport.Transport Metadata map[string]string Name string @@ -161,6 +163,13 @@ func Tracer(t trace.Tracer) Option { } } +// Auth mechanism for role based access control +func Auth(a auth.Auth) Option { + return func(o *Options) { + o.Auth = a + } +} + // Transport mechanism for communication e.g http, rabbitmq, etc func Transport(t transport.Transport) Option { return func(o *Options) { diff --git a/store/memory/memory.go b/store/memory/memory.go index 97e6bd84..00ce68b4 100644 --- a/store/memory/memory.go +++ b/store/memory/memory.go @@ -68,19 +68,26 @@ func (m *memoryStore) Read(key string, opts ...store.ReadOption) ([]*store.Recor var vals []*memoryRecord - if !options.Prefix { - v, ok := m.values[key] - if !ok { - return nil, store.ErrNotFound - } - vals = []*memoryRecord{v} - } else { + if options.Prefix { for _, v := range m.values { if !strings.HasPrefix(v.r.Key, key) { continue } vals = append(vals, v) } + } else if options.Suffix { + for _, v := range m.values { + if !strings.HasSuffix(v.r.Key, key) { + continue + } + vals = append(vals, v) + } + } else { + v, ok := m.values[key] + if !ok { + return nil, store.ErrNotFound + } + vals = []*memoryRecord{v} } //nolint:prealloc diff --git a/store/options.go b/store/options.go index a319cf58..f1ed4f52 100644 --- a/store/options.go +++ b/store/options.go @@ -11,6 +11,8 @@ type Options struct { Namespace string // Prefix of the keys used Prefix string + // Suffix of the keys used + Suffix string // Alternative options Context context.Context } @@ -45,3 +47,10 @@ func ReadPrefix() ReadOption { o.Prefix = true } } + +// ReadSuffix uses the key as a suffix +func ReadSuffix() ReadOption { + return func(o *ReadOptions) { + o.Suffix = true + } +} diff --git a/store/store.go b/store/store.go index 2e7d73ca..cf21ddee 100644 --- a/store/store.go +++ b/store/store.go @@ -39,6 +39,8 @@ type Record struct { type ReadOptions struct { // Read key as a prefix Prefix bool + // Read key as a suffix + Suffix bool } type ReadOption func(o *ReadOptions)