Refactor authenticator

Signed-off-by: hongming <hongming@kubesphere.io>
This commit is contained in:
hongming
2021-08-17 11:34:51 +08:00
parent 83df7d1ffd
commit 4b5b1c64bc
41 changed files with 1923 additions and 758 deletions

View File

@@ -17,24 +17,264 @@ limitations under the License.
package token
import (
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"hash/fnv"
"io/ioutil"
"time"
"gopkg.in/square/go-jose.v2"
"github.com/form3tech-oss/jwt-go"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
"k8s.io/apiserver/pkg/authentication/user"
)
const (
AccessToken TokenType = "access_token"
RefreshToken TokenType = "refresh_token"
StaticToken TokenType = "static_token"
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 TokenType string
type Type string
type IssueRequest struct {
User user.Info
ExpiresIn time.Duration
Claims
}
type VerifiedResponse struct {
User user.Info
Claims
}
// 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(user user.Info, tokenType TokenType, expiresIn time.Duration) (string, error)
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) (user.Info, TokenType, error)
Verify(string) (*VerifiedResponse, error)
}
type Claims struct {
jwt.StandardClaims
// Private Claim Names
// TokenType defined the type of the token
TokenType Type `json:"token_type"`
// Username is user identity same as `sub`
Username string `json:"username"`
// 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"`
// Extra contains the additional information
Extra map[string][]string `json:"extra,omitempty"`
}
type issuer struct {
// Issuer Identity
name string
// signing access_token and refresh_token
secret []byte
// signing id_token
signKey *jose.JSONWebKey
// Token verification maximum time difference
maximumClockSkew time.Duration
}
func (s *issuer) IssueTo(request *IssueRequest) (string, error) {
issueAt := time.Now().Unix()
claims := Claims{
Username: request.User.GetName(),
Extra: request.User.GetExtra(),
TokenType: request.TokenType,
StandardClaims: jwt.StandardClaims{
IssuedAt: issueAt,
Subject: request.User.GetName(),
Issuer: s.name,
},
}
if len(request.Audience) > 0 {
claims.Audience = request.Audience
}
if request.Nonce != "" {
claims.Nonce = request.Nonce
}
if request.ExpiresIn > 0 {
claims.ExpiresAt = claims.IssuedAt + int64(request.ExpiresIn.Seconds())
}
var token string
var err error
if request.TokenType == IDToken {
t := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
t.Header[headerKeyID] = s.signKey.KeyID
token, err = t.SignedString(s.signKey.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.Parser{
ValidMethods: []string{jwt.SigningMethodHS256.Alg(), jwt.SigningMethodRS256.Alg()},
UseJSONNumber: false,
SkipClaimsValidation: true,
}
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().Unix()
if claims.VerifyExpiresAt(now, false) == false {
delta := time.Unix(now, 0).Sub(time.Unix(claims.ExpiresAt, 0))
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 + int64(s.maximumClockSkew.Seconds())
if claims.VerifyIssuedAt(skewedTime, false) == 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) 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.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(options *authentication.Options) (*rsa.PrivateKey, string, error) {
var signKey *rsa.PrivateKey
var signKeyData []byte
var err error
if options.OAuthOptions.SignKey != "" {
signKeyData, err = ioutil.ReadFile(options.OAuthOptions.SignKey)
if err != nil {
klog.Errorf("issuer: failed to read private key file %s: %v", options.OAuthOptions.SignKey, err)
return nil, "", err
}
} else if options.OAuthOptions.SignKeyData != "" {
signKeyData, err = base64.StdEncoding.DecodeString(options.OAuthOptions.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(options *authentication.Options) (Issuer, error) {
// TODO(hongming) automatically rotates keys
signKey, keyID, err := loadSignKey(options)
if err != nil {
return nil, err
}
return &issuer{
name: options.OAuthOptions.Issuer,
secret: []byte(options.JwtSecret),
maximumClockSkew: options.MaximumClockSkew,
signKey: &jose.JSONWebKey{
Key: signKey,
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()
}