Files
kubesphere/pkg/apiserver/authentication/token/issuer.go
hongming 97326a89b9 add userinfo endpoint
Signed-off-by: hongming <hongming@kubesphere.io>
2021-09-17 18:03:32 +08:00

333 lines
8.9 KiB
Go

/*
Copyright 2020 The KubeSphere Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
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 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.StandardClaims
// 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 name in displayable form including all name parts,
// possibly including titles and suffixes, ordered according to the End-User's locale and preferences.
Name string `json:"name,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 name by which the End-User wishes to be referred to at the RP,
PreferredUsername string `json:"preferred_username,omitempty"`
}
type issuer struct {
// Issuer Identity
name 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().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.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 = 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.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.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) 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(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: &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()
}