github.com/goravel/framework@v1.13.9/auth/auth.go (about)

     1  package auth
     2  
     3  import (
     4  	"errors"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/golang-jwt/jwt/v5"
     9  	"github.com/spf13/cast"
    10  	"gorm.io/gorm/clause"
    11  
    12  	contractsauth "github.com/goravel/framework/contracts/auth"
    13  	"github.com/goravel/framework/contracts/cache"
    14  	"github.com/goravel/framework/contracts/config"
    15  	"github.com/goravel/framework/contracts/database/orm"
    16  	"github.com/goravel/framework/contracts/http"
    17  	"github.com/goravel/framework/support/carbon"
    18  	"github.com/goravel/framework/support/database"
    19  )
    20  
    21  const ctxKey = "GoravelAuth"
    22  
    23  type Claims struct {
    24  	Key string `json:"key"`
    25  	jwt.RegisteredClaims
    26  }
    27  
    28  type Guard struct {
    29  	Claims *Claims
    30  	Token  string
    31  }
    32  
    33  type Guards map[string]*Guard
    34  
    35  type Auth struct {
    36  	cache  cache.Cache
    37  	config config.Config
    38  	guard  string
    39  	orm    orm.Orm
    40  }
    41  
    42  func NewAuth(guard string, cache cache.Cache, config config.Config, orm orm.Orm) *Auth {
    43  	return &Auth{
    44  		cache:  cache,
    45  		config: config,
    46  		guard:  guard,
    47  		orm:    orm,
    48  	}
    49  }
    50  
    51  func (a *Auth) Guard(name string) contractsauth.Auth {
    52  	return NewAuth(name, a.cache, a.config, a.orm)
    53  }
    54  
    55  // User need parse token first.
    56  func (a *Auth) User(ctx http.Context, user any) error {
    57  	auth, ok := ctx.Value(ctxKey).(Guards)
    58  	if !ok || auth[a.guard] == nil {
    59  		return ErrorParseTokenFirst
    60  	}
    61  	if auth[a.guard].Claims == nil {
    62  		return ErrorParseTokenFirst
    63  	}
    64  	if auth[a.guard].Claims.Key == "" {
    65  		return ErrorInvalidKey
    66  	}
    67  	if auth[a.guard].Token == "" {
    68  		return ErrorTokenExpired
    69  	}
    70  	if err := a.orm.Query().FindOrFail(user, clause.Eq{Column: clause.PrimaryColumn, Value: auth[a.guard].Claims.Key}); err != nil {
    71  		return err
    72  	}
    73  
    74  	return nil
    75  }
    76  
    77  func (a *Auth) Parse(ctx http.Context, token string) (*contractsauth.Payload, error) {
    78  	token = strings.ReplaceAll(token, "Bearer ", "")
    79  	if a.cache == nil {
    80  		return nil, errors.New("cache support is required")
    81  	}
    82  	if a.tokenIsDisabled(token) {
    83  		return nil, ErrorTokenDisabled
    84  	}
    85  
    86  	jwtSecret := a.config.GetString("jwt.secret")
    87  	tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (any, error) {
    88  		return []byte(jwtSecret), nil
    89  	}, jwt.WithTimeFunc(func() time.Time {
    90  		return carbon.Now().ToStdTime()
    91  	}))
    92  	if err != nil {
    93  		if errors.Is(err, jwt.ErrTokenExpired) && tokenClaims != nil {
    94  			claims, ok := tokenClaims.Claims.(*Claims)
    95  			if !ok {
    96  				return nil, ErrorInvalidClaims
    97  			}
    98  
    99  			a.makeAuthContext(ctx, claims, "")
   100  
   101  			return &contractsauth.Payload{
   102  				Guard:    claims.Subject,
   103  				Key:      claims.Key,
   104  				ExpireAt: claims.ExpiresAt.Local(),
   105  				IssuedAt: claims.IssuedAt.Local(),
   106  			}, ErrorTokenExpired
   107  		}
   108  
   109  		return nil, ErrorInvalidToken
   110  	}
   111  	if tokenClaims == nil || !tokenClaims.Valid {
   112  		return nil, ErrorInvalidToken
   113  	}
   114  
   115  	claims, ok := tokenClaims.Claims.(*Claims)
   116  	if !ok {
   117  		return nil, ErrorInvalidClaims
   118  	}
   119  
   120  	a.makeAuthContext(ctx, claims, token)
   121  
   122  	return &contractsauth.Payload{
   123  		Guard:    claims.Subject,
   124  		Key:      claims.Key,
   125  		ExpireAt: claims.ExpiresAt.Time,
   126  		IssuedAt: claims.IssuedAt.Time,
   127  	}, nil
   128  }
   129  
   130  func (a *Auth) Login(ctx http.Context, user any) (token string, err error) {
   131  	id := database.GetID(user)
   132  	if id == nil {
   133  		return "", ErrorNoPrimaryKeyField
   134  	}
   135  
   136  	return a.LoginUsingID(ctx, id)
   137  }
   138  
   139  func (a *Auth) LoginUsingID(ctx http.Context, id any) (token string, err error) {
   140  	jwtSecret := a.config.GetString("jwt.secret")
   141  	if jwtSecret == "" {
   142  		return "", ErrorEmptySecret
   143  	}
   144  
   145  	nowTime := carbon.Now()
   146  	ttl := a.config.GetInt("jwt.ttl")
   147  	if ttl == 0 {
   148  		// 100 years
   149  		ttl = 60 * 24 * 365 * 100
   150  	}
   151  	expireTime := nowTime.AddMinutes(ttl).ToStdTime()
   152  	key := cast.ToString(id)
   153  	if key == "" {
   154  		return "", ErrorInvalidKey
   155  	}
   156  	claims := Claims{
   157  		key,
   158  		jwt.RegisteredClaims{
   159  			ExpiresAt: jwt.NewNumericDate(expireTime),
   160  			IssuedAt:  jwt.NewNumericDate(nowTime.ToStdTime()),
   161  			Subject:   a.guard,
   162  		},
   163  	}
   164  
   165  	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   166  	token, err = tokenClaims.SignedString([]byte(jwtSecret))
   167  	if err != nil {
   168  		return "", err
   169  	}
   170  
   171  	a.makeAuthContext(ctx, &claims, token)
   172  
   173  	return
   174  }
   175  
   176  // Refresh need parse token first.
   177  func (a *Auth) Refresh(ctx http.Context) (token string, err error) {
   178  	auth, ok := ctx.Value(ctxKey).(Guards)
   179  	if !ok || auth[a.guard] == nil {
   180  		return "", ErrorParseTokenFirst
   181  	}
   182  	if auth[a.guard].Claims == nil {
   183  		return "", ErrorParseTokenFirst
   184  	}
   185  
   186  	nowTime := carbon.Now()
   187  	refreshTtl := a.config.GetInt("jwt.refresh_ttl")
   188  	if refreshTtl == 0 {
   189  		// 100 years
   190  		refreshTtl = 60 * 24 * 365 * 100
   191  	}
   192  
   193  	expireTime := carbon.FromStdTime(auth[a.guard].Claims.ExpiresAt.Time).AddMinutes(refreshTtl)
   194  	if nowTime.Gt(expireTime) {
   195  		return "", ErrorRefreshTimeExceeded
   196  	}
   197  
   198  	return a.LoginUsingID(ctx, auth[a.guard].Claims.Key)
   199  }
   200  
   201  func (a *Auth) Logout(ctx http.Context) error {
   202  	auth, ok := ctx.Value(ctxKey).(Guards)
   203  	if !ok || auth[a.guard] == nil || auth[a.guard].Token == "" {
   204  		return nil
   205  	}
   206  
   207  	if a.cache == nil {
   208  		return errors.New("cache support is required")
   209  	}
   210  
   211  	ttl := a.config.GetInt("jwt.ttl")
   212  	if ttl == 0 {
   213  		if ok := a.cache.Forever(getDisabledCacheKey(auth[a.guard].Token), true); !ok {
   214  			return errors.New("cache forever failed")
   215  		}
   216  	} else {
   217  		if err := a.cache.Put(getDisabledCacheKey(auth[a.guard].Token),
   218  			true,
   219  			time.Duration(ttl)*time.Minute,
   220  		); err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	delete(auth, a.guard)
   226  	ctx.WithValue(ctxKey, auth)
   227  
   228  	return nil
   229  }
   230  
   231  func (a *Auth) makeAuthContext(ctx http.Context, claims *Claims, token string) {
   232  	guards, ok := ctx.Value(ctxKey).(Guards)
   233  	if !ok {
   234  		guards = make(Guards)
   235  	}
   236  	guards[a.guard] = &Guard{claims, token}
   237  	ctx.WithValue(ctxKey, guards)
   238  }
   239  
   240  func (a *Auth) tokenIsDisabled(token string) bool {
   241  	return a.cache.GetBool(getDisabledCacheKey(token), false)
   242  }
   243  
   244  func getDisabledCacheKey(token string) string {
   245  	return "jwt:disabled:" + token
   246  }