-
Notifications
You must be signed in to change notification settings - Fork 0
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
61 changed files
with
4,312 additions
and
28 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,137 @@ | ||
package jwt | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"time" | ||
|
||
"github.com/apus-run/van/authx" | ||
"github.com/golang-jwt/jwt/v5" | ||
) | ||
|
||
var ( | ||
ErrTokenInvalid = errors.New("token is invalid") | ||
ErrUnSupportSigningMethod = errors.New("wrong signing method") | ||
ErrSignToken = errors.New("can not sign token. is the key correct") | ||
ErrGetKey = errors.New("can not get key while signing token") | ||
) | ||
|
||
// JwtAuth implement the authx.Authenticator interface. | ||
|
||
type JwtAuth struct { | ||
*options | ||
store Storer | ||
} | ||
|
||
func NewJwtAuth(store Storer, opts ...Option) *JwtAuth { | ||
options := Apply(opts...) | ||
return &JwtAuth{ | ||
options: options, | ||
store: store, | ||
} | ||
} | ||
|
||
func (j *JwtAuth) Sign(ctx context.Context) (authx.Token, error) { | ||
now := time.Now() | ||
expiresAt := now.Add(j.expired) | ||
|
||
tokenString, err := j.GenerateToken(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
tokenInfo := &tokenInfo{ | ||
Token: tokenString, | ||
Type: j.tokenType, | ||
ExpiresAt: expiresAt.Unix(), | ||
} | ||
return tokenInfo, nil | ||
} | ||
|
||
func (j *JwtAuth) Destroy(ctx context.Context, refreshToken string) error { | ||
claims, err := j.ParseClaims(ctx, refreshToken) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// If storage is set, put the unexpired token in | ||
store := func(store Storer) error { | ||
expired := time.Until(claims.ExpiresAt.Time) | ||
return store.Set(ctx, refreshToken, "1", expired) | ||
} | ||
return j.callStore(store) | ||
} | ||
|
||
func (j *JwtAuth) ParseClaims(ctx context.Context, accessToken string) (*jwt.RegisteredClaims, error) { | ||
if accessToken == "" { | ||
return nil, ErrTokenInvalid | ||
} | ||
|
||
token, err := j.ParseToken(ctx, accessToken) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
store := func(store Storer) error { | ||
exists, err := store.Check(ctx, accessToken) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if exists { | ||
return ErrTokenInvalid | ||
} | ||
|
||
return nil | ||
} | ||
|
||
if err := j.callStore(store); err != nil { | ||
return nil, err | ||
} | ||
|
||
return token.Claims.(*jwt.RegisteredClaims), nil | ||
} | ||
|
||
func (j *JwtAuth) ParseToken(ctx context.Context, accessToken string) (token *jwt.Token, err error) { | ||
if j.claims != nil { | ||
token, err = jwt.ParseWithClaims(accessToken, j.claims(), j.keyfunc) | ||
} else { | ||
token, err = jwt.Parse(accessToken, j.keyfunc) | ||
} | ||
|
||
// 过期的, 伪造的, 都可以认为是无效token | ||
if err != nil || !token.Valid { | ||
return nil, ErrTokenInvalid | ||
} | ||
|
||
if token.Method != j.signingMethod { | ||
return nil, ErrUnSupportSigningMethod | ||
} | ||
|
||
return token, nil | ||
} | ||
|
||
func (j *JwtAuth) GenerateToken(ctx context.Context) (string, error) { | ||
token := jwt.NewWithClaims(j.signingMethod, j.claims()) | ||
if j.tokenHeader != nil { | ||
for k, v := range j.tokenHeader { | ||
token.Header[k] = v | ||
} | ||
} | ||
key, err := j.keyfunc(token) | ||
if err != nil { | ||
return "", ErrGetKey | ||
} | ||
tokenStr, err := token.SignedString(key) | ||
if err != nil { | ||
return "", ErrSignToken | ||
} | ||
|
||
return tokenStr, nil | ||
} | ||
|
||
func (j *JwtAuth) callStore(fn func(Storer) error) error { | ||
if store := j.store; store != nil { | ||
return fn(store) | ||
} | ||
return 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,75 @@ | ||
package jwt | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/golang-jwt/jwt/v5" | ||
|
||
"github.com/apus-run/van/authx/jwt/store/redis" | ||
) | ||
|
||
type CustomClaims struct { | ||
UserID uint64 | ||
|
||
// UserAgent 增强安全性,防止token被盗用 | ||
UserAgent string | ||
|
||
jwt.RegisteredClaims | ||
} | ||
|
||
func TestGenerateToken(t *testing.T) { | ||
|
||
} | ||
|
||
func TestParseToken(t *testing.T) { | ||
|
||
} | ||
|
||
func TestNewJwtAuth(t *testing.T) { | ||
headers := make(map[string]any) | ||
headers["kid"] = "8b5228a5-b3d2-4165-aaac-58a052629846" | ||
now := time.Now() | ||
expiresAt := now.Add(2 * time.Hour) | ||
opts := []Option{ | ||
|
||
WithTokenHeader(headers), | ||
WithExpired(2 * time.Hour), | ||
WithKeyfunc(func(token *jwt.Token) (any, error) { | ||
// Verify that the signing method is HMAC. | ||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||
return nil, ErrTokenInvalid | ||
} | ||
return []byte("moyn8y9abnd7q4zkq2m73yw8tu9j5ixm"), nil | ||
}), | ||
WithClaims(func() jwt.Claims { | ||
return &CustomClaims{ | ||
UserID: 1, | ||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36", | ||
RegisteredClaims: jwt.RegisteredClaims{ | ||
// Issuer = iss,令牌颁发者。它表示该令牌是由谁创建的 | ||
Issuer: "", | ||
// IssuedAt = iat,令牌颁发时的时间戳。它表示令牌是何时被创建的 | ||
IssuedAt: jwt.NewNumericDate(now), | ||
// ExpiresAt = exp,令牌的过期时间戳。它表示令牌将在何时过期 | ||
ExpiresAt: jwt.NewNumericDate(expiresAt), | ||
// NotBefore = nbf,令牌的生效时的时间戳。它表示令牌从什么时候开始生效 | ||
NotBefore: jwt.NewNumericDate(now), | ||
// Subject = sub,令牌的主体。它表示该令牌是关于谁的 | ||
Subject: "", | ||
}, | ||
} | ||
}), | ||
} | ||
|
||
opts = append(opts, WithSigningMethod(jwt.SigningMethodHS256)) | ||
|
||
store := redis.NewStore(nil, "authx") | ||
|
||
j, err := NewJwtAuth(store, opts...).Sign(context.Background()) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
t.Log(j.GetToken()) | ||
} |
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,86 @@ | ||
package jwt | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/golang-jwt/jwt/v5" | ||
) | ||
|
||
const ( | ||
// defaultKey holds the default key used to sign a jwt token. | ||
defaultKey = "authx::jwt(#)9527" | ||
) | ||
|
||
// Option is jwt option. | ||
type Option func(*options) | ||
|
||
// Parser is a jwt parser | ||
type options struct { | ||
signingMethod jwt.SigningMethod | ||
claims func() jwt.Claims | ||
tokenHeader map[string]any | ||
|
||
expired time.Duration | ||
keyfunc jwt.Keyfunc | ||
tokenType string | ||
} | ||
|
||
// DefaultOptions . | ||
func DefaultOptions() *options { | ||
return &options{ | ||
tokenType: "Bearer", | ||
expired: 2 * time.Hour, | ||
signingMethod: jwt.SigningMethodHS256, | ||
keyfunc: func(token *jwt.Token) (any, error) { | ||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||
return nil, ErrTokenInvalid | ||
} | ||
return []byte(defaultKey), nil | ||
}, | ||
} | ||
} | ||
|
||
func Apply(opts ...Option) *options { | ||
options := DefaultOptions() | ||
for _, opt := range opts { | ||
opt(options) | ||
} | ||
return options | ||
} | ||
|
||
// WithSigningMethod with signing method option. | ||
func WithSigningMethod(method jwt.SigningMethod) Option { | ||
return func(o *options) { | ||
o.signingMethod = method | ||
} | ||
} | ||
|
||
// WithClaims with customer claim | ||
// If you use it in Server, f needs to return a new jwt.Claims object each time to avoid concurrent write problems | ||
// If you use it in Client, f only needs to return a single object to provide performance | ||
func WithClaims(f func() jwt.Claims) Option { | ||
return func(o *options) { | ||
o.claims = f | ||
} | ||
} | ||
|
||
// WithTokenHeader withe customer tokenHeader for client side | ||
func WithTokenHeader(header map[string]any) Option { | ||
return func(o *options) { | ||
o.tokenHeader = header | ||
} | ||
} | ||
|
||
// WithKeyfunc set the callback function for verifying the key. | ||
func WithKeyfunc(keyFunc jwt.Keyfunc) Option { | ||
return func(o *options) { | ||
o.keyfunc = keyFunc | ||
} | ||
} | ||
|
||
// WithExpired set the token expiration time (in seconds, default 2h). | ||
func WithExpired(expired time.Duration) Option { | ||
return func(o *options) { | ||
o.expired = expired | ||
} | ||
} |
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,18 @@ | ||
package jwt | ||
|
||
import ( | ||
"context" | ||
"time" | ||
) | ||
|
||
// Storer token storage interface. | ||
type Storer interface { | ||
// Set Store token data and specify expiration time. | ||
Set(ctx context.Context, accessToken string, val any, expiration time.Duration) error | ||
|
||
// Delete token data from storage. | ||
Delete(ctx context.Context, accessToken string) (bool, error) | ||
|
||
// Check if token exists. | ||
Check(ctx context.Context, accessToken string) (bool, error) | ||
} |
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,53 @@ | ||
package redis | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/redis/go-redis/v9" | ||
) | ||
|
||
// Store redis storage. | ||
type Store struct { | ||
client redis.Cmdable | ||
|
||
prefix string | ||
} | ||
|
||
// NewStore create an *Store instance to handle token storage, deletion, and checking. | ||
func NewStore(client redis.Cmdable, prefix string) *Store { | ||
return &Store{client: client, prefix: prefix} | ||
} | ||
|
||
// Set call the Redis client to set a key-value pair with an | ||
// expiration time, where the key name format is <prefix><accessToken>. | ||
func (s *Store) Set(ctx context.Context, accessToken string, val any, expiration time.Duration) error { | ||
cmd := s.client.Set(ctx, s.key(accessToken), val, expiration) | ||
return cmd.Err() | ||
} | ||
|
||
// Delete delete the specified JWT Token in Redis. | ||
func (s *Store) Delete(ctx context.Context, accessToken string) (bool, error) { | ||
cmd := s.client.Del(ctx, s.key(accessToken)) | ||
if err := cmd.Err(); err != nil { | ||
return false, err | ||
} | ||
return cmd.Val() > 0, nil | ||
} | ||
|
||
// Check check if the specified JWT Token exists in Redis. | ||
func (s *Store) Check(ctx context.Context, accessToken string) (bool, error) { | ||
s.client.Get(ctx, s.key(accessToken)) | ||
|
||
cmd := s.client.Exists(ctx, s.key(accessToken)) | ||
if err := cmd.Err(); err != nil { | ||
return false, err | ||
} | ||
return cmd.Val() > 0, nil | ||
} | ||
|
||
// wrapperKey is used to build the key name in Redis. | ||
func (s *Store) key(key string) string { | ||
return fmt.Sprintf("%s%s", s.prefix, key) | ||
} |
Oops, something went wrong.