diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go new file mode 100644 index 00000000..86815bce --- /dev/null +++ b/auth/jwt/jwt.go @@ -0,0 +1,190 @@ +package jwt + +import ( + "sync" + + "github.com/micro/go-micro/v2/auth" + "github.com/micro/go-micro/v2/auth/token" + jwtToken "github.com/micro/go-micro/v2/auth/token/jwt" +) + +// NewAuth returns a new instance of the Auth service +func NewAuth(opts ...auth.Option) auth.Auth { + j := new(jwt) + j.Init(opts...) + return j +} + +type rule struct { + role string + resource *auth.Resource +} + +type jwt struct { + options auth.Options + jwt token.Provider + rules []*rule + + sync.Mutex +} + +func (j *jwt) String() string { + return "jwt" +} + +func (j *jwt) Init(opts ...auth.Option) { + j.Lock() + defer j.Unlock() + + for _, o := range opts { + o(&j.options) + } + + if len(j.options.Namespace) == 0 { + j.options.Namespace = auth.DefaultNamespace + } + + j.jwt = jwtToken.NewTokenProvider( + token.WithPrivateKey(j.options.PrivateKey), + token.WithPublicKey(j.options.PublicKey), + ) +} + +func (j *jwt) Options() auth.Options { + j.Lock() + defer j.Unlock() + return j.options +} + +func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) { + options := auth.NewGenerateOptions(opts...) + account := &auth.Account{ + ID: id, + Type: options.Type, + Roles: options.Roles, + Provider: options.Provider, + Metadata: options.Metadata, + Namespace: options.Namespace, + } + + // generate a JWT secret which can be provided to the Token() method + // and exchanged for an access token + secret, err := j.jwt.Generate(account) + if err != nil { + return nil, err + } + account.Secret = secret.Token + + // return the account + return account, nil +} + +func (j *jwt) Grant(role string, res *auth.Resource) error { + j.Lock() + defer j.Unlock() + j.rules = append(j.rules, &rule{role, res}) + return nil +} + +func (j *jwt) Revoke(role string, res *auth.Resource) error { + j.Lock() + defer j.Unlock() + + rules := make([]*rule, 0, len(j.rules)) + + var ruleFound bool + for _, r := range rules { + if r.role == role && r.resource == res { + ruleFound = true + } else { + rules = append(rules, r) + } + } + + if !ruleFound { + return auth.ErrNotFound + } + + j.rules = rules + return nil +} + +func (j *jwt) Verify(acc *auth.Account, res *auth.Resource) error { + j.Lock() + if len(res.Namespace) == 0 { + res.Namespace = j.options.Namespace + } + rules := j.rules + j.Unlock() + + for _, rule := range rules { + // validate the rule applies to the requested resource + if rule.resource.Namespace != "*" && rule.resource.Namespace != res.Namespace { + continue + } + if rule.resource.Type != "*" && rule.resource.Type != res.Type { + continue + } + if rule.resource.Name != "*" && rule.resource.Name != res.Name { + continue + } + if rule.resource.Endpoint != "*" && rule.resource.Endpoint != res.Endpoint { + continue + } + + // a blank role indicates anyone can access the resource, even without an account + if rule.role == "" { + return nil + } + + // all furter checks require an account + if acc == nil { + continue + } + + // this rule allows any account access, allow the request + if rule.role == "*" { + return nil + } + + // if the account has the necessary role, allow the request + for _, r := range acc.Roles { + if r == rule.role { + return nil + } + } + } + + // no rules matched, forbid the request + return auth.ErrForbidden +} + +func (j *jwt) Inspect(token string) (*auth.Account, error) { + return j.jwt.Inspect(token) +} + +func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) { + options := auth.NewTokenOptions(opts...) + + secret := options.RefreshToken + if len(options.Secret) > 0 { + secret = options.Secret + } + + account, err := j.jwt.Inspect(secret) + if err != nil { + return nil, err + } + + tok, err := j.jwt.Generate(account, token.WithExpiry(options.Expiry)) + if err != nil { + return nil, err + } + + return &auth.Token{ + Created: tok.Created, + Expiry: tok.Expiry, + AccessToken: tok.Token, + RefreshToken: tok.Token, + }, nil +} diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index b5cd79f8..7cec8f99 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -70,6 +70,7 @@ import ( memTracer "github.com/micro/go-micro/v2/debug/trace/memory" // auth + jwtAuth "github.com/micro/go-micro/v2/auth/jwt" svcAuth "github.com/micro/go-micro/v2/auth/service" // auth providers @@ -369,6 +370,7 @@ var ( DefaultAuths = map[string]func(...auth.Option) auth.Auth{ "service": svcAuth.NewAuth, + "jwt": jwtAuth.NewAuth, } DefaultAuthProviders = map[string]func(...provider.Option) provider.Provider{