Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use general token signing secret #29205

Merged
merged 7 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/base/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string {

// create sha1 encode string
sh := sha1.New()
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes)))
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
encoded := hex.EncodeToString(sh.Sum(nil))

code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
Expand Down
3 changes: 2 additions & 1 deletion modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package context

import (
"context"
"encoding/hex"
"fmt"
"html/template"
"io"
Expand Down Expand Up @@ -124,7 +125,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
csrfOpts := CsrfOptions{
Secret: setting.SecretKey,
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
Cookie: setting.CSRFCookieName,
SetCookie: true,
Secure: setting.SessionConfig.Secure,
Expand Down
21 changes: 10 additions & 11 deletions modules/setting/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ import (

// LFS represents the configuration for Git LFS
var LFS = struct {
StartServer bool `ini:"LFS_START_SERVER"`
JWTSecretBase64 string `ini:"LFS_JWT_SECRET"`
JWTSecretBytes []byte `ini:"-"`
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
StartServer bool `ini:"LFS_START_SERVER"`
JWTSecretBytes []byte `ini:"-"`
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`

Storage *Storage
}{}
Expand Down Expand Up @@ -59,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
return nil
}

LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(LFS.JWTSecretBase64)
jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
if err != nil {
LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
}
Expand All @@ -72,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
if err != nil {
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
}
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
if err := saveCfg.Save(); err != nil {
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
}
Expand Down
35 changes: 28 additions & 7 deletions modules/setting/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package setting
import (
"math"
"path/filepath"
"sync/atomic"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
Expand Down Expand Up @@ -96,7 +97,6 @@ var OAuth2 = struct {
RefreshTokenExpirationTime int64
InvalidateRefreshTokens bool
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
JWTSecretBase64 string `ini:"JWT_SECRET"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
MaxTokenLength int
DefaultApplications []string
Expand Down Expand Up @@ -128,28 +128,49 @@ func loadOAuth2From(rootCfg ConfigProvider) {
return
}

OAuth2.JWTSecretBase64 = loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")

if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
}

if InstallLock {
if _, err := generate.DecodeJwtSecretBase64(OAuth2.JWTSecretBase64); err != nil {
_, OAuth2.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
if err != nil {
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
}

saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
}
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
if err := saveCfg.Save(); err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
}
}
generalSigningSecret.Store(&jwtSecretBytes)
}
}

// generalSigningSecret is used as container for a []byte value
// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
var generalSigningSecret atomic.Pointer[[]byte]

func GetGeneralTokenSigningSecret() []byte {
old := generalSigningSecret.Load()
if old == nil || len(*old) == 0 {
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
if err != nil {
log.Fatal("Unable to generate general JWT secret: %s", err.Error())
}
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
6543 marked this conversation as resolved.
Show resolved Hide resolved
return jwtSecret
}
return *generalSigningSecret.Load()
}
return *old
}
34 changes: 34 additions & 0 deletions modules/setting/oauth2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
"testing"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/test"

"github.com/stretchr/testify/assert"
)

func TestGetGeneralSigningSecret(t *testing.T) {
// when there is no general signing secret, it should be generated, and keep the same value
assert.Nil(t, generalSigningSecret.Load())
s1 := GetGeneralTokenSigningSecret()
assert.NotNil(t, s1)
s2 := GetGeneralTokenSigningSecret()
assert.Equal(t, s1, s2)

// the config value should always override any pre-generated value
cfg, _ := NewConfigProviderFromData(`
[oauth2]
JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
`)
defer test.MockVariableValue(&InstallLock, true)()
loadOAuth2From(cfg)
actual := GetGeneralTokenSigningSecret()
expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
assert.Len(t, actual, 32)
assert.EqualValues(t, expected, actual)
}
4 changes: 2 additions & 2 deletions services/actions/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

tokenString, err := token.SignedString([]byte(setting.SecretKey))
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
if err != nil {
return "", err
}
Expand All @@ -62,7 +62,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(setting.SecretKey), nil
return setting.GetGeneralTokenSigningSecret(), nil
})
if err != nil {
return 0, err
Expand Down
2 changes: 1 addition & 1 deletion services/actions/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
assert.NotEqual(t, "", token)
claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(setting.SecretKey), nil
return setting.GetGeneralTokenSigningSecret(), nil
})
assert.Nil(t, err)
scp, ok := claims["scp"]
Expand Down
9 changes: 1 addition & 8 deletions services/auth/source/oauth2/jwtsigningkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -301,7 +300,7 @@ func InitSigningKey() error {
case "HS384":
fallthrough
case "HS512":
key, err = loadSymmetricKey()
key = setting.GetGeneralTokenSigningSecret()
case "RS256":
fallthrough
case "RS384":
Expand Down Expand Up @@ -334,12 +333,6 @@ func InitSigningKey() error {
return nil
}

// loadSymmetricKey checks if the configured secret is valid.
// If it is not valid, it will return an error.
func loadSymmetricKey() (any, error) {
return generate.DecodeJwtSecretBase64(setting.OAuth2.JWTSecretBase64)
}

// loadOrCreateAsymmetricKey checks if the configured private key exists.
// If it does not exist a new random key gets generated and saved on the configured path.
func loadOrCreateAsymmetricKey() (any, error) {
Expand Down
4 changes: 2 additions & 2 deletions services/packages/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

tokenString, err := token.SignedString([]byte(setting.SecretKey))
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
if err != nil {
return "", err
}
Expand All @@ -57,7 +57,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(setting.SecretKey), nil
return setting.GetGeneralTokenSigningSecret(), nil
})
if err != nil {
return 0, err
Expand Down
Loading