From 669364985e7b96f350dc489b3f30b513c7c0b569 Mon Sep 17 00:00:00 2001 From: Ben Toogood Date: Wed, 29 Apr 2020 09:21:17 +0100 Subject: [PATCH 1/5] JWT auth implementation --- auth/jwt/jwt.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++ config/cmd/cmd.go | 2 ++ 2 files changed, 84 insertions(+) create mode 100644 auth/jwt/jwt.go diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go new file mode 100644 index 00000000..9eb7b864 --- /dev/null +++ b/auth/jwt/jwt.go @@ -0,0 +1,82 @@ +package jwt + +import ( + "errors" + + "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 +} + +// jwt is the service implementation of the Auth interface +type jwt struct { + options auth.Options + jwt token.Provider +} + +func (j *jwt) String() string { + return "jwt" +} + +func (j *jwt) Init(opts ...auth.Option) { + for _, o := range opts { + o(&j.options) + } + + j.jwt = jwtToken.NewTokenProvider( + token.WithPrivateKey(j.options.PublicKey), + token.WithPublicKey(j.options.PublicKey), + ) +} + +func (j *jwt) Options() auth.Options { + return j.options +} + +func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) { + return nil, errors.New("JWT does not support Generate, use the Token method") +} + +func (j *jwt) Grant(role string, res *auth.Resource) error { + return errors.New("JWT does not support Grant") +} + +func (j *jwt) Revoke(role string, res *auth.Resource) error { + return errors.New("JWT does not support Revoke") +} + +func (j *jwt) Verify(acc *auth.Account, res *auth.Resource) error { + if acc == nil { + return auth.ErrForbidden + } + return nil +} + +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...) + account := &auth.Account{ + ID: options.ID, + } + + 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, + }, 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{ From 7e27c97c6cae8f966aba25bba567476d2459dc49 Mon Sep 17 00:00:00 2001 From: Ben Toogood Date: Wed, 29 Apr 2020 09:22:15 +0100 Subject: [PATCH 2/5] Remove Comment --- auth/jwt/jwt.go | 1 - 1 file changed, 1 deletion(-) diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go index 9eb7b864..3854391d 100644 --- a/auth/jwt/jwt.go +++ b/auth/jwt/jwt.go @@ -15,7 +15,6 @@ func NewAuth(opts ...auth.Option) auth.Auth { return j } -// jwt is the service implementation of the Auth interface type jwt struct { options auth.Options jwt token.Provider From 0ed66d066461518e77f1b46280f55e6e11bda261 Mon Sep 17 00:00:00 2001 From: Ben Toogood Date: Wed, 29 Apr 2020 09:38:39 +0100 Subject: [PATCH 3/5] Fix Typo --- auth/jwt/jwt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go index 3854391d..59cd3cdb 100644 --- a/auth/jwt/jwt.go +++ b/auth/jwt/jwt.go @@ -30,7 +30,7 @@ func (j *jwt) Init(opts ...auth.Option) { } j.jwt = jwtToken.NewTokenProvider( - token.WithPrivateKey(j.options.PublicKey), + token.WithPrivateKey(j.options.PrivateKey), token.WithPublicKey(j.options.PublicKey), ) } From 94971aee77f397382c31598e6cea09494f2be888 Mon Sep 17 00:00:00 2001 From: Ben Toogood Date: Wed, 29 Apr 2020 13:21:51 +0100 Subject: [PATCH 4/5] Complete JWT implementation --- auth/jwt/jwt.go | 126 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go index 59cd3cdb..5907f2bf 100644 --- a/auth/jwt/jwt.go +++ b/auth/jwt/jwt.go @@ -1,7 +1,7 @@ package jwt import ( - "errors" + "sync" "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/auth/token" @@ -15,9 +15,17 @@ func NewAuth(opts ...auth.Option) auth.Auth { 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 { @@ -25,10 +33,17 @@ func (j *jwt) String() string { } 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), @@ -36,26 +51,112 @@ func (j *jwt) Init(opts ...auth.Option) { } 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) { - return nil, errors.New("JWT does not support Generate, use the Token method") + 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 { - return errors.New("JWT does not support Grant") + 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 { - return errors.New("JWT does not support Revoke") + 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 { - if acc == nil { - return auth.ErrForbidden + j.Lock() + if len(res.Namespace) == 0 { + res.Namespace = j.options.Namespace } - return nil + 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) { @@ -64,8 +165,15 @@ func (j *jwt) Inspect(token string) (*auth.Account, error) { func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) { options := auth.NewTokenOptions(opts...) - account := &auth.Account{ - ID: options.ID, + + 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)) From 70736e24c04a7c7906e53dee356700450e85ad6e Mon Sep 17 00:00:00 2001 From: Ben Toogood Date: Wed, 29 Apr 2020 13:33:22 +0100 Subject: [PATCH 5/5] Set RefreshToken --- auth/jwt/jwt.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go index 5907f2bf..86815bce 100644 --- a/auth/jwt/jwt.go +++ b/auth/jwt/jwt.go @@ -182,8 +182,9 @@ func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) { } return &auth.Token{ - Created: tok.Created, - Expiry: tok.Expiry, - AccessToken: tok.Token, + Created: tok.Created, + Expiry: tok.Expiry, + AccessToken: tok.Token, + RefreshToken: tok.Token, }, nil }