mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-13 13:55:41 +00:00
Merge pull request #25270 from bobbyrullo/deps
Implement OIDC client AuthProvider
This commit is contained in:
commit
7170c8910d
10
Godeps/Godeps.json
generated
10
Godeps/Godeps.json
generated
@ -485,23 +485,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-oidc/http",
|
"ImportPath": "github.com/coreos/go-oidc/http",
|
||||||
"Rev": "d7cb66526fffc811d602b6770581064f4b66b507"
|
"Rev": "5cf2aa52da8c574d3aa4458f471ad6ae2240fe6b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-oidc/jose",
|
"ImportPath": "github.com/coreos/go-oidc/jose",
|
||||||
"Rev": "d7cb66526fffc811d602b6770581064f4b66b507"
|
"Rev": "5cf2aa52da8c574d3aa4458f471ad6ae2240fe6b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-oidc/key",
|
"ImportPath": "github.com/coreos/go-oidc/key",
|
||||||
"Rev": "d7cb66526fffc811d602b6770581064f4b66b507"
|
"Rev": "5cf2aa52da8c574d3aa4458f471ad6ae2240fe6b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-oidc/oauth2",
|
"ImportPath": "github.com/coreos/go-oidc/oauth2",
|
||||||
"Rev": "d7cb66526fffc811d602b6770581064f4b66b507"
|
"Rev": "5cf2aa52da8c574d3aa4458f471ad6ae2240fe6b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-oidc/oidc",
|
"ImportPath": "github.com/coreos/go-oidc/oidc",
|
||||||
"Rev": "d7cb66526fffc811d602b6770581064f4b66b507"
|
"Rev": "5cf2aa52da8c574d3aa4458f471ad6ae2240fe6b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-semver/semver",
|
"ImportPath": "github.com/coreos/go-semver/semver",
|
||||||
|
@ -17,99 +17,24 @@ limitations under the License.
|
|||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
"github.com/coreos/go-oidc/jose"
|
||||||
"github.com/coreos/go-oidc/key"
|
|
||||||
"github.com/coreos/go-oidc/oidc"
|
"github.com/coreos/go-oidc/oidc"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/auth/user"
|
"k8s.io/kubernetes/pkg/auth/user"
|
||||||
|
oidctesting "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type oidcProvider struct {
|
func generateToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string, iat, exp time.Time) string {
|
||||||
mux *http.ServeMux
|
signer := op.PrivKey.Signer()
|
||||||
pcfg oidc.ProviderConfig
|
|
||||||
privKey *key.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOIDCProvider(t *testing.T) *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,
|
|
||||||
}
|
|
||||||
|
|
||||||
op.mux.HandleFunc("/.well-known/openid-configuration", op.handleConfig)
|
|
||||||
op.mux.HandleFunc("/keys", op.handleKeys)
|
|
||||||
|
|
||||||
return op
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustParseURL(t *testing.T, s string) *url.URL {
|
|
||||||
u, err := url.Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse url: %v", err)
|
|
||||||
}
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string, iat, exp time.Time) string {
|
|
||||||
signer := op.privKey.Signer()
|
|
||||||
claims := oidc.NewClaims(iss, sub, aud, iat, exp)
|
claims := oidc.NewClaims(iss, sub, aud, iat, exp)
|
||||||
claims.Add(usernameClaim, value)
|
claims.Add(usernameClaim, value)
|
||||||
if groups != nil && groupsClaim != "" {
|
if groups != nil && groupsClaim != "" {
|
||||||
@ -124,79 +49,16 @@ func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, userna
|
|||||||
return jwt.Encode()
|
return jwt.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (op *oidcProvider) generateGoodToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
func generateGoodToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
||||||
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour))
|
return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (op *oidcProvider) generateMalformedToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
func generateMalformedToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
||||||
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits"
|
return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (op *oidcProvider) generateExpiredToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
func generateExpiredToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
||||||
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
|
return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCDiscoveryTimeout(t *testing.T) {
|
func TestOIDCDiscoveryTimeout(t *testing.T) {
|
||||||
@ -217,20 +79,17 @@ func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) {
|
|||||||
defer os.Remove(cert)
|
defer os.Remove(cert)
|
||||||
defer os.Remove(key)
|
defer os.Remove(key)
|
||||||
|
|
||||||
generateSelfSignedCert(t, "127.0.0.1", cert, key)
|
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert, key)
|
||||||
|
|
||||||
op := newOIDCProvider(t)
|
op := oidctesting.NewOIDCProvider(t)
|
||||||
srv := httptest.NewUnstartedServer(op.mux)
|
srv, err := op.ServeTLSWithKeyPair(cert, key)
|
||||||
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
|
||||||
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot load cert/key pair: %v", err)
|
t.Fatalf("Cannot start server %v", err)
|
||||||
}
|
}
|
||||||
srv.StartTLS()
|
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
op.pcfg = oidc.ProviderConfig{
|
op.PCFG = oidc.ProviderConfig{
|
||||||
Issuer: mustParseURL(t, srv.URL), // An invalid ProviderConfig. Keys endpoint is required.
|
Issuer: oidctesting.MustParseURL(srv.URL), // An invalid ProviderConfig. Keys endpoint is required.
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = New(OIDCOptions{srv.URL, "client-foo", cert, "sub", "", 0, 0})
|
_, err = New(OIDCOptions{srv.URL, "client-foo", cert, "sub", "", 0, 0})
|
||||||
@ -241,13 +100,13 @@ func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
func TestOIDCDiscoverySecureConnection(t *testing.T) {
|
func TestOIDCDiscoverySecureConnection(t *testing.T) {
|
||||||
// Verify that plain HTTP issuer URL is forbidden.
|
// Verify that plain HTTP issuer URL is forbidden.
|
||||||
op := newOIDCProvider(t)
|
op := oidctesting.NewOIDCProvider(t)
|
||||||
srv := httptest.NewServer(op.mux)
|
srv := httptest.NewServer(op.Mux)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
op.pcfg = oidc.ProviderConfig{
|
op.PCFG = oidc.ProviderConfig{
|
||||||
Issuer: mustParseURL(t, srv.URL),
|
Issuer: oidctesting.MustParseURL(srv.URL),
|
||||||
KeysEndpoint: mustParseURL(t, srv.URL+"/keys"),
|
KeysEndpoint: oidctesting.MustParseURL(srv.URL + "/keys"),
|
||||||
}
|
}
|
||||||
|
|
||||||
expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http")
|
expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http")
|
||||||
@ -268,22 +127,19 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) {
|
|||||||
defer os.Remove(cert2)
|
defer os.Remove(cert2)
|
||||||
defer os.Remove(key2)
|
defer os.Remove(key2)
|
||||||
|
|
||||||
generateSelfSignedCert(t, "127.0.0.1", cert1, key1)
|
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert1, key1)
|
||||||
generateSelfSignedCert(t, "127.0.0.1", cert2, key2)
|
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert2, key2)
|
||||||
|
|
||||||
// Create a TLS server using cert/key pair 1.
|
// Create a TLS server using cert/key pair 1.
|
||||||
tlsSrv := httptest.NewUnstartedServer(op.mux)
|
tlsSrv, err := op.ServeTLSWithKeyPair(cert1, key1)
|
||||||
tlsSrv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
|
||||||
tlsSrv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert1, key1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot load cert/key pair: %v", err)
|
t.Fatalf("Cannot start server: %v", err)
|
||||||
}
|
}
|
||||||
tlsSrv.StartTLS()
|
|
||||||
defer tlsSrv.Close()
|
defer tlsSrv.Close()
|
||||||
|
|
||||||
op.pcfg = oidc.ProviderConfig{
|
op.PCFG = oidc.ProviderConfig{
|
||||||
Issuer: mustParseURL(t, tlsSrv.URL),
|
Issuer: oidctesting.MustParseURL(tlsSrv.URL),
|
||||||
KeysEndpoint: mustParseURL(t, tlsSrv.URL+"/keys"),
|
KeysEndpoint: oidctesting.MustParseURL(tlsSrv.URL + "/keys"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a client using cert2, should fail.
|
// Create a client using cert2, should fail.
|
||||||
@ -303,29 +159,18 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
defer os.Remove(cert)
|
defer os.Remove(cert)
|
||||||
defer os.Remove(key)
|
defer os.Remove(key)
|
||||||
|
|
||||||
generateSelfSignedCert(t, "127.0.0.1", cert, key)
|
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert, key)
|
||||||
|
|
||||||
// Create a TLS server and a client.
|
// Create a TLS server and a client.
|
||||||
op := newOIDCProvider(t)
|
op := oidctesting.NewOIDCProvider(t)
|
||||||
srv := httptest.NewUnstartedServer(op.mux)
|
srv, err := op.ServeTLSWithKeyPair(cert, key)
|
||||||
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
|
||||||
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot load cert/key pair: %v", err)
|
t.Fatalf("Cannot start server: %v", err)
|
||||||
}
|
}
|
||||||
srv.StartTLS()
|
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
// A provider config with all required fields.
|
// A provider config with all required fields.
|
||||||
op.pcfg = oidc.ProviderConfig{
|
op.AddMinimalProviderConfig(srv)
|
||||||
Issuer: mustParseURL(t, srv.URL),
|
|
||||||
AuthEndpoint: mustParseURL(t, srv.URL+"/auth"),
|
|
||||||
TokenEndpoint: mustParseURL(t, srv.URL+"/token"),
|
|
||||||
KeysEndpoint: mustParseURL(t, srv.URL+"/keys"),
|
|
||||||
ResponseTypesSupported: []string{"code"},
|
|
||||||
SubjectTypesSupported: []string{"public"},
|
|
||||||
IDTokenSigningAlgValues: []string{"RS256"},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
userClaim string
|
userClaim string
|
||||||
@ -338,7 +183,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"sub",
|
"sub",
|
||||||
"",
|
"",
|
||||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||||
&user.DefaultInfo{Name: fmt.Sprintf("%s#%s", srv.URL, "user-foo")},
|
&user.DefaultInfo{Name: fmt.Sprintf("%s#%s", srv.URL, "user-foo")},
|
||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
@ -347,7 +192,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
// Use user defined claim (email here).
|
// Use user defined claim (email here).
|
||||||
"email",
|
"email",
|
||||||
"",
|
"",
|
||||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "", nil),
|
generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "", nil),
|
||||||
&user.DefaultInfo{Name: "foo@example.com"},
|
&user.DefaultInfo{Name: "foo@example.com"},
|
||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
@ -356,7 +201,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
// Use user defined claim (email here).
|
// Use user defined claim (email here).
|
||||||
"email",
|
"email",
|
||||||
"",
|
"",
|
||||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
|
generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
|
||||||
&user.DefaultInfo{Name: "foo@example.com"},
|
&user.DefaultInfo{Name: "foo@example.com"},
|
||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
@ -365,7 +210,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
// Use user defined claim (email here).
|
// Use user defined claim (email here).
|
||||||
"email",
|
"email",
|
||||||
"groups",
|
"groups",
|
||||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
|
generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
|
||||||
&user.DefaultInfo{Name: "foo@example.com", Groups: []string{"group1", "group2"}},
|
&user.DefaultInfo{Name: "foo@example.com", Groups: []string{"group1", "group2"}},
|
||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
@ -373,7 +218,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"sub",
|
"sub",
|
||||||
"",
|
"",
|
||||||
op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
generateMalformedToken(t, op, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
"oidc: unable to verify JWT signature: no matching keys",
|
"oidc: unable to verify JWT signature: no matching keys",
|
||||||
@ -382,7 +227,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
// Invalid 'aud'.
|
// Invalid 'aud'.
|
||||||
"sub",
|
"sub",
|
||||||
"",
|
"",
|
||||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-bar", "sub", "user-foo", "", nil),
|
generateGoodToken(t, op, srv.URL, "client-foo", "client-bar", "sub", "user-foo", "", nil),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
"oidc: JWT claims invalid: invalid claims, 'aud' claim and 'client_id' do not match",
|
"oidc: JWT claims invalid: invalid claims, 'aud' claim and 'client_id' do not match",
|
||||||
@ -391,7 +236,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
// Invalid issuer.
|
// Invalid issuer.
|
||||||
"sub",
|
"sub",
|
||||||
"",
|
"",
|
||||||
op.generateGoodToken(t, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
generateGoodToken(t, op, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
"oidc: JWT claims invalid: invalid claim value: 'iss'.",
|
"oidc: JWT claims invalid: invalid claim value: 'iss'.",
|
||||||
@ -399,7 +244,7 @@ func TestOIDCAuthentication(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"sub",
|
"sub",
|
||||||
"",
|
"",
|
||||||
op.generateExpiredToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
generateExpiredToken(t, op, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
"oidc: JWT claims invalid: token is expired",
|
"oidc: JWT claims invalid: token is expired",
|
||||||
|
194
plugin/pkg/auth/authenticator/token/oidc/testing/provider.go
Normal file
194
plugin/pkg/auth/authenticator/token/oidc/testing/provider.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 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/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) *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,
|
||||||
|
}
|
||||||
|
|
||||||
|
op.Mux.HandleFunc("/.well-known/openid-configuration", op.handleConfig)
|
||||||
|
op.Mux.HandleFunc("/keys", op.handleKeys)
|
||||||
|
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCProvider struct {
|
||||||
|
Mux *http.ServeMux
|
||||||
|
PCFG oidc.ProviderConfig
|
||||||
|
PrivKey *key.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
return srv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op *OIDCProvider) AddMinimalProviderConfig(srv *httptest.Server) {
|
||||||
|
op.PCFG = oidc.ProviderConfig{
|
||||||
|
Issuer: MustParseURL(srv.URL),
|
||||||
|
AuthEndpoint: MustParseURL(srv.URL + "/auth"),
|
||||||
|
TokenEndpoint: MustParseURL(srv.URL + "/token"),
|
||||||
|
KeysEndpoint: MustParseURL(srv.URL + "/keys"),
|
||||||
|
ResponseTypesSupported: []string{"code"},
|
||||||
|
SubjectTypesSupported: []string{"public"},
|
||||||
|
IDTokenSigningAlgValues: []string{"RS256"},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustParseURL(s string) *url.URL {
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Failed to parse url: %v", err))
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
2
plugin/pkg/client/auth/oidc/OWNERS
Normal file
2
plugin/pkg/client/auth/oidc/OWNERS
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
assignees:
|
||||||
|
- bobbyrullo
|
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 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 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/kubernetes/pkg/client/restclient"
|
||||||
|
"k8s.io/kubernetes/pkg/util/wait"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := restclient.RegisterAuthProviderPlugin("oidc", newOIDCAuthProvider); err != nil {
|
||||||
|
glog.Fatalf("Failed to register oidc auth plugin: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOIDCAuthProvider(_ string, cfg map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.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 := restclient.Config{
|
||||||
|
TLSClientConfig: restclient.TLSClientConfig{
|
||||||
|
CAFile: cfg[cfgCertificateAuthority],
|
||||||
|
CAData: certAuthData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
trans, err := restclient.TransportFor(&clientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hc := &http.Client{Transport: trans}
|
||||||
|
|
||||||
|
providerCfg, err := oidc.FetchProviderConfig(hc, strings.TrimSuffix(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 restclient.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)
|
||||||
|
}
|
642
plugin/pkg/client/auth/oidc/oidc_test.go
Normal file
642
plugin/pkg/client/auth/oidc/oidc_test.go
Normal file
@ -0,0 +1,642 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 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/kubernetes/pkg/util/diff"
|
||||||
|
"k8s.io/kubernetes/pkg/util/wait"
|
||||||
|
oidctesting "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewOIDCAuthProvider(t *testing.T) {
|
||||||
|
cert := path.Join(os.TempDir(), "oidc-cert")
|
||||||
|
key := path.Join(os.TempDir(), "oidc-key")
|
||||||
|
|
||||||
|
defer os.Remove(cert)
|
||||||
|
defer os.Remove(key)
|
||||||
|
|
||||||
|
oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert, key)
|
||||||
|
op := oidctesting.NewOIDCProvider(t)
|
||||||
|
srv, err := op.ServeTLSWithKeyPair(cert, key)
|
||||||
|
op.AddMinimalProviderConfig(srv)
|
||||||
|
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)
|
||||||
|
}
|
@ -19,4 +19,5 @@ package plugins
|
|||||||
import (
|
import (
|
||||||
// Initialize all known client auth plugins.
|
// Initialize all known client auth plugins.
|
||||||
_ "k8s.io/kubernetes/plugin/pkg/client/auth/gcp"
|
_ "k8s.io/kubernetes/plugin/pkg/client/auth/gcp"
|
||||||
|
_ "k8s.io/kubernetes/plugin/pkg/client/auth/oidc"
|
||||||
)
|
)
|
||||||
|
3
vendor/github.com/coreos/go-oidc/jose/sig.go
generated
vendored
3
vendor/github.com/coreos/go-oidc/jose/sig.go
generated
vendored
@ -2,7 +2,6 @@ package jose
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Verifier interface {
|
type Verifier interface {
|
||||||
@ -17,7 +16,7 @@ type Signer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewVerifier(jwk JWK) (Verifier, error) {
|
func NewVerifier(jwk JWK) (Verifier, error) {
|
||||||
if strings.ToUpper(jwk.Type) != "RSA" {
|
if jwk.Type != "RSA" {
|
||||||
return nil, fmt.Errorf("unsupported key type %q", jwk.Type)
|
return nil, fmt.Errorf("unsupported key type %q", jwk.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
vendor/github.com/coreos/go-oidc/jose/sig_hmac.go
generated
vendored
3
vendor/github.com/coreos/go-oidc/jose/sig_hmac.go
generated
vendored
@ -7,7 +7,6 @@ import (
|
|||||||
_ "crypto/sha256"
|
_ "crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type VerifierHMAC struct {
|
type VerifierHMAC struct {
|
||||||
@ -21,7 +20,7 @@ type SignerHMAC struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewVerifierHMAC(jwk JWK) (*VerifierHMAC, error) {
|
func NewVerifierHMAC(jwk JWK) (*VerifierHMAC, error) {
|
||||||
if strings.ToUpper(jwk.Alg) != "HS256" {
|
if jwk.Alg != "" && jwk.Alg != "HS256" {
|
||||||
return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg)
|
return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
vendor/github.com/coreos/go-oidc/jose/sig_rsa.go
generated
vendored
3
vendor/github.com/coreos/go-oidc/jose/sig_rsa.go
generated
vendored
@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type VerifierRSA struct {
|
type VerifierRSA struct {
|
||||||
@ -20,7 +19,7 @@ type SignerRSA struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewVerifierRSA(jwk JWK) (*VerifierRSA, error) {
|
func NewVerifierRSA(jwk JWK) (*VerifierRSA, error) {
|
||||||
if strings.ToUpper(jwk.Alg) != "RS256" {
|
if jwk.Alg != "" && jwk.Alg != "RS256" {
|
||||||
return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg)
|
return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
vendor/github.com/coreos/go-oidc/key/key.go
generated
vendored
2
vendor/github.com/coreos/go-oidc/key/key.go
generated
vendored
@ -20,7 +20,7 @@ type PublicKey struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *PublicKey) MarshalJSON() ([]byte, error) {
|
func (k *PublicKey) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(k.jwk)
|
return json.Marshal(&k.jwk)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *PublicKey) UnmarshalJSON(data []byte) error {
|
func (k *PublicKey) UnmarshalJSON(data []byte) error {
|
||||||
|
30
vendor/github.com/coreos/go-oidc/oauth2/oauth2.go
generated
vendored
30
vendor/github.com/coreos/go-oidc/oauth2/oauth2.go
generated
vendored
@ -56,6 +56,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
GrantTypeAuthCode = "authorization_code"
|
GrantTypeAuthCode = "authorization_code"
|
||||||
GrantTypeClientCreds = "client_credentials"
|
GrantTypeClientCreds = "client_credentials"
|
||||||
|
GrantTypeUserCreds = "password"
|
||||||
GrantTypeImplicit = "implicit"
|
GrantTypeImplicit = "implicit"
|
||||||
GrantTypeRefreshToken = "refresh_token"
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
@ -140,6 +141,11 @@ func NewClient(hc phttp.Client, cfg Config) (c *Client, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the embedded HTTP client
|
||||||
|
func (c *Client) HttpClient() phttp.Client {
|
||||||
|
return c.hc
|
||||||
|
}
|
||||||
|
|
||||||
// Generate the url for initial redirect to oauth provider.
|
// Generate the url for initial redirect to oauth provider.
|
||||||
func (c *Client) AuthCodeURL(state, accessType, prompt string) string {
|
func (c *Client) AuthCodeURL(state, accessType, prompt string) string {
|
||||||
v := c.commonURLValues()
|
v := c.commonURLValues()
|
||||||
@ -220,6 +226,30 @@ func (c *Client) ClientCredsToken(scope []string) (result TokenResponse, err err
|
|||||||
return parseTokenResponse(resp)
|
return parseTokenResponse(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserCredsToken posts the username and password to obtain a token scoped to the OAuth2 client via the "password" grant_type
|
||||||
|
// May not be supported by all OAuth2 servers.
|
||||||
|
func (c *Client) UserCredsToken(username, password string) (result TokenResponse, err error) {
|
||||||
|
v := url.Values{
|
||||||
|
"scope": {strings.Join(c.scope, " ")},
|
||||||
|
"grant_type": {GrantTypeUserCreds},
|
||||||
|
"username": {username},
|
||||||
|
"password": {password},
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return parseTokenResponse(resp)
|
||||||
|
}
|
||||||
|
|
||||||
// RequestToken requests a token from the Token Endpoint with the specified grantType.
|
// RequestToken requests a token from the Token Endpoint with the specified grantType.
|
||||||
// If 'grantType' == GrantTypeAuthCode, then 'value' should be the authorization code.
|
// If 'grantType' == GrantTypeAuthCode, then 'value' should be the authorization code.
|
||||||
// If 'grantType' == GrantTypeRefreshToken, then 'value' should be the refresh token.
|
// If 'grantType' == GrantTypeRefreshToken, then 'value' should be the refresh token.
|
||||||
|
12
vendor/github.com/coreos/go-oidc/oidc/key.go
generated
vendored
12
vendor/github.com/coreos/go-oidc/oidc/key.go
generated
vendored
@ -11,6 +11,11 @@ import (
|
|||||||
"github.com/coreos/go-oidc/key"
|
"github.com/coreos/go-oidc/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultPublicKeySetTTL is the default TTL set on the PublicKeySet if no
|
||||||
|
// Cache-Control header is provided by the JWK Set document endpoint.
|
||||||
|
const DefaultPublicKeySetTTL = 24 * time.Hour
|
||||||
|
|
||||||
|
// NewRemotePublicKeyRepo is responsible for fetching the JWK Set document.
|
||||||
func NewRemotePublicKeyRepo(hc phttp.Client, ep string) *remotePublicKeyRepo {
|
func NewRemotePublicKeyRepo(hc phttp.Client, ep string) *remotePublicKeyRepo {
|
||||||
return &remotePublicKeyRepo{hc: hc, ep: ep}
|
return &remotePublicKeyRepo{hc: hc, ep: ep}
|
||||||
}
|
}
|
||||||
@ -20,6 +25,11 @@ type remotePublicKeyRepo struct {
|
|||||||
ep string
|
ep string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get returns a PublicKeySet fetched from the JWK Set document endpoint. A TTL
|
||||||
|
// is set on the Key Set to avoid it having to be re-retrieved for every
|
||||||
|
// encryption event. This TTL is typically controlled by the endpoint returning
|
||||||
|
// a Cache-Control header, but defaults to 24 hours if no Cache-Control header
|
||||||
|
// is found.
|
||||||
func (r *remotePublicKeyRepo) Get() (key.KeySet, error) {
|
func (r *remotePublicKeyRepo) Get() (key.KeySet, error) {
|
||||||
req, err := http.NewRequest("GET", r.ep, nil)
|
req, err := http.NewRequest("GET", r.ep, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -48,7 +58,7 @@ func (r *remotePublicKeyRepo) Get() (key.KeySet, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("HTTP cache headers not set")
|
ttl = DefaultPublicKeySetTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
exp := time.Now().UTC().Add(ttl)
|
exp := time.Now().UTC().Add(ttl)
|
||||||
|
7
vendor/github.com/coreos/go-oidc/oidc/provider.go
generated
vendored
7
vendor/github.com/coreos/go-oidc/oidc/provider.go
generated
vendored
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -618,7 +619,11 @@ func NewHTTPProviderConfigGetter(hc phttp.Client, issuerURL string) *httpProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) {
|
func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) {
|
||||||
req, err := http.NewRequest("GET", r.issuerURL+discoveryConfigPath, nil)
|
// If the Issuer value contains a path component, any terminating / MUST be removed before
|
||||||
|
// appending /.well-known/openid-configuration.
|
||||||
|
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
|
||||||
|
discoveryURL := strings.TrimSuffix(r.issuerURL, "/") + discoveryConfigPath
|
||||||
|
req, err := http.NewRequest("GET", discoveryURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
9
vendor/github.com/coreos/go-oidc/oidc/transport.go
generated
vendored
9
vendor/github.com/coreos/go-oidc/oidc/transport.go
generated
vendored
@ -67,6 +67,15 @@ func (t *AuthenticatedTransport) verifiedJWT() (jose.JWT, error) {
|
|||||||
return t.jwt, nil
|
return t.jwt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetJWT sets the JWT held by the Transport.
|
||||||
|
// This is useful for cases in which you want to set an initial JWT.
|
||||||
|
func (t *AuthenticatedTransport) SetJWT(jwt jose.JWT) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
t.jwt = jwt
|
||||||
|
}
|
||||||
|
|
||||||
func (t *AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
func (t *AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
jwt, err := t.verifiedJWT()
|
jwt, err := t.verifiedJWT()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user