Skip to content

Commit

Permalink
add: auth, subscriptions, template
Browse files Browse the repository at this point in the history
  • Loading branch information
moocss committed Aug 19, 2024
1 parent c6d0bf5 commit 909380a
Show file tree
Hide file tree
Showing 61 changed files with 4,312 additions and 28 deletions.
137 changes: 137 additions & 0 deletions authx/jwt/jwt.go
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
}
75 changes: 75 additions & 0 deletions authx/jwt/jwt_test.go
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())
}
86 changes: 86 additions & 0 deletions authx/jwt/options.go
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
}
}
18 changes: 18 additions & 0 deletions authx/jwt/store.go
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)
}
53 changes: 53 additions & 0 deletions authx/jwt/store/redis/redis.go
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)
}
Loading

0 comments on commit 909380a

Please sign in to comment.