diff --git a/auth/options.go b/auth/options.go index 274935b3..90bbc1df 100644 --- a/auth/options.go +++ b/auth/options.go @@ -8,8 +8,12 @@ import ( ) type Options struct { - // Token is an auth token - Token string + // ID is the services auth ID + ID string + // Secret is used to generate new tokens + Secret string + // Token is the services token used to authenticate itself + Token *Token // Public key base64 encoded PublicKey string // Private key base64 encoded @@ -45,10 +49,11 @@ func PrivateKey(key string) Option { } } -// ServiceToken sets an auth token -func ServiceToken(t string) Option { +// Credentials sets the auth credentials +func Credentials(id, secret string) Option { return func(o *Options) { - o.Token = t + o.ID = id + o.Secret = secret } } diff --git a/auth/service/service.go b/auth/service/service.go index 2aade9fc..0cc11d98 100644 --- a/auth/service/service.go +++ b/auth/service/service.go @@ -55,13 +55,13 @@ func (s *svc) Init(opts ...auth.Option) { } // load rules periodically from the auth service - timer := time.NewTicker(time.Second * 30) + ruleTimer := time.NewTicker(time.Second * 30) go func() { // load rules immediately on startup s.loadRules() for { - <-timer.C + <-ruleTimer.C // jitter for up to 5 seconds, this stops // all the services calling the auth service @@ -70,9 +70,39 @@ func (s *svc) Init(opts ...auth.Option) { s.loadRules() } }() + + // we have client credentials and must load a new token + // periodically + if len(s.options.ID) > 0 || len(s.options.Secret) > 0 { + tokenTimer := time.NewTicker(time.Minute) + + go func() { + s.loadToken() + + for { + <-tokenTimer.C + + // Do not get a new token if the current one has more than three + // minutes remaining. We do 3 minutes to allow multiple retires in + // the case one request fails + t := s.Options().Token + if t != nil && t.Expiry.Unix() > time.Now().Add(time.Minute*3).Unix() { + continue + } + + // jitter for up to 5 seconds, this stops + // all the services calling the auth service + // at the exact same time + time.Sleep(jitter.Do(time.Second * 5)) + s.loadToken() + } + }() + } } func (s *svc) Options() auth.Options { + s.Lock() + defer s.Unlock() return s.options } @@ -256,6 +286,24 @@ func (s *svc) loadRules() { s.rules = rsp.Rules } +// loadToken generates a new token for the service to use when making calls +func (s *svc) loadToken() { + rsp, err := s.auth.Token(context.TODO(), &pb.TokenRequest{ + Id: s.Options().ID, + Secret: s.Options().Secret, + TokenExpiry: int64((time.Minute * 15).Seconds()), + }) + s.Lock() + defer s.Unlock() + + if err != nil { + log.Errorf("Error generating token: %v", err) + return + } + + s.options.Token = serializeToken(rsp.Token) +} + func serializeToken(t *pb.Token) *auth.Token { return &auth.Token{ Token: t.Token, diff --git a/client/grpc/grpc.go b/client/grpc/grpc.go index 0357abc3..5f14c25d 100644 --- a/client/grpc/grpc.go +++ b/client/grpc/grpc.go @@ -131,9 +131,20 @@ func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.R // set the content type for the request header["x-content-type"] = req.ContentType() - // set the authorization token if one is saved locally + // if the caller specifies using service token or no token + // was passed with the request, set the service token + var srvToken string + if g.opts.Auth != nil && g.opts.Auth.Options().Token != nil { + srvToken = g.opts.Auth.Options().Token.Token + } + if (opts.ServiceToken || len(header["authorization"]) == 0) && len(srvToken) > 0 { + header["authorization"] = auth.BearerScheme + srvToken + } + + // fall back to using the authorization token set in config, + // this enables the CLI to provide a token if len(header["authorization"]) == 0 { - if token, err := config.Get("token"); err == nil && len(token) > 0 { + if token, err := config.Get("micro", "auth", "token"); err == nil && len(token) > 0 { header["authorization"] = auth.BearerScheme + token } } diff --git a/client/options.go b/client/options.go index 2d201510..378957ed 100644 --- a/client/options.go +++ b/client/options.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/broker" "github.com/micro/go-micro/v2/client/selector" "github.com/micro/go-micro/v2/codec" @@ -16,6 +17,7 @@ type Options struct { ContentType string // Plugged interfaces + Auth auth.Auth Broker broker.Broker Codecs map[string]codec.NewCodec Registry registry.Registry @@ -55,6 +57,8 @@ type CallOptions struct { Retries int // Request/Response timeout RequestTimeout time.Duration + // Use the services own auth token + ServiceToken bool // Middleware for low level call func CallWrappers []CallWrapper @@ -99,6 +103,7 @@ func NewOptions(options ...Option) Options { }, PoolSize: DefaultPoolSize, PoolTTL: DefaultPoolTTL, + Auth: auth.DefaultAuth, Broker: broker.DefaultBroker, Selector: selector.DefaultSelector, Registry: registry.DefaultRegistry, @@ -119,6 +124,13 @@ func Broker(b broker.Broker) Option { } } +// Auth to be used when making a request +func Auth(a auth.Auth) Option { + return func(o *Options) { + o.Auth = a + } +} + // Codec to be used to encode/decode requests for a given content type func Codec(contentType string, c codec.NewCodec) Option { return func(o *Options) { @@ -291,6 +303,14 @@ func WithDialTimeout(d time.Duration) CallOption { } } +// WithServiceToken is a CallOption which overrides the +// authorization header with the services own auth token +func WithServiceToken() CallOption { + return func(o *CallOptions) { + o.ServiceToken = true + } +} + func WithMessageContentType(ct string) MessageOption { return func(o *MessageOptions) { o.ContentType = ct diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 287c085e..0af37fdd 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -255,9 +255,14 @@ var ( Usage: "Auth for role based access control, e.g. service", }, &cli.StringFlag{ - Name: "auth_token", - EnvVars: []string{"MICRO_AUTH_TOKEN"}, - Usage: "Auth token used for client authentication", + Name: "auth_id", + EnvVars: []string{"MICRO_AUTH_ID"}, + Usage: "Account ID used for client authentication", + }, + &cli.StringFlag{ + Name: "auth_secret", + EnvVars: []string{"MICRO_AUTH_SECRET"}, + Usage: "Account secret used for client authentication", }, &cli.StringFlag{ Name: "auth_public_key", @@ -488,6 +493,7 @@ func (c *cmd) Before(ctx *cli.Context) error { } *c.opts.Auth = a() + clientOpts = append(clientOpts, client.Auth(*c.opts.Auth)) } // Set the profile @@ -655,8 +661,10 @@ func (c *cmd) Before(ctx *cli.Context) error { } } - if len(ctx.String("auth_token")) > 0 { - authOpts = append(authOpts, auth.ServiceToken(ctx.String("auth_token"))) + if len(ctx.String("auth_id")) > 0 || len(ctx.String("auth_secret")) > 0 { + authOpts = append(authOpts, auth.Credentials( + ctx.String("auth_id"), ctx.String("auth_secret"), + )) } if len(ctx.String("auth_public_key")) > 0 { diff --git a/options.go b/options.go index 36e290a3..1ef8b955 100644 --- a/options.go +++ b/options.go @@ -142,6 +142,7 @@ func Tracer(t trace.Tracer) Option { func Auth(a auth.Auth) Option { return func(o *Options) { o.Auth = a + o.Client.Init(client.Auth(a)) o.Server.Init(server.Auth(a)) } } diff --git a/service.go b/service.go index a693aa34..a13b4e19 100644 --- a/service.go +++ b/service.go @@ -18,7 +18,6 @@ import ( "github.com/micro/go-micro/v2/plugin" "github.com/micro/go-micro/v2/server" "github.com/micro/go-micro/v2/store" - "github.com/micro/go-micro/v2/util/config" "github.com/micro/go-micro/v2/util/wrapper" ) @@ -117,9 +116,9 @@ func (s *service) Init(opts ...Option) { // Right now we're just going to load a token // May need to re-read value on change // TODO: should be scoped to micro/auth/token - if tk, _ := config.Get("token"); len(tk) > 0 { - s.opts.Auth.Init(auth.ServiceToken(tk)) - } + // if tk, _ := config.Get("token"); len(tk) > 0 { + // s.opts.Auth.Init(auth.ServiceToken(tk)) + // } }) } diff --git a/util/config/config.go b/util/config/config.go index e0e85e37..a15eade7 100644 --- a/util/config/config.go +++ b/util/config/config.go @@ -21,13 +21,13 @@ const FileName = ".micro" var config = newConfig() // Get a value from the .micro file -func Get(key string) (string, error) { - tk := config.Get(key).String("") +func Get(path ...string) (string, error) { + tk := config.Get(path...).String("") return strings.TrimSpace(tk), nil } // Set a value in the .micro file -func Set(key, value string) error { +func Set(value string, path ...string) error { // get the filepath fp, err := filePath() if err != nil { @@ -35,7 +35,7 @@ func Set(key, value string) error { } // set the value - config.Set(value, key) + config.Set(value, path...) // write to the file return ioutil.WriteFile(fp, config.Bytes(), 0644) diff --git a/util/wrapper/wrapper.go b/util/wrapper/wrapper.go index d28155c1..f795e705 100644 --- a/util/wrapper/wrapper.go +++ b/util/wrapper/wrapper.go @@ -38,15 +38,6 @@ func (c *clientWrapper) setHeaders(ctx context.Context) context.Context { mda, _ := metadata.FromContext(ctx) md := metadata.Copy(mda) - // get auth token - if a := c.auth(); a != nil { - tk := a.Options().Token - // if the token if exists and auth header isn't set then set it - if len(tk) > 0 && len(md["Authorization"]) == 0 { - md["Authorization"] = auth.BearerScheme + tk - } - } - // set headers for k, v := range c.headers { if _, ok := md[k]; !ok {