code.gitea.io/gitea@v1.22.3/services/auth/auth_token.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package auth
     5  
     6  import (
     7  	"context"
     8  	"crypto/sha256"
     9  	"crypto/subtle"
    10  	"encoding/hex"
    11  	"errors"
    12  	"strings"
    13  	"time"
    14  
    15  	auth_model "code.gitea.io/gitea/models/auth"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	"code.gitea.io/gitea/modules/timeutil"
    18  	"code.gitea.io/gitea/modules/util"
    19  )
    20  
    21  // Based on https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies
    22  
    23  // The auth token consists of two parts: ID and token hash
    24  // Every device login creates a new auth token with an individual id and hash.
    25  // If a device uses the token to login into the instance, a fresh token gets generated which has the same id but a new hash.
    26  
    27  var (
    28  	ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format")
    29  	ErrAuthTokenExpired       = util.NewInvalidArgumentErrorf("auth token has expired")
    30  	ErrAuthTokenInvalidHash   = util.NewInvalidArgumentErrorf("auth token is invalid")
    31  )
    32  
    33  func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) {
    34  	if len(value) == 0 {
    35  		return nil, nil
    36  	}
    37  
    38  	parts := strings.SplitN(value, ":", 2)
    39  	if len(parts) != 2 {
    40  		return nil, ErrAuthTokenInvalidFormat
    41  	}
    42  
    43  	t, err := auth_model.GetAuthTokenByID(ctx, parts[0])
    44  	if err != nil {
    45  		if errors.Is(err, util.ErrNotExist) {
    46  			return nil, ErrAuthTokenExpired
    47  		}
    48  		return nil, err
    49  	}
    50  
    51  	if t.ExpiresUnix < timeutil.TimeStampNow() {
    52  		return nil, ErrAuthTokenExpired
    53  	}
    54  
    55  	hashedToken := sha256.Sum256([]byte(parts[1]))
    56  
    57  	if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(hex.EncodeToString(hashedToken[:]))) == 0 {
    58  		// If an attacker steals a token and uses the token to create a new session the hash gets updated.
    59  		// When the victim uses the old token the hashes don't match anymore and the victim should be notified about the compromised token.
    60  		return nil, ErrAuthTokenInvalidHash
    61  	}
    62  
    63  	return t, nil
    64  }
    65  
    66  func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_model.AuthToken, string, error) {
    67  	token, hash, err := generateTokenAndHash()
    68  	if err != nil {
    69  		return nil, "", err
    70  	}
    71  
    72  	newToken := &auth_model.AuthToken{
    73  		ID:          t.ID,
    74  		TokenHash:   hash,
    75  		UserID:      t.UserID,
    76  		ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
    77  	}
    78  
    79  	if err := auth_model.UpdateAuthTokenByID(ctx, newToken); err != nil {
    80  		return nil, "", err
    81  	}
    82  
    83  	return newToken, token, nil
    84  }
    85  
    86  func CreateAuthTokenForUserID(ctx context.Context, userID int64) (*auth_model.AuthToken, string, error) {
    87  	t := &auth_model.AuthToken{
    88  		UserID:      userID,
    89  		ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
    90  	}
    91  
    92  	var err error
    93  	t.ID, err = util.CryptoRandomString(10)
    94  	if err != nil {
    95  		return nil, "", err
    96  	}
    97  
    98  	token, hash, err := generateTokenAndHash()
    99  	if err != nil {
   100  		return nil, "", err
   101  	}
   102  
   103  	t.TokenHash = hash
   104  
   105  	if err := auth_model.InsertAuthToken(ctx, t); err != nil {
   106  		return nil, "", err
   107  	}
   108  
   109  	return t, token, nil
   110  }
   111  
   112  func generateTokenAndHash() (string, string, error) {
   113  	buf, err := util.CryptoRandomBytes(32)
   114  	if err != nil {
   115  		return "", "", err
   116  	}
   117  
   118  	token := hex.EncodeToString(buf)
   119  
   120  	hashedToken := sha256.Sum256([]byte(token))
   121  
   122  	return token, hex.EncodeToString(hashedToken[:]), nil
   123  }