Add asymmetric JWT signing (#16010)
* Added asymmetric token signing. * Load signing key from settings. * Added optional kid parameter. * Updated documentation. * Add "kid" to token header.
This commit is contained in:
		
							parent
							
								
									f7cd394680
								
							
						
					
					
						commit
						29695cd6d5
					
				|  | @ -71,7 +71,7 @@ func runGenerateInternalToken(c *cli.Context) error { | |||
| } | ||||
| 
 | ||||
| func runGenerateLfsJwtSecret(c *cli.Context) error { | ||||
| 	JWTSecretBase64, err := generate.NewJwtSecret() | ||||
| 	JWTSecretBase64, err := generate.NewJwtSecretBase64() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -858,7 +858,9 @@ NB: You must have `DISABLE_ROUTER_LOG` set to `false` for this option to take ef | |||
| - `ACCESS_TOKEN_EXPIRATION_TIME`: **3600**: Lifetime of an OAuth2 access token in seconds | ||||
| - `REFRESH_TOKEN_EXPIRATION_TIME`: **730**: Lifetime of an OAuth2 refresh token in hours | ||||
| - `INVALIDATE_REFRESH_TOKENS`: **false**: Check if refresh token has already been used | ||||
| - `JWT_SECRET`: **\<empty\>**: OAuth2 authentication secret for access and refresh tokens, change this a unique string. | ||||
| - `JWT_SIGNING_ALGORITHM`: **RS256**: Algorithm used to sign OAuth2 tokens. Valid values: \[`HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`\] | ||||
| - `JWT_SECRET`: **\<empty\>**: OAuth2 authentication secret for access and refresh tokens, change this to a unique string. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `HS256`, `HS384` or `HS512`. | ||||
| - `JWT_SIGNING_PRIVATE_KEY_FILE`: **jwt/private.pem**: Private key file path used to sign OAuth2 tokens. The path is relative to `CUSTOM_PATH`. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `RS256`, `RS384`, `RS512`, `ES256`, `ES384` or `ES512`. The file must contain a RSA or ECDSA private key in the PKCS8 format. | ||||
| - `MAX_TOKEN_LENGTH`: **32767**: Maximum length of token/cookie to accept from OAuth2 provider | ||||
| 
 | ||||
| ## i18n (`i18n`) | ||||
|  |  | |||
|  | @ -24,9 +24,12 @@ Gitea supports acting as an OAuth2 provider to allow third party applications to | |||
| ## Endpoints | ||||
| 
 | ||||
| | Endpoint                 | URL                                 | | ||||
| | ---------------------- | --------------------------- | | ||||
| | ------------------------ | ----------------------------------- | | ||||
| | OpenID Connect Discovery | `/.well-known/openid-configuration` | | ||||
| | Authorization Endpoint   | `/login/oauth/authorize`            | | ||||
| | Access Token Endpoint    | `/login/oauth/access_token`         | | ||||
| | OpenID Connect UserInfo  | `/login/oauth/userinfo`             | | ||||
| | JSON Web Key Set         | `/login/oauth/keys`                 | | ||||
| 
 | ||||
| ## Supported OAuth2 Grants | ||||
| 
 | ||||
|  |  | |||
|  | @ -132,6 +132,9 @@ func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) { | |||
| 
 | ||||
| // InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
 | ||||
| func InitOAuth2() error { | ||||
| 	if err := oauth2.InitSigningKey(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := oauth2.Init(x); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ import ( | |||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/auth/oauth2" | ||||
| 	"code.gitea.io/gitea/modules/secret" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
|  | @ -540,10 +540,10 @@ type OAuth2Token struct { | |||
| // ParseOAuth2Token parses a singed jwt string
 | ||||
| func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) { | ||||
| 	parsedToken, err := jwt.ParseWithClaims(jwtToken, &OAuth2Token{}, func(token *jwt.Token) (interface{}, error) { | ||||
| 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||||
| 		if token.Method == nil || token.Method.Alg() != oauth2.DefaultSigningKey.SigningMethod().Alg() { | ||||
| 			return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"]) | ||||
| 		} | ||||
| 		return setting.OAuth2.JWTSecretBytes, nil | ||||
| 		return oauth2.DefaultSigningKey.VerifyKey(), nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  | @ -559,8 +559,9 @@ func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) { | |||
| // SignToken signs the token with the JWT secret
 | ||||
| func (token *OAuth2Token) SignToken() (string, error) { | ||||
| 	token.IssuedAt = time.Now().Unix() | ||||
| 	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, token) | ||||
| 	return jwtToken.SignedString(setting.OAuth2.JWTSecretBytes) | ||||
| 	jwtToken := jwt.NewWithClaims(oauth2.DefaultSigningKey.SigningMethod(), token) | ||||
| 	oauth2.DefaultSigningKey.PreProcessToken(jwtToken) | ||||
| 	return jwtToken.SignedString(oauth2.DefaultSigningKey.SignKey()) | ||||
| } | ||||
| 
 | ||||
| // OIDCToken represents an OpenID Connect id_token
 | ||||
|  | @ -583,8 +584,9 @@ type OIDCToken struct { | |||
| } | ||||
| 
 | ||||
| // SignToken signs an id_token with the (symmetric) client secret key
 | ||||
| func (token *OIDCToken) SignToken(clientSecret string) (string, error) { | ||||
| func (token *OIDCToken) SignToken(signingKey oauth2.JWTSigningKey) (string, error) { | ||||
| 	token.IssuedAt = time.Now().Unix() | ||||
| 	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, token) | ||||
| 	return jwtToken.SignedString([]byte(clientSecret)) | ||||
| 	jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token) | ||||
| 	signingKey.PreProcessToken(jwtToken) | ||||
| 	return jwtToken.SignedString(signingKey.SignKey()) | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,378 @@ | |||
| // Copyright 2021 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package oauth2 | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/elliptic" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/sha256" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"math/big" | ||||
| 	"os" | ||||
| 	"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" | ||||
| 
 | ||||
| 	"github.com/dgrijalva/jwt-go" | ||||
| 	ini "gopkg.in/ini.v1" | ||||
| ) | ||||
| 
 | ||||
| // ErrInvalidAlgorithmType represents an invalid algorithm error.
 | ||||
| type ErrInvalidAlgorithmType struct { | ||||
| 	Algorightm string | ||||
| } | ||||
| 
 | ||||
| func (err ErrInvalidAlgorithmType) Error() string { | ||||
| 	return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorightm) | ||||
| } | ||||
| 
 | ||||
| // JWTSigningKey represents a algorithm/key pair to sign JWTs
 | ||||
| type JWTSigningKey interface { | ||||
| 	IsSymmetric() bool | ||||
| 	SigningMethod() jwt.SigningMethod | ||||
| 	SignKey() interface{} | ||||
| 	VerifyKey() interface{} | ||||
| 	ToJWK() (map[string]string, error) | ||||
| 	PreProcessToken(*jwt.Token) | ||||
| } | ||||
| 
 | ||||
| type hmacSigningKey struct { | ||||
| 	signingMethod jwt.SigningMethod | ||||
| 	secret        []byte | ||||
| } | ||||
| 
 | ||||
| func (key hmacSigningKey) IsSymmetric() bool { | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func (key hmacSigningKey) SigningMethod() jwt.SigningMethod { | ||||
| 	return key.signingMethod | ||||
| } | ||||
| 
 | ||||
| func (key hmacSigningKey) SignKey() interface{} { | ||||
| 	return key.secret | ||||
| } | ||||
| 
 | ||||
| func (key hmacSigningKey) VerifyKey() interface{} { | ||||
| 	return key.secret | ||||
| } | ||||
| 
 | ||||
| func (key hmacSigningKey) ToJWK() (map[string]string, error) { | ||||
| 	return map[string]string{ | ||||
| 		"kty": "oct", | ||||
| 		"alg": key.SigningMethod().Alg(), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (key hmacSigningKey) PreProcessToken(*jwt.Token) {} | ||||
| 
 | ||||
| type rsaSingingKey struct { | ||||
| 	signingMethod jwt.SigningMethod | ||||
| 	key           *rsa.PrivateKey | ||||
| 	id            string | ||||
| } | ||||
| 
 | ||||
| func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) { | ||||
| 	kid, err := createPublicKeyFingerprint(key.Public().(*rsa.PublicKey)) | ||||
| 	if err != nil { | ||||
| 		return rsaSingingKey{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return rsaSingingKey{ | ||||
| 		signingMethod, | ||||
| 		key, | ||||
| 		base64.RawURLEncoding.EncodeToString(kid), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (key rsaSingingKey) IsSymmetric() bool { | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func (key rsaSingingKey) SigningMethod() jwt.SigningMethod { | ||||
| 	return key.signingMethod | ||||
| } | ||||
| 
 | ||||
| func (key rsaSingingKey) SignKey() interface{} { | ||||
| 	return key.key | ||||
| } | ||||
| 
 | ||||
| func (key rsaSingingKey) VerifyKey() interface{} { | ||||
| 	return key.key.Public() | ||||
| } | ||||
| 
 | ||||
| func (key rsaSingingKey) ToJWK() (map[string]string, error) { | ||||
| 	pubKey := key.key.Public().(*rsa.PublicKey) | ||||
| 
 | ||||
| 	return map[string]string{ | ||||
| 		"kty": "RSA", | ||||
| 		"alg": key.SigningMethod().Alg(), | ||||
| 		"kid": key.id, | ||||
| 		"e":   base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()), | ||||
| 		"n":   base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (key rsaSingingKey) PreProcessToken(token *jwt.Token) { | ||||
| 	token.Header["kid"] = key.id | ||||
| } | ||||
| 
 | ||||
| type ecdsaSingingKey struct { | ||||
| 	signingMethod jwt.SigningMethod | ||||
| 	key           *ecdsa.PrivateKey | ||||
| 	id            string | ||||
| } | ||||
| 
 | ||||
| func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) { | ||||
| 	kid, err := createPublicKeyFingerprint(key.Public().(*ecdsa.PublicKey)) | ||||
| 	if err != nil { | ||||
| 		return ecdsaSingingKey{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return ecdsaSingingKey{ | ||||
| 		signingMethod, | ||||
| 		key, | ||||
| 		base64.RawURLEncoding.EncodeToString(kid), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (key ecdsaSingingKey) IsSymmetric() bool { | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod { | ||||
| 	return key.signingMethod | ||||
| } | ||||
| 
 | ||||
| func (key ecdsaSingingKey) SignKey() interface{} { | ||||
| 	return key.key | ||||
| } | ||||
| 
 | ||||
| func (key ecdsaSingingKey) VerifyKey() interface{} { | ||||
| 	return key.key.Public() | ||||
| } | ||||
| 
 | ||||
| func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { | ||||
| 	pubKey := key.key.Public().(*ecdsa.PublicKey) | ||||
| 
 | ||||
| 	return map[string]string{ | ||||
| 		"kty": "EC", | ||||
| 		"alg": key.SigningMethod().Alg(), | ||||
| 		"kid": key.id, | ||||
| 		"crv": pubKey.Params().Name, | ||||
| 		"x":   base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), | ||||
| 		"y":   base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) { | ||||
| 	token.Header["kid"] = key.id | ||||
| } | ||||
| 
 | ||||
| // createPublicKeyFingerprint creates a fingerprint of the given key.
 | ||||
| // The fingerprint is the sha256 sum of the PKIX structure of the key.
 | ||||
| func createPublicKeyFingerprint(key interface{}) ([]byte, error) { | ||||
| 	bytes, err := x509.MarshalPKIXPublicKey(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	checksum := sha256.Sum256(bytes) | ||||
| 
 | ||||
| 	return checksum[:], nil | ||||
| } | ||||
| 
 | ||||
| // CreateJWTSingingKey creates a signing key from an algorithm / key pair.
 | ||||
| func CreateJWTSingingKey(algorithm string, key interface{}) (JWTSigningKey, error) { | ||||
| 	var signingMethod jwt.SigningMethod | ||||
| 	switch algorithm { | ||||
| 	case "HS256": | ||||
| 		signingMethod = jwt.SigningMethodHS256 | ||||
| 	case "HS384": | ||||
| 		signingMethod = jwt.SigningMethodHS384 | ||||
| 	case "HS512": | ||||
| 		signingMethod = jwt.SigningMethodHS512 | ||||
| 
 | ||||
| 	case "RS256": | ||||
| 		signingMethod = jwt.SigningMethodRS256 | ||||
| 	case "RS384": | ||||
| 		signingMethod = jwt.SigningMethodRS384 | ||||
| 	case "RS512": | ||||
| 		signingMethod = jwt.SigningMethodRS512 | ||||
| 
 | ||||
| 	case "ES256": | ||||
| 		signingMethod = jwt.SigningMethodES256 | ||||
| 	case "ES384": | ||||
| 		signingMethod = jwt.SigningMethodES384 | ||||
| 	case "ES512": | ||||
| 		signingMethod = jwt.SigningMethodES512 | ||||
| 	default: | ||||
| 		return nil, ErrInvalidAlgorithmType{algorithm} | ||||
| 	} | ||||
| 
 | ||||
| 	switch signingMethod.(type) { | ||||
| 	case *jwt.SigningMethodECDSA: | ||||
| 		privateKey, ok := key.(*ecdsa.PrivateKey) | ||||
| 		if !ok { | ||||
| 			return nil, jwt.ErrInvalidKeyType | ||||
| 		} | ||||
| 		return newECDSASingingKey(signingMethod, privateKey) | ||||
| 	case *jwt.SigningMethodRSA: | ||||
| 		privateKey, ok := key.(*rsa.PrivateKey) | ||||
| 		if !ok { | ||||
| 			return nil, jwt.ErrInvalidKeyType | ||||
| 		} | ||||
| 		return newRSASingingKey(signingMethod, privateKey) | ||||
| 	default: | ||||
| 		secret, ok := key.([]byte) | ||||
| 		if !ok { | ||||
| 			return nil, jwt.ErrInvalidKeyType | ||||
| 		} | ||||
| 		return hmacSigningKey{signingMethod, secret}, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // DefaultSigningKey is the default signing key for JWTs.
 | ||||
| var DefaultSigningKey JWTSigningKey | ||||
| 
 | ||||
| // InitSigningKey creates the default signing key from settings or creates a random key.
 | ||||
| func InitSigningKey() error { | ||||
| 	var err error | ||||
| 	var key interface{} | ||||
| 
 | ||||
| 	switch setting.OAuth2.JWTSigningAlgorithm { | ||||
| 	case "HS256": | ||||
| 		fallthrough | ||||
| 	case "HS384": | ||||
| 		fallthrough | ||||
| 	case "HS512": | ||||
| 		key, err = loadOrCreateSymmetricKey() | ||||
| 
 | ||||
| 	case "RS256": | ||||
| 		fallthrough | ||||
| 	case "RS384": | ||||
| 		fallthrough | ||||
| 	case "RS512": | ||||
| 		fallthrough | ||||
| 	case "ES256": | ||||
| 		fallthrough | ||||
| 	case "ES384": | ||||
| 		fallthrough | ||||
| 	case "ES512": | ||||
| 		key, err = loadOrCreateAsymmetricKey() | ||||
| 
 | ||||
| 	default: | ||||
| 		return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm} | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error while loading or creating symmetric key: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	signingKey, err := CreateJWTSingingKey(setting.OAuth2.JWTSigningAlgorithm, key) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	DefaultSigningKey = signingKey | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // loadOrCreateSymmetricKey checks if the configured secret is valid.
 | ||||
| // If it is not valid a new secret is created and saved in the configuration file.
 | ||||
| func loadOrCreateSymmetricKey() (interface{}, error) { | ||||
| 	key := make([]byte, 32) | ||||
| 	n, err := base64.RawURLEncoding.Decode(key, []byte(setting.OAuth2.JWTSecretBase64)) | ||||
| 	if err != nil || n != 32 { | ||||
| 		key, err = generate.NewJwtSecret() | ||||
| 		if err != nil { | ||||
| 			log.Fatal("error generating JWT secret: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		setting.CreateOrAppendToCustomConf(func(cfg *ini.File) { | ||||
| 			secretBase64 := base64.RawURLEncoding.EncodeToString(key) | ||||
| 			cfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return key, nil | ||||
| } | ||||
| 
 | ||||
| // 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() (interface{}, error) { | ||||
| 	keyPath := setting.OAuth2.JWTSigningPrivateKeyFile | ||||
| 
 | ||||
| 	isExist, err := util.IsExist(keyPath) | ||||
| 	if err != nil { | ||||
| 		log.Fatal("Unable to check if %s exists. Error: %v", keyPath, err) | ||||
| 	} | ||||
| 	if !isExist { | ||||
| 		err := func() error { | ||||
| 			key, err := func() (interface{}, error) { | ||||
| 				if strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS") { | ||||
| 					return rsa.GenerateKey(rand.Reader, 4096) | ||||
| 				} | ||||
| 				return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||||
| 			}() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			bytes, err := x509.MarshalPKCS8PrivateKey(key) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes} | ||||
| 
 | ||||
| 			if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			defer func() { | ||||
| 				if err = f.Close(); err != nil { | ||||
| 					log.Error("Close: %v", err) | ||||
| 				} | ||||
| 			}() | ||||
| 
 | ||||
| 			return pem.Encode(f, privateKeyPEM) | ||||
| 		}() | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Error generating private key: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	bytes, err := ioutil.ReadFile(keyPath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	block, _ := pem.Decode(bytes) | ||||
| 	if block == nil { | ||||
| 		return nil, fmt.Errorf("no valid PEM data found in %s", keyPath) | ||||
| 	} else if block.Type != "PRIVATE KEY" { | ||||
| 		return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath) | ||||
| 	} | ||||
| 
 | ||||
| 	return x509.ParsePKCS8PrivateKey(block.Bytes) | ||||
| } | ||||
|  | @ -38,14 +38,23 @@ func NewInternalToken() (string, error) { | |||
| 	return internalToken, nil | ||||
| } | ||||
| 
 | ||||
| // NewJwtSecret generate a new value intended to be used by LFS_JWT_SECRET.
 | ||||
| func NewJwtSecret() (string, error) { | ||||
| 	JWTSecretBytes := make([]byte, 32) | ||||
| 	_, err := io.ReadFull(rand.Reader, JWTSecretBytes) | ||||
| // NewJwtSecret generates a new value intended to be used for JWT secrets.
 | ||||
| func NewJwtSecret() ([]byte, error) { | ||||
| 	bytes := make([]byte, 32) | ||||
| 	_, err := io.ReadFull(rand.Reader, bytes) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return bytes, nil | ||||
| } | ||||
| 
 | ||||
| // NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
 | ||||
| func NewJwtSecretBase64() (string, error) { | ||||
| 	bytes, err := NewJwtSecret() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return base64.RawURLEncoding.EncodeToString(JWTSecretBytes), nil | ||||
| 	return base64.RawURLEncoding.EncodeToString(bytes), nil | ||||
| } | ||||
| 
 | ||||
| // NewSecretKey generate a new value intended to be used by SECRET_KEY.
 | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ func newLFSService() { | |||
| 		n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64)) | ||||
| 
 | ||||
| 		if err != nil || n != 32 { | ||||
| 			LFS.JWTSecretBase64, err = generate.NewJwtSecret() | ||||
| 			LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64() | ||||
| 			if err != nil { | ||||
| 				log.Fatal("Error generating JWT Secret for custom config: %v", err) | ||||
| 				return | ||||
|  |  | |||
|  | @ -371,14 +371,17 @@ var ( | |||
| 		AccessTokenExpirationTime  int64 | ||||
| 		RefreshTokenExpirationTime int64 | ||||
| 		InvalidateRefreshTokens    bool | ||||
| 		JWTSecretBytes             []byte `ini:"-"` | ||||
| 		JWTSigningAlgorithm        string `ini:"JWT_SIGNING_ALGORITHM"` | ||||
| 		JWTSecretBase64            string `ini:"JWT_SECRET"` | ||||
| 		JWTSigningPrivateKeyFile   string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` | ||||
| 		MaxTokenLength             int | ||||
| 	}{ | ||||
| 		Enable:                     true, | ||||
| 		AccessTokenExpirationTime:  3600, | ||||
| 		RefreshTokenExpirationTime: 730, | ||||
| 		InvalidateRefreshTokens:    false, | ||||
| 		JWTSigningAlgorithm:        "RS256", | ||||
| 		JWTSigningPrivateKeyFile:   "jwt/private.pem", | ||||
| 		MaxTokenLength:             math.MaxInt16, | ||||
| 	} | ||||
| 
 | ||||
|  | @ -801,21 +804,8 @@ func NewContext() { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if OAuth2.Enable { | ||||
| 		OAuth2.JWTSecretBytes = make([]byte, 32) | ||||
| 		n, err := base64.RawURLEncoding.Decode(OAuth2.JWTSecretBytes, []byte(OAuth2.JWTSecretBase64)) | ||||
| 
 | ||||
| 		if err != nil || n != 32 { | ||||
| 			OAuth2.JWTSecretBase64, err = generate.NewJwtSecret() | ||||
| 			if err != nil { | ||||
| 				log.Fatal("error generating JWT secret: %v", err) | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			CreateOrAppendToCustomConf(func(cfg *ini.File) { | ||||
| 				cfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64) | ||||
| 			}) | ||||
| 		} | ||||
| 	if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { | ||||
| 		OAuth2.JWTSigningPrivateKeyFile = filepath.Join(CustomPath, OAuth2.JWTSigningPrivateKeyFile) | ||||
| 	} | ||||
| 
 | ||||
| 	sec = Cfg.Section("admin") | ||||
|  |  | |||
|  | @ -343,7 +343,7 @@ func SubmitInstall(ctx *context.Context) { | |||
| 		cfg.Section("server").Key("LFS_START_SERVER").SetValue("true") | ||||
| 		cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath) | ||||
| 		var secretKey string | ||||
| 		if secretKey, err = generate.NewJwtSecret(); err != nil { | ||||
| 		if secretKey, err = generate.NewJwtSecretBase64(); err != nil { | ||||
| 			ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) | ||||
| 			return | ||||
| 		} | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import ( | |||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/auth/oauth2" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
|  | @ -24,6 +25,7 @@ import ( | |||
| 
 | ||||
| 	"gitea.com/go-chi/binding" | ||||
| 	"github.com/dgrijalva/jwt-go" | ||||
| 	jsoniter "github.com/json-iterator/go" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -131,7 +133,7 @@ type AccessTokenResponse struct { | |||
| 	IDToken      string    `json:"id_token,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) { | ||||
| func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) { | ||||
| 	if setting.OAuth2.InvalidateRefreshTokens { | ||||
| 		if err := grant.IncreaseCounter(); err != nil { | ||||
| 			return nil, &AccessTokenError{ | ||||
|  | @ -223,7 +225,7 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*Ac | |||
| 			idToken.EmailVerified = app.User.IsActive | ||||
| 		} | ||||
| 
 | ||||
| 		signedIDToken, err = idToken.SignToken(clientSecret) | ||||
| 		signedIDToken, err = idToken.SignToken(signingKey) | ||||
| 		if err != nil { | ||||
| 			return nil, &AccessTokenError{ | ||||
| 				ErrorCode:        AccessTokenErrorCodeInvalidRequest, | ||||
|  | @ -480,12 +482,37 @@ func GrantApplicationOAuth(ctx *context.Context) { | |||
| func OIDCWellKnown(ctx *context.Context) { | ||||
| 	t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown") | ||||
| 	ctx.Resp.Header().Set("Content-Type", "application/json") | ||||
| 	ctx.Data["SigningKey"] = oauth2.DefaultSigningKey | ||||
| 	if err := t.Execute(ctx.Resp, ctx.Data); err != nil { | ||||
| 		log.Error("%v", err) | ||||
| 		ctx.Error(http.StatusInternalServerError) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // OIDCKeys generates the JSON Web Key Set
 | ||||
| func OIDCKeys(ctx *context.Context) { | ||||
| 	jwk, err := oauth2.DefaultSigningKey.ToJWK() | ||||
| 	if err != nil { | ||||
| 		log.Error("Error converting signing key to JWK: %v", err) | ||||
| 		ctx.Error(http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	jwk["use"] = "sig" | ||||
| 
 | ||||
| 	jwks := map[string][]map[string]string{ | ||||
| 		"keys": { | ||||
| 			jwk, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Resp.Header().Set("Content-Type", "application/json") | ||||
| 	enc := jsoniter.NewEncoder(ctx.Resp) | ||||
| 	if err := enc.Encode(jwks); err != nil { | ||||
| 		log.Error("Failed to encode representation as json. Error: %v", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // AccessTokenOAuth manages all access token requests by the client
 | ||||
| func AccessTokenOAuth(ctx *context.Context) { | ||||
| 	form := *web.GetForm(ctx).(*forms.AccessTokenForm) | ||||
|  | @ -513,13 +540,25 @@ func AccessTokenOAuth(ctx *context.Context) { | |||
| 			form.ClientSecret = pair[1] | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	signingKey := oauth2.DefaultSigningKey | ||||
| 	if signingKey.IsSymmetric() { | ||||
| 		clientKey, err := oauth2.CreateJWTSingingKey(signingKey.SigningMethod().Alg(), []byte(form.ClientSecret)) | ||||
| 		if err != nil { | ||||
| 			handleAccessTokenError(ctx, AccessTokenError{ | ||||
| 				ErrorCode:        AccessTokenErrorCodeInvalidRequest, | ||||
| 				ErrorDescription: "Error creating signing key", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		signingKey = clientKey | ||||
| 	} | ||||
| 
 | ||||
| 	switch form.GrantType { | ||||
| 	case "refresh_token": | ||||
| 		handleRefreshToken(ctx, form) | ||||
| 		return | ||||
| 		handleRefreshToken(ctx, form, signingKey) | ||||
| 	case "authorization_code": | ||||
| 		handleAuthorizationCode(ctx, form) | ||||
| 		return | ||||
| 		handleAuthorizationCode(ctx, form, signingKey) | ||||
| 	default: | ||||
| 		handleAccessTokenError(ctx, AccessTokenError{ | ||||
| 			ErrorCode:        AccessTokenErrorCodeUnsupportedGrantType, | ||||
|  | @ -528,7 +567,7 @@ func AccessTokenOAuth(ctx *context.Context) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) { | ||||
| func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) { | ||||
| 	token, err := models.ParseOAuth2Token(form.RefreshToken) | ||||
| 	if err != nil { | ||||
| 		handleAccessTokenError(ctx, AccessTokenError{ | ||||
|  | @ -556,7 +595,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) { | |||
| 		log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID) | ||||
| 		return | ||||
| 	} | ||||
| 	accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret) | ||||
| 	accessToken, tokenErr := newAccessTokenResponse(grant, signingKey) | ||||
| 	if tokenErr != nil { | ||||
| 		handleAccessTokenError(ctx, *tokenErr) | ||||
| 		return | ||||
|  | @ -564,7 +603,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) { | |||
| 	ctx.JSON(http.StatusOK, accessToken) | ||||
| } | ||||
| 
 | ||||
| func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) { | ||||
| func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) { | ||||
| 	app, err := models.GetOAuth2ApplicationByClientID(form.ClientID) | ||||
| 	if err != nil { | ||||
| 		handleAccessTokenError(ctx, AccessTokenError{ | ||||
|  | @ -618,7 +657,7 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) { | |||
| 			ErrorDescription: "cannot proceed your request", | ||||
| 		}) | ||||
| 	} | ||||
| 	resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret) | ||||
| 	resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, signingKey) | ||||
| 	if tokenErr != nil { | ||||
| 		handleAccessTokenError(ctx, *tokenErr) | ||||
| 		return | ||||
|  |  | |||
|  | @ -295,6 +295,7 @@ func RegisterRoutes(m *web.Route) { | |||
| 	}, ignSignInAndCsrf, reqSignIn) | ||||
| 	m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth) | ||||
| 	m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) | ||||
| 	m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys) | ||||
| 
 | ||||
| 	m.Group("/user/settings", func() { | ||||
| 		m.Get("", userSetting.Profile) | ||||
|  |  | |||
|  | @ -2,11 +2,18 @@ | |||
|     "issuer": "{{AppUrl | JSEscape | Safe}}", | ||||
|     "authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize", | ||||
|     "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token", | ||||
|     "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys", | ||||
|     "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo", | ||||
|     "response_types_supported": [ | ||||
|         "code", | ||||
|         "id_token" | ||||
|     ], | ||||
|     "id_token_signing_alg_values_supported": [ | ||||
|         "{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}" | ||||
|     ], | ||||
|     "subject_types_supported": [ | ||||
|         "public" | ||||
|     ], | ||||
|     "scopes_supported": [ | ||||
|         "openid", | ||||
|         "profile", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue