diff --git a/.golangci.yml b/.golangci.yml index 0b684c7..70255e9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,7 +32,7 @@ linters: linters-settings: lll: - line-length: 110 + line-length: 120 goimports: local-prefixes: "github.com/shaj13/go-guardian/v2" issues: diff --git a/README.md b/README.md index 3a07df6..7f5e3b2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ [![Coverage Status](https://coveralls.io/repos/github/shaj13/go-guardian/badge.svg?branch=master)](https://coveralls.io/github/shaj13/go-guardian?branch=master) [![CircleCI](https://circleci.com/gh/shaj13/go-guardian/tree/master.svg?style=svg)](https://circleci.com/gh/shaj13/go-guardian/tree/master) +| :exclamation: Cache package has been moved to [libcache](https://github.com/shaj13/libcache) repository | +|----------------------------------------------------------------------------------------------------------| + # Go-Guardian Go-Guardian is a golang library that provides a simple, clean, and idiomatic way to create powerful modern API and web authentication. @@ -30,7 +33,6 @@ you want to focus on building awesome software. go-guardian is here to help with Here are a few bullet point reasons you might like to try it out: * provides simple, clean, and idiomatic API. * provides top trends and traditional authentication methods. -* provides a package to caches the authentication decisions, based on different mechanisms and algorithms. * provides two-factor authentication and one-time password as defined in [RFC-4226](https://tools.ietf.org/html/rfc4226) and [RFC-6238](https://tools.ietf.org/html/rfc6238) * provides a mechanism to customize strategies, even enables writing a custom strategy diff --git a/auth/strategies/kubernetes/kubernetes.go b/auth/strategies/kubernetes/kubernetes.go index 30c211f..6425e9e 100644 --- a/auth/strategies/kubernetes/kubernetes.go +++ b/auth/strategies/kubernetes/kubernetes.go @@ -12,6 +12,7 @@ import ( "io/ioutil" "net/http" "strings" + "time" kubeauth "k8s.io/api/authentication/v1" kubemeta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,7 +30,9 @@ type kubeReview struct { client *http.Client } -func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token string) (auth.Info, error) { +func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) { + var t time.Time + tr := &kubeauth.TokenReview{ Spec: kubeauth.TokenReviewSpec{ Token: token, @@ -39,7 +42,7 @@ func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token st body, err := json.Marshal(tr) if err != nil { - return nil, fmt.Errorf( + return nil, t, fmt.Errorf( "strategies/kubernetes: Failed to Marshal TokenReview Err: %s", err, ) @@ -49,7 +52,7 @@ func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token st req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { - return nil, err + return nil, t, err } req.Header.Set("Authorization", "Bearer "+k.token) @@ -58,12 +61,12 @@ func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token st resp, err := k.client.Do(req) if err != nil { - return nil, err + return nil, t, err } body, err = ioutil.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, t, err } defer resp.Body.Close() @@ -72,24 +75,24 @@ func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token st status := &kubemeta.Status{} err = json.Unmarshal(body, status) if err == nil && status.Status != kubemeta.StatusSuccess { - return nil, fmt.Errorf("strategies/kubernetes: %s", status.Message) + return nil, t, fmt.Errorf("strategies/kubernetes: %s", status.Message) } tr = &kubeauth.TokenReview{} err = json.Unmarshal(body, tr) if err != nil { - return nil, fmt.Errorf( + return nil, t, fmt.Errorf( "strategies/kubernetes: Failed to Unmarshal Response body to TokenReview Err: %s", err, ) } if len(tr.Status.Error) > 0 { - return nil, fmt.Errorf("strategies/kubernetes: %s", tr.Status.Error) + return nil, t, fmt.Errorf("strategies/kubernetes: %s", tr.Status.Error) } if !tr.Status.Authenticated { - return nil, fmt.Errorf("strategies/kubernetes: Token Unauthorized") + return nil, t, fmt.Errorf("strategies/kubernetes: Token Unauthorized") } user := tr.Status.User @@ -98,7 +101,7 @@ func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token st extensions[k] = v } - return auth.NewUserInfo(user.Username, user.UID, user.Groups, extensions), nil + return auth.NewUserInfo(user.Username, user.UID, user.Groups, extensions), t, nil } // GetAuthenticateFunc return function to authenticate request using kubernetes token review. diff --git a/auth/strategies/kubernetes/kubernetes_test.go b/auth/strategies/kubernetes/kubernetes_test.go index d7e5ef3..d60012c 100644 --- a/auth/strategies/kubernetes/kubernetes_test.go +++ b/auth/strategies/kubernetes/kubernetes_test.go @@ -77,7 +77,7 @@ func TestKubeReview(t *testing.T) { client: srv.Client(), } r, _ := http.NewRequest("", "", nil) - info, err := kr.authenticate(r.Context(), r, "") + info, _, err := kr.authenticate(r.Context(), r, "") assert.Equal(t, tt.err, err) assert.Equal(t, tt.info, info) diff --git a/auth/strategies/token/cached.go b/auth/strategies/token/cached.go index 35fff69..1501f04 100644 --- a/auth/strategies/token/cached.go +++ b/auth/strategies/token/cached.go @@ -3,28 +3,18 @@ package token import ( "context" "net/http" + "time" "github.com/shaj13/go-guardian/v2/auth" ) -// AuthenticateFunc declare custom function to authenticate request using token. -// The authenticate function invoked by Authenticate Strategy method when -// The token does not exist in the cahce and the invocation result will be cached, unless an error returned. -// Use NoOpAuthenticate instead to refresh/mangae token directly using cache or Append function. -type AuthenticateFunc func(ctx context.Context, r *http.Request, token string) (auth.Info, error) +// AuthenticateFunc declare function signature to authenticate request using token. +// Any function that has the appropriate signature can be registered to the token strategy. +// AuthenticateFunc must return authenticated user info and token expiry time, otherwise error. +type AuthenticateFunc func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) -// New return new auth.Strategy. -// The returned strategy, caches the invocation result of authenticate function, See AuthenticateFunc. -// Use NoOpAuthenticate to refresh/mangae token directly using cache or Append function, See NoOpAuthenticate. +// 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 { - if auth == nil { - panic("strategies/token: Authenticate Function required and can't be nil") - } - - if c == nil { - panic("strategies/token: Cache object required and can't be nil") - } - cached := &cachedToken{ authFunc: auth, cache: c, @@ -56,11 +46,12 @@ func (c *cachedToken) Authenticate(ctx context.Context, r *http.Request) (auth.I // if token not found invoke user authenticate function if !ok { - info, err = c.authFunc(ctx, r, token) + var t time.Time + info, t, err = c.authFunc(ctx, r, token) if err != nil { return nil, err } - c.cache.Store(token, info) + c.cache.StoreWithTTL(token, info, time.Until(t)) } if _, ok := info.(auth.Info); !ok { @@ -80,9 +71,9 @@ func (c *cachedToken) Revoke(token interface{}) error { return nil } -// NoOpAuthenticate implements Authenticate function, it return nil, ErrNOOP, +// NoOpAuthenticate implements AuthenticateFunc, it return nil, time.Time{}, ErrNOOP, // commonly used when token refreshed/mangaed directly using cache or Append function, // and there is no need to parse token and authenticate request. -func NoOpAuthenticate(ctx context.Context, r *http.Request, token string) (auth.Info, error) { - return nil, ErrNOOP +func NoOpAuthenticate(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) { + return nil, time.Time{}, ErrNOOP } diff --git a/auth/strategies/token/cached_test.go b/auth/strategies/token/cached_test.go index 986be4e..b1dd11b 100644 --- a/auth/strategies/token/cached_test.go +++ b/auth/strategies/token/cached_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "testing" + "time" "github.com/shaj13/libcache" _ "github.com/shaj13/libcache/lru" @@ -15,7 +16,6 @@ import ( func TestNewCahced(t *testing.T) { table := []struct { name string - panic bool expectedErr bool authFunc AuthenticateFunc info interface{} @@ -25,49 +25,28 @@ func TestNewCahced(t *testing.T) { name: "it return error when user authenticate func return error", expectedErr: true, authFunc: NoOpAuthenticate, - panic: false, info: nil, }, { name: "it return error when cache return invalid type", expectedErr: true, - authFunc: func(_ context.Context, _ *http.Request, _ string) (auth.Info, error) { return nil, nil }, - panic: false, - info: "sample-data", - token: "valid", + authFunc: func(_ context.Context, _ *http.Request, _ string) (auth.Info, time.Time, error) { + return nil, time.Time{}, nil + }, + info: "sample-data", + token: "valid", }, { name: "it return user when token cached", expectedErr: false, authFunc: NoOpAuthenticate, - panic: false, info: auth.NewDefaultUser("1", "1", nil, nil), token: "valid-user", }, - { - name: "it panic when Authenticate func nil", - expectedErr: false, - panic: true, - info: nil, - }, - { - name: "it panic when Cache nil", - expectedErr: false, - authFunc: NoOpAuthenticate, - panic: true, - info: nil, - }, } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - if tt.panic { - assert.Panics(t, func() { - New(tt.authFunc, nil) - }) - return - } - cache := libcache.LRU.New(0) strategy := New(tt.authFunc, cache) r, _ := http.NewRequest("GET", "/", nil) diff --git a/auth/strategies/token/example_test.go b/auth/strategies/token/example_test.go index b7f5fd3..0e37d47 100644 --- a/auth/strategies/token/example_test.go +++ b/auth/strategies/token/example_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "time" "github.com/shaj13/libcache" _ "github.com/shaj13/libcache/lru" @@ -34,12 +35,12 @@ func ExampleNewStatic() { } func ExampleNew() { - authFunc := AuthenticateFunc(func(ctx context.Context, r *http.Request, token string) (auth.Info, error) { + authFunc := AuthenticateFunc(func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) { fmt.Print("authFunc called ") if token == "90d64460d14870c08c81352a05dedd3465940a7" { - return auth.NewDefaultUser("example", "1", nil, nil), nil + return auth.NewDefaultUser("example", "1", nil, nil), time.Now().Add(time.Hour), nil } - return nil, fmt.Errorf("Invalid user token") + return nil, time.Time{}, fmt.Errorf("Invalid user token") }) cache := libcache.LRU.New(0) @@ -138,11 +139,11 @@ func ExampleNew_apikey() { parser := QueryParser("api_key") opt := SetParser(parser) - authFunc := AuthenticateFunc(func(ctx context.Context, r *http.Request, token string) (auth.Info, error) { + authFunc := AuthenticateFunc(func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) { if token == "token" { - return auth.NewDefaultUser("example", "1", nil, nil), nil + return auth.NewDefaultUser("example", "1", nil, nil), time.Now().Add(time.Hour), nil } - return nil, fmt.Errorf("Invalid user token") + return nil, time.Time{}, fmt.Errorf("Invalid user token") }) cache := libcache.LRU.New(0)