JWT token generation/verification

This commit is contained in:
Jordan Liggitt 2015-05-01 12:02:38 -04:00
parent 6e570732f5
commit db1f0dc906
7 changed files with 536 additions and 10 deletions

View File

@ -24,6 +24,8 @@
{% if grains.cloud is defined -%}
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
{% set service_account_key = " --service_account_private_key_file=/srv/kubernetes/server.key " -%}
{% if grains.cloud == 'gce' -%}
{% if grains.cloud_config is defined -%}
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
@ -55,7 +57,7 @@
{% endif -%}
{% endif -%}
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + " " + pillar['log_level'] -%}
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + service_account_key + pillar['log_level'] -%}
{
"apiVersion": "v1beta3",

View File

@ -67,6 +67,8 @@ type APIServer struct {
BasicAuthFile string
ClientCAFile string
TokenAuthFile string
ServiceAccountKeyFile string
ServiceAccountLookup bool
AuthorizationMode string
AuthorizationPolicyFile string
AdmissionControl string
@ -162,6 +164,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.")
fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.")
fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
@ -267,7 +271,11 @@ func (s *APIServer) Run(_ []string) error {
n := net.IPNet(s.PortalNet)
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile)
// Default to the private server key for service account token signing
if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" {
s.ServiceAccountKeyFile = s.TLSPrivateKeyFile
}
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, client)
if err != nil {
glog.Fatalf("Invalid Authentication Config: %v", err)
}

View File

@ -20,7 +20,6 @@ limitations under the License.
package app
import (
"fmt"
"net"
"net/http"
"net/http/pprof"
@ -75,6 +74,7 @@ type CMServer struct {
PodEvictionTimeout time.Duration
DeletingPodsQps float32
DeletingPodsBurst int
ServiceAccountKeyFile string
// TODO: Discover these by pinging the host machines, and rip out these params.
NodeMilliCPU int64
@ -143,6 +143,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) {
"Amount of time which we allow starting Node to be unresponsive before marking it unhealty.")
fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second,
"The period for syncing NodeStatus in NodeController.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA key used to sign service account tokens.")
// TODO: Discover these by pinging the host machines, and rip out these flags.
// TODO: in the meantime, use resource.QuantityFlag() instead of these
fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node")
@ -251,12 +252,24 @@ func (s *CMServer) Run(_ []string) error {
pvclaimBinder.Run()
}
// TODO: generate signed token
tokenGenerator := serviceaccount.TokenGeneratorFunc(func(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
return fmt.Sprintf("serviceaccount:%s:%s:%s:%s", serviceAccount.Namespace, serviceAccount.Name, serviceAccount.UID, secret.Name), nil
})
serviceaccount.NewTokensController(kubeClient, serviceaccount.DefaultTokenControllerOptions(tokenGenerator)).Run()
serviceaccount.NewServiceAccountsController(kubeClient, serviceaccount.DefaultServiceAccountControllerOptions()).Run()
if len(s.ServiceAccountKeyFile) > 0 {
privateKey, err := serviceaccount.ReadPrivateKey(s.ServiceAccountKeyFile)
if err != nil {
glog.Errorf("Error reading key for service account token controller: %v", err)
} else {
serviceaccount.NewTokensController(
kubeClient,
serviceaccount.DefaultTokenControllerOptions(
serviceaccount.JWTTokenGenerator(privateKey),
),
).Run()
}
}
serviceaccount.NewServiceAccountsController(
kubeClient,
serviceaccount.DefaultServiceAccountControllerOptions(),
).Run()
select {}
return nil

View File

@ -132,12 +132,21 @@ trap cleanup EXIT
echo "Starting etcd"
kube::etcd::start
SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-false}
SERVICE_ACCOUNT_KEY=${SERVICE_ACCOUNT_KEY:-"/var/run/kubernetes/serviceaccount.key"}
# Generate ServiceAccount key if needed
if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
openssl genrsa -out "${SERVICE_ACCOUNT_KEY}" 2048 2>/dev/null
fi
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
APISERVER_LOG=/tmp/kube-apiserver.log
sudo -E "${GO_OUT}/kube-apiserver" \
--v=${LOG_LEVEL} \
--service_account_key_file="${SERVICE_ACCOUNT_KEY}" \
--service_account_lookup="${SERVICE_ACCOUNT_LOOKUP}" \
--admission_control="${ADMISSION_CONTROL}" \
--address="${API_HOST}" \
--port="${API_PORT}" \
@ -155,6 +164,7 @@ CTLRMGR_LOG=/tmp/kube-controller-manager.log
sudo -E "${GO_OUT}/kube-controller-manager" \
--v=${LOG_LEVEL} \
--machines="127.0.0.1" \
--service_account_private_key_file="${SERVICE_ACCOUNT_KEY}" \
--master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 &
CTLRMGR_PID=$!

View File

@ -17,8 +17,12 @@ limitations under the License.
package apiserver
import (
"crypto/rsa"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
@ -28,7 +32,7 @@ import (
)
// NewAuthenticator returns an authenticator.Request or an error
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authenticator.Request, error) {
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, client client.Interface) (authenticator.Request, error) {
var authenticators []authenticator.Request
if len(basicAuthFile) > 0 {
@ -55,6 +59,14 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authentica
authenticators = append(authenticators, tokenAuth)
}
if len(serviceAccountKeyFile) > 0 {
serviceAccountAuth, err := newServiceAccountAuthenticator(serviceAccountKeyFile, serviceAccountLookup, client)
if err != nil {
return nil, err
}
authenticators = append(authenticators, serviceAccountAuth)
}
switch len(authenticators) {
case 0:
return nil, nil
@ -85,6 +97,16 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request,
return bearertoken.New(tokenAuthenticator), nil
}
// newServiceAccountAuthenticator returns an authenticator.Request or an error
func newServiceAccountAuthenticator(keyfile string, lookup bool, client client.Interface) (authenticator.Request, error) {
publicKey, err := serviceaccount.ReadPublicKey(keyfile)
if err != nil {
return nil, err
}
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, client)
return bearertoken.New(tokenAuthenticator), nil
}
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) {
roots, err := util.CertPoolFromFile(clientCAFile)

228
pkg/serviceaccount/jwt.go Normal file
View File

@ -0,0 +1,228 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"bytes"
"crypto/rsa"
"errors"
"fmt"
"io/ioutil"
"strings"
jwt "github.com/dgrijalva/jwt-go"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
)
const (
ServiceAccountUsernamePrefix = "serviceaccount"
ServiceAccountUsernameSeparator = ":"
Issuer = "kubernetes/serviceaccount"
SubjectClaim = "sub"
IssuerClaim = "iss"
ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name"
ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid"
SecretNameClaim = "kubernetes.io/serviceaccount/secret.name"
NamespaceClaim = "kubernetes.io/serviceaccount/namespace"
)
type TokenGenerator interface {
// GenerateToken generates a token which will identify the given ServiceAccount.
// The returned token will be stored in the given (and yet-unpersisted) Secret.
GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error)
}
// ReadPrivateKey is a helper function for reading an rsa.PrivateKey from a PEM-encoded file
func ReadPrivateKey(file string) (*rsa.PrivateKey, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
return jwt.ParseRSAPrivateKeyFromPEM(data)
}
// ReadPublicKey is a helper function for reading an rsa.PublicKey from a PEM-encoded file
// Reads public keys from both public and private key files
func ReadPublicKey(file string) (*rsa.PublicKey, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil {
return &privateKey.PublicKey, nil
}
return jwt.ParseRSAPublicKeyFromPEM(data)
}
// MakeUsername generates a username from the given namespace and ServiceAccount name.
// The resulting username can be passed to SplitUsername to extract the original namespace and ServiceAccount name.
func MakeUsername(namespace, name string) string {
return strings.Join([]string{ServiceAccountUsernamePrefix, namespace, name}, ServiceAccountUsernameSeparator)
}
// SplitUsername returns the namespace and ServiceAccount name embedded in the given username,
// or an error if the username is not a valid name produced by MakeUsername
func SplitUsername(username string) (string, string, error) {
parts := strings.Split(username, ServiceAccountUsernameSeparator)
if len(parts) != 3 || parts[0] != ServiceAccountUsernamePrefix || len(parts[1]) == 0 || len(parts[2]) == 0 {
return "", "", fmt.Errorf("Username must be in the form %s", MakeUsername("namespace", "name"))
}
return parts[1], parts[2], nil
}
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
// privateKey is a PEM-encoded byte array of a private RSA key.
// JWTTokenAuthenticator()
func JWTTokenGenerator(key *rsa.PrivateKey) TokenGenerator {
return &jwtTokenGenerator{key}
}
type jwtTokenGenerator struct {
key *rsa.PrivateKey
}
func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
token := jwt.New(jwt.SigningMethodRS256)
// Identify the issuer
token.Claims[IssuerClaim] = Issuer
// Username: `serviceaccount:<namespace>:<serviceaccount>`
token.Claims[SubjectClaim] = MakeUsername(serviceAccount.Namespace, serviceAccount.Name)
// Persist enough structured info for the authenticator to be able to look up the service account and secret
token.Claims[NamespaceClaim] = serviceAccount.Namespace
token.Claims[ServiceAccountNameClaim] = serviceAccount.Name
token.Claims[ServiceAccountUIDClaim] = serviceAccount.UID
token.Claims[SecretNameClaim] = secret.Name
// Sign and get the complete encoded token as a string
return token.SignedString(j.key)
}
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified using the given client
func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, client client.Interface) authenticator.Token {
return &jwtTokenAuthenticator{keys, lookup, client}
}
type jwtTokenAuthenticator struct {
keys []*rsa.PublicKey
lookup bool
client client.Interface
}
func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
var validationError error
for _, key := range j.keys {
// Attempt to verify with each key until we find one that works
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
switch err := err.(type) {
case *jwt.ValidationError:
if (err.Errors & jwt.ValidationErrorMalformed) != 0 {
// Not a JWT, no point in continuing
return nil, false, nil
}
if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 {
// Signature error, perhaps one of the other keys will verify the signature
// If not, we want to return this error
validationError = err
continue
}
}
// Other errors should just return as errors
return nil, false, err
}
// If we get here, we have a token with a recognized signature
// Make sure we issued the token
iss, _ := parsedToken.Claims[IssuerClaim].(string)
if iss != Issuer {
return nil, false, nil
}
// Make sure the claims we need exist
sub, _ := parsedToken.Claims[SubjectClaim].(string)
if len(sub) == 0 {
return nil, false, errors.New("sub claim is missing")
}
namespace, _ := parsedToken.Claims[NamespaceClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("namespace claim is missing")
}
secretName, _ := parsedToken.Claims[SecretNameClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("secretName claim is missing")
}
serviceAccountName, _ := parsedToken.Claims[ServiceAccountNameClaim].(string)
if len(serviceAccountName) == 0 {
return nil, false, errors.New("serviceAccountName claim is missing")
}
serviceAccountUID, _ := parsedToken.Claims[ServiceAccountUIDClaim].(string)
if len(serviceAccountUID) == 0 {
return nil, false, errors.New("serviceAccountUID claim is missing")
}
if j.lookup {
// Make sure token hasn't been invalidated by deletion of the secret
secret, err := j.client.Secrets(namespace).Get(secretName)
if err != nil {
return nil, false, errors.New("Token has been invalidated")
}
if bytes.Compare(secret.Data[api.ServiceAccountTokenKey], []byte(token)) != 0 {
return nil, false, errors.New("Token does not match server's copy")
}
// Make sure service account still exists (name and UID)
serviceAccount, err := j.client.ServiceAccounts(namespace).Get(serviceAccountName)
if err != nil {
return nil, false, err
}
if string(serviceAccount.UID) != serviceAccountUID {
return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
}
}
return &user.DefaultInfo{
Name: sub,
UID: serviceAccountUID,
Groups: []string{},
}, true, nil
}
return nil, false, validationError
}

View File

@ -0,0 +1,243 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"crypto/rsa"
"io/ioutil"
"os"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
"github.com/dgrijalva/jwt-go"
)
const otherPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArXz0QkIG1B5Bj2/W69GH
rsm5e+RC3kE+VTgocge0atqlLBek35tRqLgUi3AcIrBZ/0YctMSWDVcRt5fkhWwe
Lqjj6qvAyNyOkrkBi1NFDpJBjYJtuKHgRhNxXbOzTSNpdSKXTfOkzqv56MwHOP25
yP/NNAODUtr92D5ySI5QX8RbXW+uDn+ixul286PBW/BCrE4tuS88dA0tYJPf8LCu
sqQOwlXYH/rNUg4Pyl9xxhR5DIJR0OzNNfChjw60zieRIt2LfM83fXhwk8IxRGkc
gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0
PwIDAQAB
-----END PUBLIC KEY-----`
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx
OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI
zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG
51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU
j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi
B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM
WwIDAQAB
-----END PUBLIC KEY-----
`
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26
e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy
EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T
ctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh/AjjCnMnkgvKT2tpKxYQ59P
gDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp
1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRMWwIDAQABAoIBAHJx8GqyCBDNbqk7
e7/hI9iE1S10Wwol5GH2RWxqX28cYMKq+8aE2LI1vPiXO89xOgelk4DN6urX6xjK
ZBF8RRIMQy/e/O2F4+3wl+Nl4vOXV1u6iVXMsD6JRg137mqJf1Fr9elg1bsaRofL
Q7CxPoB8dhS+Qb+hj0DhlqhgA9zG345CQCAds0ZYAZe8fP7bkwrLqZpMn7Dz9WVm
++YgYYKjuE95kPuup/LtWfA9rJyE/Fws8/jGvRSpVn1XglMLSMKhLd27sE8ZUSV0
2KUzbfRGE0+AnRULRrjpYaPu0XQ2JjdNvtkjBnv27RB89W9Gklxq821eH1Y8got8
FZodjxECgYEA93pz7AQZ2xDs67d1XLCzpX84GxKzttirmyj3OIlxgzVHjEMsvw8v
sjFiBU5xEEQDosrBdSknnlJqyiq1YwWG/WDckr13d8G2RQWoySN7JVmTQfXcLoTu
YGRiiTuoEi3ab3ZqrgGrFgX7T/cHuasbYvzCvhM2b4VIR3aSxU2DTUMCgYEA4x7J
T/ErP6GkU5nKstu/mIXwNzayEO1BJvPYsy7i7EsxTm3xe/b8/6cYOz5fvJLGH5mT
Q8YvuLqBcMwZardrYcwokD55UvNLOyfADDFZ6l3WntIqbA640Ok2g1X4U8J09xIq
ZLIWK1yWbbvi4QCeN5hvWq47e8sIj5QHjIIjRwkCgYEAyNqjltxFN9zmzPDa2d24
EAvOt3pYTYBQ1t9KtqImdL0bUqV6fZ6PsWoPCgt+DBuHb+prVPGP7Bkr/uTmznU/
+AlTO+12NsYLbr2HHagkXE31DEXE7CSLa8RNjN/UKtz4Ohq7vnowJvG35FCz/mb3
FUHbtHTXa2+bGBUOTf/5Hw0CgYBxw0r9EwUhw1qnUYJ5op7OzFAtp+T7m4ul8kCa
SCL8TxGsgl+SQ34opE775dtYfoBk9a0RJqVit3D8yg71KFjOTNAIqHJm/Vyyjc+h
i9rJDSXiuczsAVfLtPVMRfS0J9QkqeG4PIfkQmVLI/CZ2ZBmsqEcX+eFs4ZfPLun
Qsxe2QKBgGuPilIbLeIBDIaPiUI0FwU8v2j8CEQBYvoQn34c95hVQsig/o5z7zlo
UsO0wlTngXKlWdOcCs1kqEhTLrstf48djDxAYAxkw40nzeJOt7q52ib/fvf4/UBy
X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ
-----END RSA PRIVATE KEY-----
`
func getPrivateKey(data string) *rsa.PrivateKey {
key, _ := jwt.ParseRSAPrivateKeyFromPEM([]byte(data))
return key
}
func getPublicKey(data string) *rsa.PublicKey {
key, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(data))
return key
}
func TestReadPrivateKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(privateKey), os.FileMode(0600)); err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
if _, err := ReadPrivateKey(f.Name()); err != nil {
t.Fatalf("error reading key: %v", err)
}
}
func TestReadPublicKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(publicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
if _, err := ReadPublicKey(f.Name()); err != nil {
t.Fatalf("error reading key: %v", err)
}
}
func TestTokenGenerateAndValidate(t *testing.T) {
expectedUserName := "serviceaccount:test:my-service-account"
expectedUserUID := "12345"
// Related API objects
serviceAccount := &api.ServiceAccount{
ObjectMeta: api.ObjectMeta{
Name: "my-service-account",
UID: "12345",
Namespace: "test",
},
}
secret := &api.Secret{
ObjectMeta: api.ObjectMeta{
Name: "my-secret",
Namespace: "test",
},
}
// Generate the token
generator := JWTTokenGenerator(getPrivateKey(privateKey))
token, err := generator.GenerateToken(*serviceAccount, *secret)
if err != nil {
t.Fatalf("error generating token: %v", err)
}
if len(token) == 0 {
t.Fatalf("no token generated")
}
// "Save" the token
secret.Data = map[string][]byte{
"token": []byte(token),
}
testCases := map[string]struct {
Client client.Interface
Keys []*rsa.PublicKey
ExpectedErr bool
ExpectedOK bool
ExpectedUserName string
ExpectedUserUID string
}{
"no keys": {
Client: nil,
Keys: []*rsa.PublicKey{},
ExpectedErr: false,
ExpectedOK: false,
},
"invalid keys": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
"valid key": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
},
"rotated keys": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey), getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
},
"valid lookup": {
Client: testclient.NewSimpleFake(serviceAccount, secret),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
},
"invalid secret lookup": {
Client: testclient.NewSimpleFake(serviceAccount),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
"invalid serviceaccount lookup": {
Client: testclient.NewSimpleFake(secret),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
}
for k, tc := range testCases {
authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, tc.Client)
user, ok, err := authenticator.AuthenticateToken(token)
if (err != nil) != tc.ExpectedErr {
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
continue
}
if ok != tc.ExpectedOK {
t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
continue
}
if err != nil || !ok {
continue
}
if user.GetName() != tc.ExpectedUserName {
t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, user.GetName())
continue
}
if user.GetUID() != tc.ExpectedUserUID {
t.Errorf("%s: Expected userUID=%v, got %v", k, tc.ExpectedUserUID, user.GetUID())
continue
}
}
}