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 }