mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-04 07:49:35 +00:00 
			
		
		
		
	Allow the group claim to be a single string instead of an array of
strings. This means the following claim
    {
      "role": "admin"
    }
Will be mapped to the groups
   ["admin"]
		
	
		
			
				
	
	
		
			323 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			323 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2015 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 (
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"reflect"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/coreos/go-oidc/jose"
 | 
						|
	"github.com/coreos/go-oidc/oidc"
 | 
						|
 | 
						|
	"k8s.io/kubernetes/pkg/auth/user"
 | 
						|
	oidctesting "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc/testing"
 | 
						|
)
 | 
						|
 | 
						|
func generateToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}, iat, exp time.Time) string {
 | 
						|
	signer := op.PrivKey.Signer()
 | 
						|
	claims := oidc.NewClaims(iss, sub, aud, iat, exp)
 | 
						|
	claims.Add(usernameClaim, value)
 | 
						|
	if groups != nil && groupsClaim != "" {
 | 
						|
		claims.Add(groupsClaim, groups)
 | 
						|
	}
 | 
						|
 | 
						|
	jwt, err := jose.NewSignedJWT(claims, signer)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("Cannot generate token: %v", err)
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	return jwt.Encode()
 | 
						|
}
 | 
						|
 | 
						|
func generateGoodToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}) string {
 | 
						|
	return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour))
 | 
						|
}
 | 
						|
 | 
						|
func generateMalformedToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}) string {
 | 
						|
	return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits"
 | 
						|
}
 | 
						|
 | 
						|
func generateExpiredToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}) string {
 | 
						|
	return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSConfig(t *testing.T) {
 | 
						|
	// Verify the cert/key pair works.
 | 
						|
	cert1 := path.Join(os.TempDir(), "oidc-cert-1")
 | 
						|
	key1 := path.Join(os.TempDir(), "oidc-key-1")
 | 
						|
	cert2 := path.Join(os.TempDir(), "oidc-cert-2")
 | 
						|
	key2 := path.Join(os.TempDir(), "oidc-key-2")
 | 
						|
 | 
						|
	defer os.Remove(cert1)
 | 
						|
	defer os.Remove(key1)
 | 
						|
	defer os.Remove(cert2)
 | 
						|
	defer os.Remove(key2)
 | 
						|
 | 
						|
	oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert1, key1)
 | 
						|
	oidctesting.GenerateSelfSignedCert(t, "127.0.0.1", cert2, key2)
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		testCase string
 | 
						|
 | 
						|
		serverCertFile string
 | 
						|
		serverKeyFile  string
 | 
						|
 | 
						|
		trustedCertFile string
 | 
						|
 | 
						|
		wantErr bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			testCase:       "provider using untrusted custom cert",
 | 
						|
			serverCertFile: cert1,
 | 
						|
			serverKeyFile:  key1,
 | 
						|
			wantErr:        true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			testCase:        "provider using untrusted cert",
 | 
						|
			serverCertFile:  cert1,
 | 
						|
			serverKeyFile:   key1,
 | 
						|
			trustedCertFile: cert2,
 | 
						|
			wantErr:         true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			testCase:        "provider using trusted cert",
 | 
						|
			serverCertFile:  cert1,
 | 
						|
			serverKeyFile:   key1,
 | 
						|
			trustedCertFile: cert1,
 | 
						|
			wantErr:         false,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range tests {
 | 
						|
		func() {
 | 
						|
			op := oidctesting.NewOIDCProvider(t, "")
 | 
						|
			srv, err := op.ServeTLSWithKeyPair(tc.serverCertFile, tc.serverKeyFile)
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("%s: %v", tc.testCase, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			defer srv.Close()
 | 
						|
 | 
						|
			issuer := srv.URL
 | 
						|
			clientID := "client-foo"
 | 
						|
 | 
						|
			options := OIDCOptions{
 | 
						|
				IssuerURL:     srv.URL,
 | 
						|
				ClientID:      clientID,
 | 
						|
				CAFile:        tc.trustedCertFile,
 | 
						|
				UsernameClaim: "email",
 | 
						|
				GroupsClaim:   "groups",
 | 
						|
			}
 | 
						|
 | 
						|
			authenticator, err := New(options)
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("%s: failed to initialize authenticator: %v", tc.testCase, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			defer authenticator.Close()
 | 
						|
 | 
						|
			email := "user-1@example.com"
 | 
						|
			groups := []string{"group1", "group2"}
 | 
						|
			sort.Strings(groups)
 | 
						|
 | 
						|
			token := generateGoodToken(t, op, issuer, "user-1", clientID, "email", email, "groups", groups)
 | 
						|
 | 
						|
			// Because this authenticator behaves differently for subsequent requests, run these
 | 
						|
			// tests multiple times (but expect the same result).
 | 
						|
			for i := 1; i < 4; i++ {
 | 
						|
 | 
						|
				user, ok, err := authenticator.AuthenticateToken(token)
 | 
						|
				if err != nil {
 | 
						|
					if !tc.wantErr {
 | 
						|
						t.Errorf("%s (req #%d): failed to authenticate token: %v", tc.testCase, i, err)
 | 
						|
					}
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				if tc.wantErr {
 | 
						|
					t.Errorf("%s (req #%d): expected error authenticating", tc.testCase, i)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				if !ok {
 | 
						|
					t.Errorf("%s (req #%d): did not get user or error", tc.testCase, i)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				if gotUsername := user.GetName(); email != gotUsername {
 | 
						|
					t.Errorf("%s (req #%d): GetName() expected=%q got %q", tc.testCase, i, email, gotUsername)
 | 
						|
				}
 | 
						|
				gotGroups := user.GetGroups()
 | 
						|
				sort.Strings(gotGroups)
 | 
						|
				if !reflect.DeepEqual(gotGroups, groups) {
 | 
						|
					t.Errorf("%s (req #%d): GetGroups() expected=%q got %q", tc.testCase, i, groups, gotGroups)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}()
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestOIDCAuthentication(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)
 | 
						|
 | 
						|
	// Ensure all tests pass when the issuer is not at a base URL.
 | 
						|
	for _, path := range []string{"", "/path/with/trailing/slash/"} {
 | 
						|
 | 
						|
		// Create a TLS server and a client.
 | 
						|
		op := oidctesting.NewOIDCProvider(t, path)
 | 
						|
		srv, err := op.ServeTLSWithKeyPair(cert, key)
 | 
						|
		if err != nil {
 | 
						|
			t.Fatalf("Cannot start server: %v", err)
 | 
						|
		}
 | 
						|
		defer srv.Close()
 | 
						|
 | 
						|
		tests := []struct {
 | 
						|
			userClaim   string
 | 
						|
			groupsClaim string
 | 
						|
			token       string
 | 
						|
			userInfo    user.Info
 | 
						|
			verified    bool
 | 
						|
			err         string
 | 
						|
		}{
 | 
						|
			{
 | 
						|
				"sub",
 | 
						|
				"",
 | 
						|
				generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
 | 
						|
				&user.DefaultInfo{Name: fmt.Sprintf("%s#%s", srv.URL, "user-foo")},
 | 
						|
				true,
 | 
						|
				"",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Use user defined claim (email here).
 | 
						|
				"email",
 | 
						|
				"",
 | 
						|
				generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "", nil),
 | 
						|
				&user.DefaultInfo{Name: "foo@example.com"},
 | 
						|
				true,
 | 
						|
				"",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Use user defined claim (email here).
 | 
						|
				"email",
 | 
						|
				"",
 | 
						|
				generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
 | 
						|
				&user.DefaultInfo{Name: "foo@example.com"},
 | 
						|
				true,
 | 
						|
				"",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Use user defined claim (email here).
 | 
						|
				"email",
 | 
						|
				"groups",
 | 
						|
				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"}},
 | 
						|
				true,
 | 
						|
				"",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Group claim is a string rather than an array. Map that string to a single group.
 | 
						|
				"email",
 | 
						|
				"groups",
 | 
						|
				generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", "group1"),
 | 
						|
				&user.DefaultInfo{Name: "foo@example.com", Groups: []string{"group1"}},
 | 
						|
				true,
 | 
						|
				"",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Group claim is not a string or array of strings. Throw out this as invalid.
 | 
						|
				"email",
 | 
						|
				"groups",
 | 
						|
				generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", 1),
 | 
						|
				nil,
 | 
						|
				false,
 | 
						|
				"custom group claim contains invalid type: float64",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				"sub",
 | 
						|
				"",
 | 
						|
				generateMalformedToken(t, op, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
 | 
						|
				nil,
 | 
						|
				false,
 | 
						|
				"oidc: unable to verify JWT signature: no matching keys",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Invalid 'aud'.
 | 
						|
				"sub",
 | 
						|
				"",
 | 
						|
				generateGoodToken(t, op, srv.URL, "client-foo", "client-bar", "sub", "user-foo", "", nil),
 | 
						|
				nil,
 | 
						|
				false,
 | 
						|
				"oidc: JWT claims invalid: invalid claims, 'aud' claim and 'client_id' do not match",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				// Invalid issuer.
 | 
						|
				"sub",
 | 
						|
				"",
 | 
						|
				generateGoodToken(t, op, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo", "", nil),
 | 
						|
				nil,
 | 
						|
				false,
 | 
						|
				"oidc: JWT claims invalid: invalid claim value: 'iss'.",
 | 
						|
			},
 | 
						|
			{
 | 
						|
				"sub",
 | 
						|
				"",
 | 
						|
				generateExpiredToken(t, op, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
 | 
						|
				nil,
 | 
						|
				false,
 | 
						|
				"oidc: JWT claims invalid: token is expired",
 | 
						|
			},
 | 
						|
		}
 | 
						|
 | 
						|
		for i, tt := range tests {
 | 
						|
			client, err := New(OIDCOptions{srv.URL, "client-foo", cert, tt.userClaim, tt.groupsClaim})
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("Unexpected error: %v", err)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			user, result, err := client.AuthenticateToken(tt.token)
 | 
						|
			if tt.err != "" {
 | 
						|
				if !strings.HasPrefix(err.Error(), tt.err) {
 | 
						|
					t.Errorf("#%d: Expecting: %v..., but got: %v", i, tt.err, err)
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if err != nil {
 | 
						|
					t.Errorf("#%d: Unexpected error: %v", i, err)
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if !reflect.DeepEqual(tt.verified, result) {
 | 
						|
				t.Errorf("#%d: Expecting: %v, but got: %v", i, tt.verified, result)
 | 
						|
			}
 | 
						|
			if !reflect.DeepEqual(tt.userInfo, user) {
 | 
						|
				t.Errorf("#%d: Expecting: %v, but got: %v", i, tt.userInfo, user)
 | 
						|
			}
 | 
						|
			client.Close()
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |