124 lines
3.3 KiB
Go
124 lines
3.3 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
// Based on https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies
|
|
|
|
// The auth token consists of two parts: ID and token hash
|
|
// Every device login creates a new auth token with an individual id and hash.
|
|
// 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.
|
|
|
|
var (
|
|
ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format")
|
|
ErrAuthTokenExpired = util.NewInvalidArgumentErrorf("auth token has expired")
|
|
ErrAuthTokenInvalidHash = util.NewInvalidArgumentErrorf("auth token is invalid")
|
|
)
|
|
|
|
func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) {
|
|
if len(value) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
parts := strings.SplitN(value, ":", 2)
|
|
if len(parts) != 2 {
|
|
return nil, ErrAuthTokenInvalidFormat
|
|
}
|
|
|
|
t, err := auth_model.GetAuthTokenByID(ctx, parts[0])
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
return nil, ErrAuthTokenExpired
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if t.ExpiresUnix < timeutil.TimeStampNow() {
|
|
return nil, ErrAuthTokenExpired
|
|
}
|
|
|
|
hashedToken := sha256.Sum256([]byte(parts[1]))
|
|
|
|
if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(hex.EncodeToString(hashedToken[:]))) == 0 {
|
|
// If an attacker steals a token and uses the token to create a new session the hash gets updated.
|
|
// When the victim uses the old token the hashes don't match anymore and the victim should be notified about the compromised token.
|
|
return nil, ErrAuthTokenInvalidHash
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_model.AuthToken, string, error) {
|
|
token, hash, err := generateTokenAndHash()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
newToken := &auth_model.AuthToken{
|
|
ID: t.ID,
|
|
TokenHash: hash,
|
|
UserID: t.UserID,
|
|
ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
|
|
}
|
|
|
|
if err := auth_model.UpdateAuthTokenByID(ctx, newToken); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return newToken, token, nil
|
|
}
|
|
|
|
func CreateAuthTokenForUserID(ctx context.Context, userID int64) (*auth_model.AuthToken, string, error) {
|
|
t := &auth_model.AuthToken{
|
|
UserID: userID,
|
|
ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
|
|
}
|
|
|
|
var err error
|
|
t.ID, err = util.CryptoRandomString(10)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
token, hash, err := generateTokenAndHash()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
t.TokenHash = hash
|
|
|
|
if err := auth_model.InsertAuthToken(ctx, t); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return t, token, nil
|
|
}
|
|
|
|
func generateTokenAndHash() (string, string, error) {
|
|
buf, err := util.CryptoRandomBytes(32)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
token := hex.EncodeToString(buf)
|
|
|
|
hashedToken := sha256.Sum256([]byte(token))
|
|
|
|
return token, hex.EncodeToString(hashedToken[:]), nil
|
|
}
|