mirror of
https://github.com/kubernetes/client-go.git
synced 2025-10-21 22:09:33 +00:00
remove the top-level folders for versions
remove scripts
This commit is contained in:
200
plugin/pkg/auth/authenticator/token/oidc/testing/provider.go
Normal file
200
plugin/pkg/auth/authenticator/token/oidc/testing/provider.go
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes 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 testing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
)
|
||||
|
||||
// NewOIDCProvider provides a bare minimum OIDC IdP Server useful for testing.
|
||||
func NewOIDCProvider(t *testing.T, issuerPath string) *OIDCProvider {
|
||||
privKey, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create OIDC Provider: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
op := &OIDCProvider{
|
||||
Mux: http.NewServeMux(),
|
||||
PrivKey: privKey,
|
||||
issuerPath: issuerPath,
|
||||
}
|
||||
|
||||
op.Mux.HandleFunc(path.Join(issuerPath, "/.well-known/openid-configuration"), op.handleConfig)
|
||||
op.Mux.HandleFunc(path.Join(issuerPath, "/keys"), op.handleKeys)
|
||||
|
||||
return op
|
||||
}
|
||||
|
||||
type OIDCProvider struct {
|
||||
Mux *http.ServeMux
|
||||
PCFG oidc.ProviderConfig
|
||||
PrivKey *key.PrivateKey
|
||||
issuerPath string
|
||||
}
|
||||
|
||||
func (op *OIDCProvider) ServeTLSWithKeyPair(cert, key string) (*httptest.Server, error) {
|
||||
srv := httptest.NewUnstartedServer(op.Mux)
|
||||
|
||||
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
||||
var err error
|
||||
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot load cert/key pair: %v", err)
|
||||
}
|
||||
srv.StartTLS()
|
||||
|
||||
// The issuer's URL is extended by an optional path. This ensures that the plugin can
|
||||
// handle issuers that use a non-root path for discovery (see kubernetes/kubernetes#29749).
|
||||
srv.URL = srv.URL + op.issuerPath
|
||||
|
||||
u, err := url.Parse(srv.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pathFor := func(p string) *url.URL {
|
||||
u2 := *u // Shallow copy.
|
||||
u2.Path = path.Join(u2.Path, p)
|
||||
return &u2
|
||||
}
|
||||
|
||||
op.PCFG = oidc.ProviderConfig{
|
||||
Issuer: u,
|
||||
AuthEndpoint: pathFor("/auth"),
|
||||
TokenEndpoint: pathFor("/token"),
|
||||
KeysEndpoint: pathFor("/keys"),
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IDTokenSigningAlgValues: []string{"RS256"},
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func (op *OIDCProvider) handleConfig(w http.ResponseWriter, req *http.Request) {
|
||||
b, err := json.Marshal(&op.PCFG)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (op *OIDCProvider) handleKeys(w http.ResponseWriter, req *http.Request) {
|
||||
keys := struct {
|
||||
Keys []jose.JWK `json:"keys"`
|
||||
}{
|
||||
Keys: []jose.JWK{op.PrivKey.JWK()},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(time.Hour.Seconds())))
|
||||
w.Header().Set("Expires", time.Now().Add(time.Hour).Format(time.RFC1123))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
|
||||
// This method is mostly identical to crypto.GenerateSelfSignedCert except for the 'IsCA' and 'KeyUsage'
|
||||
// in the certificate template. (Maybe we can merge these two methods).
|
||||
func GenerateSelfSignedCert(t *testing.T, host, certPath, keyPath string) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365),
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, host)
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate cert
|
||||
certBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate key
|
||||
keyBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write cert
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(certPath, certBuffer.Bytes(), os.FileMode(0644)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write key
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(keyPath, keyBuffer.Bytes(), os.FileMode(0600)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
105
plugin/pkg/client/auth/gcp/gcp.go
Normal file
105
plugin/pkg/client/auth/gcp/gcp.go
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes 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 gcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"k8s.io/client-go/1.5/rest"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := rest.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil {
|
||||
glog.Fatalf("Failed to register gcp auth plugin: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type gcpAuthProvider struct {
|
||||
tokenSource oauth2.TokenSource
|
||||
persister rest.AuthProviderConfigPersister
|
||||
}
|
||||
|
||||
func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister rest.AuthProviderConfigPersister) (rest.AuthProvider, error) {
|
||||
ts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gcpAuthProvider{ts, persister}, nil
|
||||
}
|
||||
|
||||
func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
||||
return &oauth2.Transport{
|
||||
Source: g.tokenSource,
|
||||
Base: rt,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gcpAuthProvider) Login() error { return nil }
|
||||
|
||||
type cachedTokenSource struct {
|
||||
source oauth2.TokenSource
|
||||
accessToken string
|
||||
expiry time.Time
|
||||
persister rest.AuthProviderConfigPersister
|
||||
}
|
||||
|
||||
func newCachedTokenSource(accessToken, expiry string, persister rest.AuthProviderConfigPersister) (*cachedTokenSource, error) {
|
||||
var expiryTime time.Time
|
||||
if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil {
|
||||
expiryTime = parsedTime
|
||||
}
|
||||
ts, err := google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cachedTokenSource{
|
||||
source: ts,
|
||||
accessToken: accessToken,
|
||||
expiry: expiryTime,
|
||||
persister: persister,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *cachedTokenSource) Token() (*oauth2.Token, error) {
|
||||
tok := &oauth2.Token{
|
||||
AccessToken: t.accessToken,
|
||||
TokenType: "Bearer",
|
||||
Expiry: t.expiry,
|
||||
}
|
||||
if tok.Valid() && !tok.Expiry.IsZero() {
|
||||
return tok, nil
|
||||
}
|
||||
tok, err := t.source.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.persister != nil {
|
||||
cached := map[string]string{
|
||||
"access-token": tok.AccessToken,
|
||||
"expiry": tok.Expiry.Format(time.RFC3339Nano),
|
||||
}
|
||||
if err := t.persister.Persist(cached); err != nil {
|
||||
glog.V(4).Infof("Failed to persist token: %v", err)
|
||||
}
|
||||
}
|
||||
return tok, nil
|
||||
}
|
2
plugin/pkg/client/auth/oidc/OWNERS
Normal file
2
plugin/pkg/client/auth/oidc/OWNERS
Normal file
@@ -0,0 +1,2 @@
|
||||
assignees:
|
||||
- ericchiang
|
270
plugin/pkg/client/auth/oidc/oidc.go
Normal file
270
plugin/pkg/client/auth/oidc/oidc.go
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes 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 oidc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/util/wait"
|
||||
"k8s.io/client-go/1.5/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
cfgIssuerUrl = "idp-issuer-url"
|
||||
cfgClientID = "client-id"
|
||||
cfgClientSecret = "client-secret"
|
||||
cfgCertificateAuthority = "idp-certificate-authority"
|
||||
cfgCertificateAuthorityData = "idp-certificate-authority-data"
|
||||
cfgExtraScopes = "extra-scopes"
|
||||
cfgIDToken = "id-token"
|
||||
cfgRefreshToken = "refresh-token"
|
||||
)
|
||||
|
||||
var (
|
||||
backoff = wait.Backoff{
|
||||
Duration: 1 * time.Second,
|
||||
Factor: 2,
|
||||
Jitter: .1,
|
||||
Steps: 5,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := rest.RegisterAuthProviderPlugin("oidc", newOIDCAuthProvider); err != nil {
|
||||
glog.Fatalf("Failed to register oidc auth plugin: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newOIDCAuthProvider(_ string, cfg map[string]string, persister rest.AuthProviderConfigPersister) (rest.AuthProvider, error) {
|
||||
issuer := cfg[cfgIssuerUrl]
|
||||
if issuer == "" {
|
||||
return nil, fmt.Errorf("Must provide %s", cfgIssuerUrl)
|
||||
}
|
||||
|
||||
clientID := cfg[cfgClientID]
|
||||
if clientID == "" {
|
||||
return nil, fmt.Errorf("Must provide %s", cfgClientID)
|
||||
}
|
||||
|
||||
clientSecret := cfg[cfgClientSecret]
|
||||
if clientSecret == "" {
|
||||
return nil, fmt.Errorf("Must provide %s", cfgClientSecret)
|
||||
}
|
||||
|
||||
var certAuthData []byte
|
||||
var err error
|
||||
if cfg[cfgCertificateAuthorityData] != "" {
|
||||
certAuthData, err = base64.StdEncoding.DecodeString(cfg[cfgCertificateAuthorityData])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
clientConfig := rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
CAFile: cfg[cfgCertificateAuthority],
|
||||
CAData: certAuthData,
|
||||
},
|
||||
}
|
||||
|
||||
trans, err := rest.TransportFor(&clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hc := &http.Client{Transport: trans}
|
||||
|
||||
providerCfg, err := oidc.FetchProviderConfig(hc, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching provider config: %v", err)
|
||||
}
|
||||
|
||||
scopes := strings.Split(cfg[cfgExtraScopes], ",")
|
||||
oidcCfg := oidc.ClientConfig{
|
||||
HTTPClient: hc,
|
||||
Credentials: oidc.ClientCredentials{
|
||||
ID: clientID,
|
||||
Secret: clientSecret,
|
||||
},
|
||||
ProviderConfig: providerCfg,
|
||||
Scope: append(scopes, oidc.DefaultScope...),
|
||||
}
|
||||
|
||||
client, err := oidc.NewClient(oidcCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating OIDC Client: %v", err)
|
||||
}
|
||||
|
||||
oClient := &oidcClient{client}
|
||||
|
||||
var initialIDToken jose.JWT
|
||||
if cfg[cfgIDToken] != "" {
|
||||
initialIDToken, err = jose.ParseJWT(cfg[cfgIDToken])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &oidcAuthProvider{
|
||||
initialIDToken: initialIDToken,
|
||||
refresher: &idTokenRefresher{
|
||||
client: oClient,
|
||||
cfg: cfg,
|
||||
persister: persister,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type oidcAuthProvider struct {
|
||||
refresher *idTokenRefresher
|
||||
initialIDToken jose.JWT
|
||||
}
|
||||
|
||||
func (g *oidcAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
||||
at := &oidc.AuthenticatedTransport{
|
||||
TokenRefresher: g.refresher,
|
||||
RoundTripper: rt,
|
||||
}
|
||||
at.SetJWT(g.initialIDToken)
|
||||
return &roundTripper{
|
||||
wrapped: at,
|
||||
refresher: g.refresher,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *oidcAuthProvider) Login() error {
|
||||
return errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
type OIDCClient interface {
|
||||
refreshToken(rt string) (oauth2.TokenResponse, error)
|
||||
verifyJWT(jwt jose.JWT) error
|
||||
}
|
||||
|
||||
type roundTripper struct {
|
||||
refresher *idTokenRefresher
|
||||
wrapped *oidc.AuthenticatedTransport
|
||||
}
|
||||
|
||||
func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var res *http.Response
|
||||
var err error
|
||||
firstTime := true
|
||||
wait.ExponentialBackoff(backoff, func() (bool, error) {
|
||||
if !firstTime {
|
||||
var jwt jose.JWT
|
||||
jwt, err = r.refresher.Refresh()
|
||||
if err != nil {
|
||||
return true, nil
|
||||
}
|
||||
r.wrapped.SetJWT(jwt)
|
||||
} else {
|
||||
firstTime = false
|
||||
}
|
||||
|
||||
res, err = r.wrapped.RoundTrip(req)
|
||||
if err != nil {
|
||||
return true, nil
|
||||
}
|
||||
if res.StatusCode == http.StatusUnauthorized {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
type idTokenRefresher struct {
|
||||
cfg map[string]string
|
||||
client OIDCClient
|
||||
persister rest.AuthProviderConfigPersister
|
||||
intialIDToken jose.JWT
|
||||
}
|
||||
|
||||
func (r *idTokenRefresher) Verify(jwt jose.JWT) error {
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
exp, ok, err := claims.TimeClaim("exp")
|
||||
switch {
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to parse 'exp' claim: %v", err)
|
||||
case !ok:
|
||||
return errors.New("missing required 'exp' claim")
|
||||
case exp.Before(now):
|
||||
return fmt.Errorf("token already expired at: %v", exp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *idTokenRefresher) Refresh() (jose.JWT, error) {
|
||||
rt, ok := r.cfg[cfgRefreshToken]
|
||||
if !ok {
|
||||
return jose.JWT{}, errors.New("No valid id-token, and cannot refresh without refresh-token")
|
||||
}
|
||||
|
||||
tokens, err := r.client.refreshToken(rt)
|
||||
if err != nil {
|
||||
return jose.JWT{}, fmt.Errorf("could not refresh token: %v", err)
|
||||
}
|
||||
jwt, err := jose.ParseJWT(tokens.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
if tokens.RefreshToken != "" && tokens.RefreshToken != rt {
|
||||
r.cfg[cfgRefreshToken] = tokens.RefreshToken
|
||||
}
|
||||
r.cfg[cfgIDToken] = jwt.Encode()
|
||||
|
||||
err = r.persister.Persist(r.cfg)
|
||||
if err != nil {
|
||||
return jose.JWT{}, fmt.Errorf("could not perist new tokens: %v", err)
|
||||
}
|
||||
|
||||
return jwt, r.client.verifyJWT(jwt)
|
||||
}
|
||||
|
||||
type oidcClient struct {
|
||||
client *oidc.Client
|
||||
}
|
||||
|
||||
func (o *oidcClient) refreshToken(rt string) (oauth2.TokenResponse, error) {
|
||||
oac, err := o.client.OAuthClient()
|
||||
if err != nil {
|
||||
return oauth2.TokenResponse{}, err
|
||||
}
|
||||
|
||||
return oac.RequestToken(oauth2.GrantTypeRefreshToken, rt)
|
||||
}
|
||||
|
||||
func (o *oidcClient) verifyJWT(jwt jose.JWT) error {
|
||||
return o.client.VerifyJWT(jwt)
|
||||
}
|
646
plugin/pkg/client/auth/oidc/oidc_test.go
Normal file
646
plugin/pkg/client/auth/oidc/oidc_test.go
Normal file
@@ -0,0 +1,646 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes 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 oidc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/util/diff"
|
||||
"k8s.io/client-go/1.5/pkg/util/wait"
|
||||
oidctesting "k8s.io/client-go/1.5/plugin/pkg/auth/authenticator/token/oidc/testing"
|
||||
)
|
||||
|
||||
func TestNewOIDCAuthProvider(t *testing.T) {
|
||||
tempDir, err := ioutil.TempDir(os.TempDir(), "oidc_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot make temp dir %v", err)
|
||||
}
|
||||
cert := path.Join(tempDir, "oidc-cert")
|
||||
key := path.Join(tempDir, "oidc-key")
|
||||
|
||||
defer os.Remove(cert)
|
||||
defer os.Remove(key)
|
||||
defer os.Remove(tempDir)
|
||||
|
||||
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert, key)
|
||||
op := oidctesting.NewOIDCProvider(t, "")
|
||||
srv, err := op.ServeTLSWithKeyPair(cert, key)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot start server %v", err)
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
certData, err := ioutil.ReadFile(cert)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read cert bytes %v", err)
|
||||
}
|
||||
|
||||
jwt, err := jose.NewSignedJWT(jose.Claims(map[string]interface{}{
|
||||
"test": "jwt",
|
||||
}), op.PrivKey.Signer())
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create signed JWT %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
cfg map[string]string
|
||||
|
||||
wantErr bool
|
||||
wantInitialIDToken jose.JWT
|
||||
}{
|
||||
{
|
||||
// A Valid configuration
|
||||
cfg: map[string]string{
|
||||
cfgIssuerUrl: srv.URL,
|
||||
cfgCertificateAuthority: cert,
|
||||
cfgClientID: "client-id",
|
||||
cfgClientSecret: "client-secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
// A Valid configuration with an Initial JWT
|
||||
cfg: map[string]string{
|
||||
cfgIssuerUrl: srv.URL,
|
||||
cfgCertificateAuthority: cert,
|
||||
cfgClientID: "client-id",
|
||||
cfgClientSecret: "client-secret",
|
||||
cfgIDToken: jwt.Encode(),
|
||||
},
|
||||
wantInitialIDToken: *jwt,
|
||||
},
|
||||
{
|
||||
// Valid config, but using cfgCertificateAuthorityData
|
||||
cfg: map[string]string{
|
||||
cfgIssuerUrl: srv.URL,
|
||||
cfgCertificateAuthorityData: base64.StdEncoding.EncodeToString(certData),
|
||||
cfgClientID: "client-id",
|
||||
cfgClientSecret: "client-secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Missing client id
|
||||
cfg: map[string]string{
|
||||
cfgIssuerUrl: srv.URL,
|
||||
cfgCertificateAuthority: cert,
|
||||
cfgClientSecret: "client-secret",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
// Missing client secret
|
||||
cfg: map[string]string{
|
||||
cfgIssuerUrl: srv.URL,
|
||||
cfgCertificateAuthority: cert,
|
||||
cfgClientID: "client-id",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
// Missing issuer url.
|
||||
cfg: map[string]string{
|
||||
cfgCertificateAuthority: cert,
|
||||
cfgClientID: "client-id",
|
||||
cfgClientSecret: "secret",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
// No TLS config
|
||||
cfg: map[string]string{
|
||||
cfgIssuerUrl: srv.URL,
|
||||
cfgClientID: "client-id",
|
||||
cfgClientSecret: "secret",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
ap, err := newOIDCAuthProvider("cluster.example.com", tt.cfg, nil)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("case %d: want non-nil err", i)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error on newOIDCAuthProvider: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
oidcAP, ok := ap.(*oidcAuthProvider)
|
||||
if !ok {
|
||||
t.Errorf("case %d: expected ap to be an oidcAuthProvider", i)
|
||||
continue
|
||||
}
|
||||
|
||||
if diff := compareJWTs(tt.wantInitialIDToken, oidcAP.initialIDToken); diff != "" {
|
||||
t.Errorf("case %d: compareJWTs(tt.wantInitialIDToken, oidcAP.initialIDToken)=%v", i, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapTranport(t *testing.T) {
|
||||
oldBackoff := backoff
|
||||
defer func() {
|
||||
backoff = oldBackoff
|
||||
}()
|
||||
backoff = wait.Backoff{
|
||||
Duration: 1 * time.Nanosecond,
|
||||
Steps: 3,
|
||||
}
|
||||
|
||||
privKey, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("can't generate private key: %v", err)
|
||||
}
|
||||
|
||||
makeToken := func(s string, exp time.Time, count int) *jose.JWT {
|
||||
jwt, err := jose.NewSignedJWT(jose.Claims(map[string]interface{}{
|
||||
"test": s,
|
||||
"exp": exp.UTC().Unix(),
|
||||
"count": count,
|
||||
}), privKey.Signer())
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create signed JWT %v", err)
|
||||
}
|
||||
return jwt
|
||||
}
|
||||
|
||||
goodToken := makeToken("good", time.Now().Add(time.Hour), 0)
|
||||
goodToken2 := makeToken("good", time.Now().Add(time.Hour), 1)
|
||||
expiredToken := makeToken("good", time.Now().Add(-time.Hour), 0)
|
||||
|
||||
str := func(s string) *string {
|
||||
return &s
|
||||
}
|
||||
tests := []struct {
|
||||
cfgIDToken *jose.JWT
|
||||
cfgRefreshToken *string
|
||||
|
||||
expectRequests []testRoundTrip
|
||||
|
||||
expectRefreshes []testRefresh
|
||||
|
||||
expectPersists []testPersist
|
||||
|
||||
wantStatus int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
// Initial JWT is set, it is good, it is set as bearer.
|
||||
cfgIDToken: goodToken,
|
||||
|
||||
expectRequests: []testRoundTrip{
|
||||
{
|
||||
expectBearerToken: goodToken.Encode(),
|
||||
returnHTTPStatus: 200,
|
||||
},
|
||||
},
|
||||
|
||||
wantStatus: 200,
|
||||
},
|
||||
{
|
||||
// Initial JWT is set, but it's expired, so it gets refreshed.
|
||||
cfgIDToken: expiredToken,
|
||||
cfgRefreshToken: str("rt1"),
|
||||
|
||||
expectRefreshes: []testRefresh{
|
||||
{
|
||||
expectRefreshToken: "rt1",
|
||||
returnTokens: oauth2.TokenResponse{
|
||||
IDToken: goodToken.Encode(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectRequests: []testRoundTrip{
|
||||
{
|
||||
expectBearerToken: goodToken.Encode(),
|
||||
returnHTTPStatus: 200,
|
||||
},
|
||||
},
|
||||
|
||||
expectPersists: []testPersist{
|
||||
{
|
||||
cfg: map[string]string{
|
||||
cfgIDToken: goodToken.Encode(),
|
||||
cfgRefreshToken: "rt1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
wantStatus: 200,
|
||||
},
|
||||
{
|
||||
// Initial JWT is set, but it's expired, so it gets refreshed - this
|
||||
// time the refresh token itself is also refreshed
|
||||
cfgIDToken: expiredToken,
|
||||
cfgRefreshToken: str("rt1"),
|
||||
|
||||
expectRefreshes: []testRefresh{
|
||||
{
|
||||
expectRefreshToken: "rt1",
|
||||
returnTokens: oauth2.TokenResponse{
|
||||
IDToken: goodToken.Encode(),
|
||||
RefreshToken: "rt2",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectRequests: []testRoundTrip{
|
||||
{
|
||||
expectBearerToken: goodToken.Encode(),
|
||||
returnHTTPStatus: 200,
|
||||
},
|
||||
},
|
||||
|
||||
expectPersists: []testPersist{
|
||||
{
|
||||
cfg: map[string]string{
|
||||
cfgIDToken: goodToken.Encode(),
|
||||
cfgRefreshToken: "rt2",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
wantStatus: 200,
|
||||
},
|
||||
{
|
||||
// Initial JWT is not set, so it gets refreshed.
|
||||
cfgRefreshToken: str("rt1"),
|
||||
|
||||
expectRefreshes: []testRefresh{
|
||||
{
|
||||
expectRefreshToken: "rt1",
|
||||
returnTokens: oauth2.TokenResponse{
|
||||
IDToken: goodToken.Encode(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectRequests: []testRoundTrip{
|
||||
{
|
||||
expectBearerToken: goodToken.Encode(),
|
||||
returnHTTPStatus: 200,
|
||||
},
|
||||
},
|
||||
|
||||
expectPersists: []testPersist{
|
||||
{
|
||||
cfg: map[string]string{
|
||||
cfgIDToken: goodToken.Encode(),
|
||||
cfgRefreshToken: "rt1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
wantStatus: 200,
|
||||
},
|
||||
{
|
||||
// Expired token, but no refresh token.
|
||||
cfgIDToken: expiredToken,
|
||||
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
// Initial JWT is not set, so it gets refreshed, but the server
|
||||
// rejects it when it is used, so it refreshes again, which
|
||||
// succeeds.
|
||||
cfgRefreshToken: str("rt1"),
|
||||
|
||||
expectRefreshes: []testRefresh{
|
||||
{
|
||||
expectRefreshToken: "rt1",
|
||||
returnTokens: oauth2.TokenResponse{
|
||||
IDToken: goodToken.Encode(),
|
||||
},
|
||||
},
|
||||
{
|
||||
expectRefreshToken: "rt1",
|
||||
returnTokens: oauth2.TokenResponse{
|
||||
IDToken: goodToken2.Encode(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectRequests: []testRoundTrip{
|
||||
{
|
||||
expectBearerToken: goodToken.Encode(),
|
||||
returnHTTPStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
expectBearerToken: goodToken2.Encode(),
|
||||
returnHTTPStatus: http.StatusOK,
|
||||
},
|
||||
},
|
||||
|
||||
expectPersists: []testPersist{
|
||||
{
|
||||
cfg: map[string]string{
|
||||
cfgIDToken: goodToken.Encode(),
|
||||
cfgRefreshToken: "rt1",
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: map[string]string{
|
||||
cfgIDToken: goodToken2.Encode(),
|
||||
cfgRefreshToken: "rt1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
wantStatus: 200,
|
||||
},
|
||||
{
|
||||
// Initial JWT is but the server rejects it when it is used, so it
|
||||
// refreshes again, which succeeds.
|
||||
cfgRefreshToken: str("rt1"),
|
||||
cfgIDToken: goodToken,
|
||||
|
||||
expectRefreshes: []testRefresh{
|
||||
{
|
||||
expectRefreshToken: "rt1",
|
||||
returnTokens: oauth2.TokenResponse{
|
||||
IDToken: goodToken2.Encode(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expectRequests: []testRoundTrip{
|
||||
{
|
||||
expectBearerToken: goodToken.Encode(),
|
||||
returnHTTPStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
expectBearerToken: goodToken2.Encode(),
|
||||
returnHTTPStatus: http.StatusOK,
|
||||
},
|
||||
},
|
||||
|
||||
expectPersists: []testPersist{
|
||||
{
|
||||
cfg: map[string]string{
|
||||
cfgIDToken: goodToken2.Encode(),
|
||||
cfgRefreshToken: "rt1",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: 200,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
client := &testOIDCClient{
|
||||
refreshes: tt.expectRefreshes,
|
||||
}
|
||||
|
||||
persister := &testPersister{
|
||||
tt.expectPersists,
|
||||
}
|
||||
|
||||
cfg := map[string]string{}
|
||||
if tt.cfgIDToken != nil {
|
||||
cfg[cfgIDToken] = tt.cfgIDToken.Encode()
|
||||
}
|
||||
|
||||
if tt.cfgRefreshToken != nil {
|
||||
cfg[cfgRefreshToken] = *tt.cfgRefreshToken
|
||||
}
|
||||
|
||||
ap := &oidcAuthProvider{
|
||||
refresher: &idTokenRefresher{
|
||||
client: client,
|
||||
cfg: cfg,
|
||||
persister: persister,
|
||||
},
|
||||
}
|
||||
|
||||
if tt.cfgIDToken != nil {
|
||||
ap.initialIDToken = *tt.cfgIDToken
|
||||
}
|
||||
|
||||
tstRT := &testRoundTripper{
|
||||
tt.expectRequests,
|
||||
}
|
||||
|
||||
rt := ap.WrapTransport(tstRT)
|
||||
|
||||
req, err := http.NewRequest("GET", "http://cluster.example.com", nil)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error making request: %v", i, err)
|
||||
}
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("case %d: Expected non-nil error", i)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("case %d: unexpected error making round trip: %v", i, err)
|
||||
|
||||
} else {
|
||||
if res.StatusCode != tt.wantStatus {
|
||||
t.Errorf("case %d: want=%d, got=%d", i, tt.wantStatus, res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.verify(); err != nil {
|
||||
t.Errorf("case %d: %v", i, err)
|
||||
}
|
||||
|
||||
if err = persister.verify(); err != nil {
|
||||
t.Errorf("case %d: %v", i, err)
|
||||
}
|
||||
|
||||
if err = tstRT.verify(); err != nil {
|
||||
t.Errorf("case %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type testRoundTrip struct {
|
||||
expectBearerToken string
|
||||
returnHTTPStatus int
|
||||
}
|
||||
|
||||
type testRoundTripper struct {
|
||||
trips []testRoundTrip
|
||||
}
|
||||
|
||||
func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if len(t.trips) == 0 {
|
||||
return nil, errors.New("unexpected RoundTrip call")
|
||||
}
|
||||
|
||||
var trip testRoundTrip
|
||||
trip, t.trips = t.trips[0], t.trips[1:]
|
||||
|
||||
var bt string
|
||||
var parts []string
|
||||
auth := strings.TrimSpace(req.Header.Get("Authorization"))
|
||||
if auth == "" {
|
||||
goto Compare
|
||||
}
|
||||
|
||||
parts = strings.Split(auth, " ")
|
||||
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
goto Compare
|
||||
}
|
||||
|
||||
bt = parts[1]
|
||||
|
||||
Compare:
|
||||
if trip.expectBearerToken != bt {
|
||||
return nil, fmt.Errorf("want bearerToken=%v, got=%v", trip.expectBearerToken, bt)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: trip.returnHTTPStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *testRoundTripper) verify() error {
|
||||
if l := len(t.trips); l > 0 {
|
||||
return fmt.Errorf("%d uncalled round trips", l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testPersist struct {
|
||||
cfg map[string]string
|
||||
returnErr error
|
||||
}
|
||||
|
||||
type testPersister struct {
|
||||
persists []testPersist
|
||||
}
|
||||
|
||||
func (t *testPersister) Persist(cfg map[string]string) error {
|
||||
if len(t.persists) == 0 {
|
||||
return errors.New("unexpected persist call")
|
||||
}
|
||||
|
||||
var persist testPersist
|
||||
persist, t.persists = t.persists[0], t.persists[1:]
|
||||
|
||||
if !reflect.DeepEqual(persist.cfg, cfg) {
|
||||
return fmt.Errorf("Unexpected cfg: %v", diff.ObjectDiff(persist.cfg, cfg))
|
||||
}
|
||||
|
||||
return persist.returnErr
|
||||
}
|
||||
|
||||
func (t *testPersister) verify() error {
|
||||
if l := len(t.persists); l > 0 {
|
||||
return fmt.Errorf("%d uncalled persists", l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testRefresh struct {
|
||||
expectRefreshToken string
|
||||
|
||||
returnErr error
|
||||
returnTokens oauth2.TokenResponse
|
||||
}
|
||||
|
||||
type testOIDCClient struct {
|
||||
refreshes []testRefresh
|
||||
}
|
||||
|
||||
func (o *testOIDCClient) refreshToken(rt string) (oauth2.TokenResponse, error) {
|
||||
if len(o.refreshes) == 0 {
|
||||
return oauth2.TokenResponse{}, errors.New("unexpected refresh request")
|
||||
}
|
||||
|
||||
var refresh testRefresh
|
||||
refresh, o.refreshes = o.refreshes[0], o.refreshes[1:]
|
||||
|
||||
if rt != refresh.expectRefreshToken {
|
||||
return oauth2.TokenResponse{}, fmt.Errorf("want rt=%v, got=%v",
|
||||
refresh.expectRefreshToken,
|
||||
rt)
|
||||
}
|
||||
|
||||
if refresh.returnErr != nil {
|
||||
return oauth2.TokenResponse{}, refresh.returnErr
|
||||
}
|
||||
|
||||
return refresh.returnTokens, nil
|
||||
}
|
||||
|
||||
func (o *testOIDCClient) verifyJWT(jwt jose.JWT) error {
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
claim, _, _ := claims.StringClaim("test")
|
||||
if claim != "good" {
|
||||
return errors.New("bad token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testOIDCClient) verify() error {
|
||||
if l := len(t.refreshes); l > 0 {
|
||||
return fmt.Errorf("%d uncalled refreshes", l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareJWTs(a, b jose.JWT) string {
|
||||
if a.Encode() == b.Encode() {
|
||||
return ""
|
||||
}
|
||||
|
||||
var aClaims, bClaims jose.Claims
|
||||
for _, j := range []struct {
|
||||
claims *jose.Claims
|
||||
jwt jose.JWT
|
||||
}{
|
||||
{&aClaims, a},
|
||||
{&bClaims, b},
|
||||
} {
|
||||
var err error
|
||||
*j.claims, err = j.jwt.Claims()
|
||||
if err != nil {
|
||||
*j.claims = jose.Claims(map[string]interface{}{
|
||||
"msg": "bad claims",
|
||||
"err": err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return diff.ObjectDiff(aClaims, bClaims)
|
||||
}
|
23
plugin/pkg/client/auth/plugins.go
Normal file
23
plugin/pkg/client/auth/plugins.go
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes 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 auth
|
||||
|
||||
import (
|
||||
// Initialize all known client auth plugins.
|
||||
_ "k8s.io/client-go/1.5/plugin/pkg/client/auth/gcp"
|
||||
_ "k8s.io/client-go/1.5/plugin/pkg/client/auth/oidc"
|
||||
)
|
Reference in New Issue
Block a user