code.gitea.io/gitea@v1.21.7/models/auth/token.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package auth 6 7 import ( 8 "context" 9 "crypto/subtle" 10 "encoding/hex" 11 "fmt" 12 "time" 13 14 "code.gitea.io/gitea/models/db" 15 "code.gitea.io/gitea/modules/setting" 16 "code.gitea.io/gitea/modules/timeutil" 17 "code.gitea.io/gitea/modules/util" 18 19 lru "github.com/hashicorp/golang-lru/v2" 20 ) 21 22 // ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error. 23 type ErrAccessTokenNotExist struct { 24 Token string 25 } 26 27 // IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist. 28 func IsErrAccessTokenNotExist(err error) bool { 29 _, ok := err.(ErrAccessTokenNotExist) 30 return ok 31 } 32 33 func (err ErrAccessTokenNotExist) Error() string { 34 return fmt.Sprintf("access token does not exist [sha: %s]", err.Token) 35 } 36 37 func (err ErrAccessTokenNotExist) Unwrap() error { 38 return util.ErrNotExist 39 } 40 41 // ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error. 42 type ErrAccessTokenEmpty struct{} 43 44 // IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty. 45 func IsErrAccessTokenEmpty(err error) bool { 46 _, ok := err.(ErrAccessTokenEmpty) 47 return ok 48 } 49 50 func (err ErrAccessTokenEmpty) Error() string { 51 return "access token is empty" 52 } 53 54 func (err ErrAccessTokenEmpty) Unwrap() error { 55 return util.ErrInvalidArgument 56 } 57 58 var successfulAccessTokenCache *lru.Cache[string, any] 59 60 // AccessToken represents a personal access token. 61 type AccessToken struct { 62 ID int64 `xorm:"pk autoincr"` 63 UID int64 `xorm:"INDEX"` 64 Name string 65 Token string `xorm:"-"` 66 TokenHash string `xorm:"UNIQUE"` // sha256 of token 67 TokenSalt string 68 TokenLastEight string `xorm:"INDEX token_last_eight"` 69 Scope AccessTokenScope 70 71 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 72 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 73 HasRecentActivity bool `xorm:"-"` 74 HasUsed bool `xorm:"-"` 75 } 76 77 // AfterLoad is invoked from XORM after setting the values of all fields of this object. 78 func (t *AccessToken) AfterLoad() { 79 t.HasUsed = t.UpdatedUnix > t.CreatedUnix 80 t.HasRecentActivity = t.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow() 81 } 82 83 func init() { 84 db.RegisterModel(new(AccessToken), func() error { 85 if setting.SuccessfulTokensCacheSize > 0 { 86 var err error 87 successfulAccessTokenCache, err = lru.New[string, any](setting.SuccessfulTokensCacheSize) 88 if err != nil { 89 return fmt.Errorf("unable to allocate AccessToken cache: %w", err) 90 } 91 } else { 92 successfulAccessTokenCache = nil 93 } 94 return nil 95 }) 96 } 97 98 // NewAccessToken creates new access token. 99 func NewAccessToken(ctx context.Context, t *AccessToken) error { 100 salt, err := util.CryptoRandomString(10) 101 if err != nil { 102 return err 103 } 104 token, err := util.CryptoRandomBytes(20) 105 if err != nil { 106 return err 107 } 108 t.TokenSalt = salt 109 t.Token = hex.EncodeToString(token) 110 t.TokenHash = HashToken(t.Token, t.TokenSalt) 111 t.TokenLastEight = t.Token[len(t.Token)-8:] 112 _, err = db.GetEngine(ctx).Insert(t) 113 return err 114 } 115 116 // DisplayPublicOnly whether to display this as a public-only token. 117 func (t *AccessToken) DisplayPublicOnly() bool { 118 publicOnly, err := t.Scope.PublicOnly() 119 if err != nil { 120 return false 121 } 122 return publicOnly 123 } 124 125 func getAccessTokenIDFromCache(token string) int64 { 126 if successfulAccessTokenCache == nil { 127 return 0 128 } 129 tInterface, ok := successfulAccessTokenCache.Get(token) 130 if !ok { 131 return 0 132 } 133 t, ok := tInterface.(int64) 134 if !ok { 135 return 0 136 } 137 return t 138 } 139 140 // GetAccessTokenBySHA returns access token by given token value 141 func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) { 142 if token == "" { 143 return nil, ErrAccessTokenEmpty{} 144 } 145 // A token is defined as being SHA1 sum these are 40 hexadecimal bytes long 146 if len(token) != 40 { 147 return nil, ErrAccessTokenNotExist{token} 148 } 149 for _, x := range []byte(token) { 150 if x < '0' || (x > '9' && x < 'a') || x > 'f' { 151 return nil, ErrAccessTokenNotExist{token} 152 } 153 } 154 155 lastEight := token[len(token)-8:] 156 157 if id := getAccessTokenIDFromCache(token); id > 0 { 158 accessToken := &AccessToken{ 159 TokenLastEight: lastEight, 160 } 161 // Re-get the token from the db in case it has been deleted in the intervening period 162 has, err := db.GetEngine(ctx).ID(id).Get(accessToken) 163 if err != nil { 164 return nil, err 165 } 166 if has { 167 return accessToken, nil 168 } 169 successfulAccessTokenCache.Remove(token) 170 } 171 172 var tokens []AccessToken 173 err := db.GetEngine(ctx).Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens) 174 if err != nil { 175 return nil, err 176 } else if len(tokens) == 0 { 177 return nil, ErrAccessTokenNotExist{token} 178 } 179 180 for _, t := range tokens { 181 tempHash := HashToken(token, t.TokenSalt) 182 if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 { 183 if successfulAccessTokenCache != nil { 184 successfulAccessTokenCache.Add(token, t.ID) 185 } 186 return &t, nil 187 } 188 } 189 return nil, ErrAccessTokenNotExist{token} 190 } 191 192 // AccessTokenByNameExists checks if a token name has been used already by a user. 193 func AccessTokenByNameExists(ctx context.Context, token *AccessToken) (bool, error) { 194 return db.GetEngine(ctx).Table("access_token").Where("name = ?", token.Name).And("uid = ?", token.UID).Exist() 195 } 196 197 // ListAccessTokensOptions contain filter options 198 type ListAccessTokensOptions struct { 199 db.ListOptions 200 Name string 201 UserID int64 202 } 203 204 // ListAccessTokens returns a list of access tokens belongs to given user. 205 func ListAccessTokens(ctx context.Context, opts ListAccessTokensOptions) ([]*AccessToken, error) { 206 sess := db.GetEngine(ctx).Where("uid=?", opts.UserID) 207 208 if len(opts.Name) != 0 { 209 sess = sess.Where("name=?", opts.Name) 210 } 211 212 sess = sess.Desc("created_unix") 213 214 if opts.Page != 0 { 215 sess = db.SetSessionPagination(sess, &opts) 216 217 tokens := make([]*AccessToken, 0, opts.PageSize) 218 return tokens, sess.Find(&tokens) 219 } 220 221 tokens := make([]*AccessToken, 0, 5) 222 return tokens, sess.Find(&tokens) 223 } 224 225 // UpdateAccessToken updates information of access token. 226 func UpdateAccessToken(ctx context.Context, t *AccessToken) error { 227 _, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t) 228 return err 229 } 230 231 // CountAccessTokens count access tokens belongs to given user by options 232 func CountAccessTokens(ctx context.Context, opts ListAccessTokensOptions) (int64, error) { 233 sess := db.GetEngine(ctx).Where("uid=?", opts.UserID) 234 if len(opts.Name) != 0 { 235 sess = sess.Where("name=?", opts.Name) 236 } 237 return sess.Count(&AccessToken{}) 238 } 239 240 // DeleteAccessTokenByID deletes access token by given ID. 241 func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error { 242 cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{ 243 UID: userID, 244 }) 245 if err != nil { 246 return err 247 } else if cnt != 1 { 248 return ErrAccessTokenNotExist{} 249 } 250 return nil 251 }