-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
673 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// Copyright 2020 The Go-Guardian. All rights reserved. | ||
// Use of this source code is governed by a MIT | ||
// license that can be found in the LICENSE file. | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"time" | ||
|
||
gojwt "github.com/dgrijalva/jwt-go/v4" | ||
"github.com/gorilla/mux" | ||
"github.com/shaj13/libcache" | ||
_ "github.com/shaj13/libcache/fifo" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/basic" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/jwt" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/union" | ||
) | ||
|
||
// Usage: | ||
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -u admin:admin | ||
// curl -k http://127.0.0.1:8080/v1/auth/token -u admin:admin <obtain a token> | ||
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -H "Authorization: Bearer <token>" | ||
|
||
var strategy union.Union | ||
var keeper jwt.SecretsKeeper | ||
|
||
func main() { | ||
setupGoGuardian() | ||
router := mux.NewRouter() | ||
router.HandleFunc("/v1/auth/token", middleware(http.HandlerFunc(createToken))).Methods("GET") | ||
router.HandleFunc("/v1/book/{id}", middleware(http.HandlerFunc(getBookAuthor))).Methods("GET") | ||
log.Println("server started and listening on http://127.0.0.1:8080") | ||
http.ListenAndServe("127.0.0.1:8080", router) | ||
} | ||
|
||
func createToken(w http.ResponseWriter, r *http.Request) { | ||
u := auth.User(r) | ||
token, _ := jwt.IssueAccessToken(u, keeper) | ||
body := fmt.Sprintf("token: %s \n", token) | ||
w.Write([]byte(body)) | ||
} | ||
|
||
func getBookAuthor(w http.ResponseWriter, r *http.Request) { | ||
vars := mux.Vars(r) | ||
id := vars["id"] | ||
books := map[string]string{ | ||
"1449311601": "Ryan Boyd", | ||
"148425094X": "Yvonne Wilson", | ||
"1484220498": "Prabath Siriwarden", | ||
} | ||
body := fmt.Sprintf("Author: %s \n", books[id]) | ||
w.Write([]byte(body)) | ||
} | ||
|
||
func setupGoGuardian() { | ||
keeper = jwt.StaticSecret{ | ||
ID: "secret-id", | ||
Secret: []byte("secret"), | ||
Method: gojwt.SigningMethodHS256, | ||
} | ||
cache := libcache.FIFO.New(0) | ||
cache.SetTTL(time.Minute * 5) | ||
cache.RegisterOnExpired(func(key, _ interface{}) { | ||
cache.Peek(key) | ||
}) | ||
basicStrategy := basic.NewCached(validateUser, cache) | ||
jwtStrategy := jwt.New(cache, keeper) | ||
strategy = union.New(jwtStrategy, basicStrategy) | ||
} | ||
|
||
func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) { | ||
// here connect to db or any other service to fetch user and validate it. | ||
if userName == "admin" && password == "admin" { | ||
return auth.NewDefaultUser("admin", "1", nil, nil), nil | ||
} | ||
|
||
return nil, fmt.Errorf("Invalid credentials") | ||
} | ||
|
||
func middleware(next http.Handler) http.HandlerFunc { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
log.Println("Executing Auth Middleware") | ||
_, user, err := strategy.AuthenticateRequest(r) | ||
if err != nil { | ||
fmt.Println(err) | ||
code := http.StatusUnauthorized | ||
http.Error(w, http.StatusText(code), code) | ||
return | ||
} | ||
log.Printf("User %s Authenticated\n", user.GetUserName()) | ||
r = auth.RequestWithUser(user, r) | ||
next.ServeHTTP(w, r) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package jwt | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/dgrijalva/jwt-go/v4" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth" | ||
) | ||
|
||
type claims struct { | ||
UserInfo auth.Info `json:"info"` | ||
Issuer string `json:"iss"` | ||
Subject string `json:"sub"` | ||
Audience jwt.ClaimStrings `json:"aud"` | ||
Expiration time.Time `json:"exp"` | ||
NotBefore time.Time `json:"nbf"` | ||
IssuedAt time.Time `json:"iat"` | ||
} | ||
|
||
// nolint:govet | ||
func (c claims) Valid(v *jwt.ValidationHelper) error { | ||
if err := v.ValidateAudience(c.Audience); err != nil { | ||
return err | ||
} | ||
|
||
if err := v.ValidateIssuer(c.Issuer); err != nil { | ||
return err | ||
} | ||
|
||
if err := v.ValidateNotBefore(&jwt.Time{c.NotBefore}); err != nil { | ||
return err | ||
} | ||
|
||
return v.ValidateExpiresAt(&jwt.Time{c.Expiration}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package jwt | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/dgrijalva/jwt-go/v4" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestClaimsValid(t *testing.T) { | ||
table := []struct { | ||
name string | ||
c claims | ||
opt jwt.ParserOption | ||
expectErr bool | ||
}{ | ||
{ | ||
name: "it return nil when claims valid", | ||
c: claims{ | ||
Expiration: time.Now().Add(time.Hour), | ||
}, | ||
opt: jwt.WithoutClaimsValidation(), | ||
}, | ||
{ | ||
name: "it return error when claims expired", | ||
expectErr: true, | ||
c: claims{}, | ||
opt: jwt.WithoutClaimsValidation(), | ||
}, | ||
{ | ||
name: "it return error when token not valid nbf", | ||
expectErr: true, | ||
c: claims{ | ||
NotBefore: time.Now().Add(time.Hour), | ||
}, | ||
opt: jwt.WithoutClaimsValidation(), | ||
}, | ||
{ | ||
name: "it return error when token not valid aud", | ||
expectErr: true, | ||
c: claims{ | ||
Audience: jwt.ClaimStrings{"test"}, | ||
}, | ||
opt: jwt.WithAudience("#test#"), | ||
}, | ||
{ | ||
name: "it return error when token not valid iss", | ||
expectErr: true, | ||
c: claims{ | ||
Issuer: "test", | ||
}, | ||
opt: jwt.WithIssuer("#test#"), | ||
}, | ||
} | ||
|
||
for _, tt := range table { | ||
t.Run(tt.name, func(t *testing.T) { | ||
vh := jwt.NewValidationHelper(tt.opt) | ||
err := tt.c.Valid(vh) | ||
assert.Equal(t, tt.expectErr, err != nil) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package jwt_test | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth" | ||
|
||
gojwt "github.com/dgrijalva/jwt-go/v4" | ||
"github.com/shaj13/libcache" | ||
_ "github.com/shaj13/libcache/lru" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth/strategies/jwt" | ||
) | ||
|
||
type RotatedSecrets struct { | ||
Secrtes map[string][]byte | ||
LatestID string | ||
RotationDuration time.Duration | ||
LastRotation time.Time | ||
} | ||
|
||
func (r RotatedSecrets) KID() string { | ||
if time.Now().After(r.LastRotation) { | ||
r.LastRotation = time.Now().Add(r.RotationDuration) | ||
r.LatestID = "your generated id" | ||
r.Secrtes[r.LatestID] = []byte("your generated secrets") | ||
} | ||
return r.LatestID | ||
} | ||
|
||
func (r RotatedSecrets) Get(kid string) (key interface{}, m gojwt.SigningMethod, err error) { | ||
s, ok := r.Secrtes[kid] | ||
if ok { | ||
return s, gojwt.SigningMethodHS256, nil | ||
} | ||
return nil, nil, fmt.Errorf("Invalid KID %s", kid) | ||
} | ||
|
||
func 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) | ||
strategy := jwt.New(c, s) | ||
|
||
fmt.Println(err) | ||
|
||
// user request | ||
r, _ := http.NewRequest("GET", "/", nil) | ||
r.Header.Set("Authorization", "Bearer "+token) | ||
user, err := strategy.Authenticate(r.Context(), r) | ||
fmt.Println(user.GetID(), err) | ||
|
||
// Output: | ||
// <nil> | ||
// example <nil> | ||
} | ||
|
||
func ExampleSecretsKeeper() { | ||
// The example shows how to create your custom secrets keeper to rotate secrets. | ||
s := RotatedSecrets{ | ||
Secrtes: make(map[string][]byte), | ||
} | ||
u := auth.NewUserInfo("example", "example", nil, nil) | ||
c := libcache.LRU.New(0) | ||
|
||
token, err := jwt.IssueAccessToken(u, s) | ||
strategy := jwt.New(c, s) | ||
|
||
fmt.Println(err) | ||
|
||
// user request | ||
r, _ := http.NewRequest("GET", "/", nil) | ||
r.Header.Set("Authorization", "Bearer "+token) | ||
user, err := strategy.Authenticate(r.Context(), r) | ||
fmt.Println(user.GetID(), err) | ||
|
||
// Output: | ||
// <nil> | ||
// example <nil> | ||
} | ||
|
||
func ExampleSetAudience() { | ||
aud := jwt.SetAudience("example-aud") | ||
u := auth.NewUserInfo("example", "example", nil, nil) | ||
s := jwt.StaticSecret{} | ||
c := libcache.LRU.New(0) | ||
|
||
_, _ = jwt.IssueAccessToken(u, s, aud) | ||
_ = jwt.New(c, s, aud) | ||
} | ||
|
||
func ExampleSetIssuer() { | ||
iss := jwt.SetIssuer("example-iss") | ||
u := auth.NewUserInfo("example", "example", nil, nil) | ||
s := jwt.StaticSecret{} | ||
c := libcache.LRU.New(0) | ||
|
||
_, _ = jwt.IssueAccessToken(u, s, iss) | ||
_ = jwt.New(c, s, iss) | ||
} | ||
|
||
func ExampleSetExpDuration() { | ||
exp := jwt.SetExpDuration(time.Hour) | ||
u := auth.NewUserInfo("example", "example", nil, nil) | ||
s := jwt.StaticSecret{} | ||
c := libcache.LRU.New(0) | ||
|
||
_, _ = jwt.IssueAccessToken(u, s, exp) | ||
_ = jwt.New(c, s, exp) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Package jwt provides authentication strategy, | ||
// to authenticate HTTP requests based on jwt token. | ||
package jwt | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/token" | ||
) | ||
|
||
// GetAuthenticateFunc return function to authenticate request using jwt token. | ||
// 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 c.UserInfo, c.Expiration, err | ||
} | ||
} | ||
|
||
// New return strategy authenticate request using jwt token. | ||
// New is similar to token.New(). | ||
func New(c auth.Cache, s SecretsKeeper, opts ...auth.Option) auth.Strategy { | ||
fn := GetAuthenticateFunc(s, opts...) | ||
return token.New(fn, c, opts...) | ||
} |
Oops, something went wrong.