Skip to content

Commit

Permalink
jwt and token package supports scoped tokens (#77)
Browse files Browse the repository at this point in the history
* fix: jwt and token package supports scoped tokens
  • Loading branch information
shaj13 authored Jan 17, 2021
1 parent 175ea3f commit 546e864
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 14 deletions.
41 changes: 41 additions & 0 deletions auth/strategies/jwt/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/http"
"time"

"github.com/shaj13/go-guardian/v2/auth/strategies/token"

"github.com/shaj13/go-guardian/v2/auth"

gojwt "github.com/dgrijalva/jwt-go/v4"
Expand Down Expand Up @@ -63,6 +65,33 @@ func Example() {
// example <nil>
}

func Example_scope() {
opt := token.SetScopes(token.NewScope("read:example", "/example", "GET"))
ns := jwt.SetNamedScopes("read:example")
u := auth.NewUserInfo("example", "example", nil, nil)
c := libcache.LRU.New(0)
s := jwt.StaticSecret{
ID: "id",
Method: gojwt.SigningMethodHS256,
Secret: []byte("your secret"),
}

token, err := jwt.IssueAccessToken(u, s, ns)
strategy := jwt.New(c, s, opt)

fmt.Println(err)

// user request
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer "+token)
_, err = strategy.Authenticate(r.Context(), r)
fmt.Println(err)

// Output:
// <nil>
// strategies/token: The access token scopes do not grant access to the requested resource
}

func ExampleSecretsKeeper() {
// The example shows how to create your custom secrets keeper to rotate secrets.
s := RotatedSecrets{
Expand Down Expand Up @@ -116,3 +145,15 @@ func ExampleSetExpDuration() {
_, _ = jwt.IssueAccessToken(u, s, exp)
_ = jwt.New(c, s, exp)
}

func ExampleSetNamedScopes() {
u := auth.NewUserInfo("example", "example", nil, nil)
ns := jwt.SetNamedScopes("read:example")
// get jwt scope verification option
opt := token.SetScopes(token.NewScope("read:example", "/example", "GET"))
s := jwt.StaticSecret{}
c := libcache.LRU.New(0)

_, _ = jwt.IssueAccessToken(u, s, ns)
_ = jwt.New(c, s, opt)
}
15 changes: 12 additions & 3 deletions auth/strategies/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,26 @@ import (
// The returned function typically used with the token strategy.
func GetAuthenticateFunc(s SecretsKeeper, opts ...auth.Option) token.AuthenticateFunc {
t := newAccessToken(s, opts...)
return func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) {
c, err := t.parse(token)
return func(ctx context.Context, r *http.Request, tk string) (auth.Info, time.Time, error) {
c, err := t.parse(tk)
if err != nil {
return nil, time.Time{}, err
}

if len(c.Scopes) > 0 {
token.WithNamedScopes(c.UserInfo, c.Scopes...)
}
return c.UserInfo, c.ExpiresAt.Time, err
}
}

// New return strategy authenticate request using jwt token.
// New is similar to token.New().
//
// New is similar to:
//
// fn := jwt.GetAuthenticateFunc(secretsKeeper, opts...)
// token.New(fn, cache, opts...)
//
func New(c auth.Cache, s SecretsKeeper, opts ...auth.Option) auth.Strategy {
fn := GetAuthenticateFunc(s, opts...)
return token.New(fn, c, opts...)
Expand Down
9 changes: 9 additions & 0 deletions auth/strategies/jwt/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,12 @@ func SetExpDuration(d time.Duration) auth.Option {
}
})
}

// SetNamedScopes sets the access token scopes,
func SetNamedScopes(scp ...string) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if t, ok := v.(*accessToken); ok {
t.scp = scp
}
})
}
3 changes: 3 additions & 0 deletions auth/strategies/jwt/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func IssueAccessToken(info auth.Info, s SecretsKeeper, opts ...auth.Option) (str

type claims struct {
UserInfo auth.Info `json:"info"`
Scopes []string `json:"scp"`
jwt.StandardClaims
}

Expand All @@ -30,6 +31,7 @@ type accessToken struct {
d time.Duration
aud jwt.ClaimStrings
iss string
scp []string
}

func (at accessToken) issue(info auth.Info) (string, error) {
Expand All @@ -44,6 +46,7 @@ func (at accessToken) issue(info auth.Info) (string, error) {

c := claims{
UserInfo: info,
Scopes: at.scp,
StandardClaims: jwt.StandardClaims{
Subject: info.GetUserName(),
Issuer: at.iss,
Expand Down
32 changes: 21 additions & 11 deletions auth/strategies/token/cached.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import (
type AuthenticateFunc func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error)

// New return new token strategy that caches the invocation result of authenticate function.
func New(auth AuthenticateFunc, c auth.Cache, opts ...auth.Option) auth.Strategy {
func New(fn AuthenticateFunc, c auth.Cache, opts ...auth.Option) auth.Strategy {
cached := &cachedToken{
authFunc: auth,
cache: c,
typ: Bearer,
parser: AuthorizationParser(string(Bearer)),
authFunc: fn,
verify: func(_ context.Context, _ *http.Request, _ auth.Info, _ string) error {
return nil
},
cache: c,
typ: Bearer,
parser: AuthorizationParser(string(Bearer)),
}

for _, opt := range opts {
Expand All @@ -31,6 +34,7 @@ func New(auth AuthenticateFunc, c auth.Cache, opts ...auth.Option) auth.Strategy

type cachedToken struct {
parser Parser
verify verify
typ Type
cache auth.Cache
authFunc AuthenticateFunc
Expand All @@ -42,23 +46,29 @@ func (c *cachedToken) Authenticate(ctx context.Context, r *http.Request) (auth.I
return nil, err
}

info, ok := c.cache.Load(token)
i, ok := c.cache.Load(token)

// if token not found invoke user authenticate function
if !ok {
var t time.Time
info, t, err = c.authFunc(ctx, r, token)
i, t, err = c.authFunc(ctx, r, token)
if err != nil {
return nil, err
}
c.cache.StoreWithTTL(token, info, time.Until(t))
c.cache.StoreWithTTL(token, i, time.Until(t))
}

if _, ok := info.(auth.Info); !ok {
return nil, auth.NewTypeError("strategies/token:", (*auth.Info)(nil), info)
info, ok := i.(auth.Info)

if !ok {
return nil, auth.NewTypeError("strategies/token:", (*auth.Info)(nil), i)
}

if err := c.verify(ctx, r, info, token); err != nil {
return nil, err
}

return info.(auth.Info), nil
return info, nil
}

func (c *cachedToken) Append(token interface{}, info auth.Info) error {
Expand Down
26 changes: 26 additions & 0 deletions auth/strategies/token/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ import (
"github.com/shaj13/go-guardian/v2/auth"
)

func Example() {
authFunc := AuthenticateFunc(func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) {
if token == "90d64460d14870c08c81352a05dedd3465940a7" {
// hit DB or authorization server to retrieve information about the access token.
info := auth.NewDefaultUser("example", "1", nil, nil)
exp := time.Now().Add(time.Hour)
WithNamedScopes(info, "example:read")
return info, exp, nil
}
return nil, time.Time{}, fmt.Errorf("Invalid user token")
})

cache := libcache.LRU.New(0)
scope := NewScope("example:read", "/example", "GET")
opt := SetScopes(scope)
strategy := New(authFunc, cache, opt)

r, _ := http.NewRequest("PUT", "/eample", nil)
r.Header.Set("Authorization", "Bearer 90d64460d14870c08c81352a05dedd3465940a7")

_, err := strategy.Authenticate(r.Context(), r)
fmt.Println(err)
// Output:
// strategies/token: The access token scopes do not grant access to the requested resource
}

func ExampleNewStaticFromFile() {
strategy, _ := NewStaticFromFile("testdata/valid.csv")
r, _ := http.NewRequest("GET", "/", nil)
Expand Down
108 changes: 108 additions & 0 deletions auth/strategies/token/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package token

import (
"context"
"net/http"
"regexp"

"github.com/shaj13/go-guardian/v2/auth"
)

const scopesExtName = "x-go-guardian-scopes"

// Scope provide a way to manage permissions to protected resources.
//
// Scope is not an authorization alternative and should be only used to limit the access token.
type Scope interface {
// Name return's scope name.
GetName() string
// Verify is called after the user authenticated to verify the user token,
// grants access to the requested resource/endpoint.
Verify(ctx context.Context, r *http.Request, info auth.Info, token string) (ok bool)
}

// WithNamedScopes add all the provided named scopes to the provided auth.info.
// Typically used when token scopes verification enabled and need to add token scopes to the auth info.
//
// token.WithNamedScopes(info, "read:repo", "read:user")
//
func WithNamedScopes(info auth.Info, scopes ...string) {
ext := auth.Extensions{}

if v := info.GetExtensions(); v != nil {
ext = v
}

ext[scopesExtName] = scopes
info.SetExtensions(ext)
}

// GetNamedScopes return's all named scopes from auth.info.
// Typically used internally when token scopes verification enabled.
func GetNamedScopes(info auth.Info) (scopes []string) {
if info.GetExtensions() == nil {
return
}

return info.GetExtensions()[scopesExtName]
}

// NewScope return's a new scope instance.
// the returned scope verify the request by matching
// the scope endpoint to the request path and
// the scope method to the request method.
//
// The endpoint and method parameters will be passed to regexp.MustCompile
// to get a Regexp object to be used later in verification.
//
// Example:
//
// token.NewScope("admin.write","/admin|/system","POST|PUT")
// token.NewScope("read:repo","/repo","GET")
//
func NewScope(name, endpoint, method string) Scope {
return defaultScope{
name: name,
endpoint: regexp.MustCompile(endpoint),
method: regexp.MustCompile(method),
}
}

func verifyScopes(scps ...Scope) verify {
scopes := make(map[string]Scope)
for _, scope := range scps {
scopes[scope.GetName()] = scope
}

return func(ctx context.Context, r *http.Request, info auth.Info, token string) error {
// the token is not limited to scopes.
if len(GetNamedScopes(info)) == 0 {
return nil
}

for _, name := range GetNamedScopes(info) {
scope, ok := scopes[name]
if ok && scope.Verify(ctx, r, info, token) {
// we have found scope and it match request.
return nil
}
continue
}
// No scope found match the request.
return ErrTokenScopes
}
}

type defaultScope struct {
name string
endpoint *regexp.Regexp
method *regexp.Regexp
}

func (d defaultScope) Verify(ctx context.Context, r *http.Request, info auth.Info, token string) bool {
return d.endpoint.MatchString(r.URL.Path) && d.method.MatchString(r.Method)
}

func (d defaultScope) GetName() string {
return d.name
}
Loading

0 comments on commit 546e864

Please sign in to comment.