Files
kubesphere/pkg/apiserver/authentication/token/issuer.go
2025-04-30 15:53:51 +08:00

318 lines
8.4 KiB
Go

/*
* Copyright 2024 the KubeSphere Authors.
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package token
import (
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"hash/fnv"
"os"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/golang-jwt/jwt/v4"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog/v2"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)
const (
AccessToken Type = "access_token"
RefreshToken Type = "refresh_token"
StaticToken Type = "static_token"
AuthorizationCode Type = "code"
IDToken Type = "id_token"
headerKeyID string = "kid"
headerAlgorithm string = "alg"
)
type Type string
type IssueRequest struct {
User user.Info
ExpiresIn time.Duration
Claims
}
type VerifiedResponse struct {
User user.Info
Claims
}
// Keys hold encryption and signing keys.
type Keys struct {
SigningKey *jose.JSONWebKey
SigningKeyPub *jose.JSONWebKey
}
// Issuer issues token to user, tokens are required to perform mutating requests to resources
type Issuer interface {
// IssueTo issues a token a User, return error if issuing process failed
IssueTo(request *IssueRequest) (string, error)
// Verify verifies a token, and return a user info if it's a valid token, otherwise return error
Verify(string) (*VerifiedResponse, error)
// Keys hold encryption and signing keys.
Keys() *Keys
}
type Claims struct {
jwt.RegisteredClaims
// Private Claim Names
// TokenType defined the type of the token
TokenType Type `json:"token_type,omitempty"`
// Username user identity, deprecated field
Username string `json:"username,omitempty"`
// Extra contains the additional information
Extra map[string][]string `json:"extra,omitempty"`
// Used for issuing authorization code
// Scopes can be used to request that specific sets of information be made available as Claim Values.
Scopes []string `json:"scopes,omitempty"`
// The following is well-known ID Token fields
// End-User's full url in displayable form including all url parts,
// possibly including titles and suffixes, ordered according to the End-User's locale and preferences.
Name string `json:"url,omitempty"`
// String value used to associate a Client session with an ID Token, and to mitigate replay attacks.
// The value is passed through unmodified from the Authentication Request to the ID Token.
Nonce string `json:"nonce,omitempty"`
// End-User's preferred e-mail address.
Email string `json:"email,omitempty"`
// End-User's locale, represented as a BCP47 [RFC5646] language tag.
Locale string `json:"locale,omitempty"`
// Shorthand url by which the End-User wishes to be referred to at the RP,
PreferredUsername string `json:"preferred_username,omitempty"`
}
type issuer struct {
// Issuer Identifier
url string
// signing access_token and refresh_token
secret []byte
// signing id_token
signKey *Keys
// Token verification maximum time difference
maximumClockSkew time.Duration
}
func (s *issuer) IssueTo(request *IssueRequest) (string, error) {
issueAt := time.Now()
claims := Claims{
Username: request.User.GetName(),
Extra: request.User.GetExtra(),
TokenType: request.TokenType,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(issueAt),
Subject: request.User.GetName(),
Issuer: s.url,
},
}
if len(request.Audience) > 0 {
claims.Audience = request.Audience
}
if request.Name != "" {
claims.Name = request.Name
}
if request.Nonce != "" {
claims.Nonce = request.Nonce
}
if request.Email != "" {
claims.Email = request.Email
}
if request.PreferredUsername != "" {
claims.PreferredUsername = request.PreferredUsername
}
if request.Locale != "" {
claims.Locale = request.Locale
}
if len(request.Scopes) > 0 {
claims.Scopes = request.Scopes
}
if request.ExpiresIn > 0 {
claims.ExpiresAt = jwt.NewNumericDate(issueAt.Add(request.ExpiresIn))
}
var token string
var err error
if request.TokenType == IDToken {
t := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
t.Header[headerKeyID] = s.signKey.SigningKey.KeyID
token, err = t.SignedString(s.signKey.SigningKey.Key)
} else {
token, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.secret)
}
if err != nil {
klog.Warningf("jwt: failed to issue token: %v", err)
return "", err
}
return token, nil
}
func (s *issuer) Verify(token string) (*VerifiedResponse, error) {
parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg(), jwt.SigningMethodRS256.Alg()}),
jwt.WithoutClaimsValidation())
var claims Claims
_, err := parser.ParseWithClaims(token, &claims, s.keyFunc)
if err != nil {
klog.Warningf("jwt: failed to parse token: %v", err)
return nil, err
}
now := time.Now()
if !claims.VerifyExpiresAt(now, false) {
delta := now.Sub(claims.ExpiresAt.Time)
err = fmt.Errorf("jwt: token is expired by %v", delta)
klog.V(4).Info(err)
return nil, err
}
// allowing a clock skew when checking the time-based values.
skewedTime := now.Add(s.maximumClockSkew)
if !claims.VerifyIssuedAt(skewedTime, false) {
err = fmt.Errorf("jwt: token used before issued, iat:%v, now:%v", claims.IssuedAt, now)
klog.Warning(err)
return nil, err
}
verified := &VerifiedResponse{
User: &user.DefaultInfo{
Name: claims.Username,
Extra: claims.Extra,
},
Claims: claims,
}
return verified, nil
}
func (s *issuer) Keys() *Keys {
return s.signKey
}
func (s *issuer) keyFunc(token *jwt.Token) (i interface{}, err error) {
alg, _ := token.Header[headerAlgorithm].(string)
switch alg {
case jwt.SigningMethodHS256.Alg():
return s.secret, nil
case jwt.SigningMethodRS256.Alg():
return s.signKey.SigningKey.Key, nil
default:
return nil, fmt.Errorf("unexpect signature algorithm %v", token.Header[headerAlgorithm])
}
}
func loadPrivateKey(data []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("private key not in pem format")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to key file: %v", err)
}
return key, nil
}
func generatePrivateKeyData() ([]byte, error) {
privateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %v", err)
}
data := x509.MarshalPKCS1PrivateKey(privateKey)
pemData := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: data,
},
)
return pemData, nil
}
func loadSignKey(config *oauth.IssuerOptions) (*rsa.PrivateKey, string, error) {
var signKey *rsa.PrivateKey
var signKeyData []byte
var err error
if config.SignKey != "" {
signKeyData, err = os.ReadFile(config.SignKey)
if err != nil {
klog.Errorf("issuer: failed to read private key file %s: %v", config.SignKey, err)
return nil, "", err
}
} else if config.SignKeyData != "" {
signKeyData, err = base64.StdEncoding.DecodeString(config.SignKeyData)
if err != nil {
klog.Errorf("issuer: failed to decode sign key data: %s", err)
return nil, "", err
}
}
// automatically generate private key
if len(signKeyData) == 0 {
signKeyData, err = generatePrivateKeyData()
if err != nil {
klog.Errorf("issuer: failed to generate private key: %v", err)
return nil, "", err
}
}
if len(signKeyData) > 0 {
signKey, err = loadPrivateKey(signKeyData)
if err != nil {
klog.Errorf("issuer: failed to load private key from data: %v", err)
}
}
keyID := fmt.Sprint(fnv32a(signKeyData))
return signKey, keyID, nil
}
func NewIssuer(config *oauth.IssuerOptions) (Issuer, error) {
// TODO(hongming) automatically rotates keys
signKey, keyID, err := loadSignKey(config)
if err != nil {
return nil, err
}
return &issuer{
url: config.URL,
secret: []byte(config.JWTSecret),
maximumClockSkew: config.MaximumClockSkew,
signKey: &Keys{
SigningKey: &jose.JSONWebKey{
Key: signKey,
KeyID: keyID,
Algorithm: jwt.SigningMethodRS256.Alg(),
Use: "sig",
},
SigningKeyPub: &jose.JSONWebKey{
Key: signKey.Public(),
KeyID: keyID,
Algorithm: jwt.SigningMethodRS256.Alg(),
Use: "sig",
},
},
}, nil
}
// fnv32a hashes using fnv32a algorithm
func fnv32a(data []byte) uint32 {
algorithm := fnv.New32a()
algorithm.Write(data)
return algorithm.Sum32()
}