func TestOIDCDiscoverySecureConnection(t *testing.T) { // Verify that plain HTTP issuer URL is forbidden. op := oidctesting.NewOIDCProvider(t) srv := httptest.NewServer(op.Mux) defer srv.Close() op.PCFG = oidc.ProviderConfig{ Issuer: oidctesting.MustParseURL(srv.URL), KeysEndpoint: oidctesting.MustParseURL(srv.URL + "/keys"), } expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http") _, err := New(OIDCOptions{srv.URL, "client-foo", "", "sub", "", 0, 0}) if !reflect.DeepEqual(err, expectErr) { t.Errorf("Expecting %v, but got %v", expectErr, err) } // 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) // Create a TLS server using cert/key pair 1. tlsSrv, err := op.ServeTLSWithKeyPair(cert1, key1) if err != nil { t.Fatalf("Cannot start server: %v", err) } defer tlsSrv.Close() op.PCFG = oidc.ProviderConfig{ Issuer: oidctesting.MustParseURL(tlsSrv.URL), KeysEndpoint: oidctesting.MustParseURL(tlsSrv.URL + "/keys"), } // Create a client using cert2, should fail. _, err = New(OIDCOptions{tlsSrv.URL, "client-foo", cert2, "sub", "", 0, 0}) if err == nil { t.Fatalf("Expecting error, but got nothing") } }
func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) { var err error expectErr := fmt.Errorf("failed to fetch provider config after 0 retries") 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) if err != nil { t.Fatalf("Cannot start server %v", err) } defer srv.Close() op.PCFG = oidc.ProviderConfig{ Issuer: oidctesting.MustParseURL(srv.URL), // An invalid ProviderConfig. Keys endpoint is required. } _, err = New(OIDCOptions{srv.URL, "client-foo", cert, "sub", "", 0, 0}) if !reflect.DeepEqual(err, expectErr) { t.Errorf("Expecting %v, but got %v", expectErr, err) } }
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.RemoveAll(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) } makeToken := func(exp time.Time) *jose.JWT { jwt, err := jose.NewSignedJWT(jose.Claims(map[string]interface{}{ "exp": exp.UTC().Unix(), }), op.PrivKey.Signer()) if err != nil { t.Fatalf("Could not create signed JWT %v", err) } return jwt } t0 := time.Now() goodToken := makeToken(t0.Add(time.Hour)).Encode() expiredToken := makeToken(t0.Add(-time.Hour)).Encode() tests := []struct { name string cfg map[string]string wantInitErr bool client OIDCClient wantCfg map[string]string wantTokenErr bool }{ { // A Valid configuration name: "no id token and no refresh token", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", }, wantTokenErr: true, }, { name: "valid config with an initial token", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgIDToken: goodToken, }, client: new(noRefreshOIDCClient), wantCfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgIDToken: goodToken, }, }, { name: "invalid ID token with a refresh token", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgRefreshToken: "foo", cfgIDToken: expiredToken, }, client: &mockOIDCClient{ tokenResponse: oauth2.TokenResponse{ IDToken: goodToken, }, }, wantCfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgRefreshToken: "foo", cfgIDToken: goodToken, }, }, { name: "invalid ID token with a refresh token, server returns new refresh token", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgRefreshToken: "foo", cfgIDToken: expiredToken, }, client: &mockOIDCClient{ tokenResponse: oauth2.TokenResponse{ IDToken: goodToken, RefreshToken: "bar", }, }, wantCfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgRefreshToken: "bar", cfgIDToken: goodToken, }, }, { name: "expired token and no refresh otken", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "client-secret", cfgIDToken: expiredToken, }, wantTokenErr: true, }, { name: "valid base64d ca", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthorityData: base64.StdEncoding.EncodeToString(certData), cfgClientID: "client-id", cfgClientSecret: "client-secret", }, client: new(noRefreshOIDCClient), wantTokenErr: true, }, { name: "missing client ID", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientSecret: "client-secret", }, wantInitErr: true, }, { name: "missing client secret", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgCertificateAuthority: cert, cfgClientID: "client-id", }, wantInitErr: true, }, { name: "missing issuer URL", cfg: map[string]string{ cfgCertificateAuthority: cert, cfgClientID: "client-id", cfgClientSecret: "secret", }, wantInitErr: true, }, { name: "missing TLS config", cfg: map[string]string{ cfgIssuerUrl: srv.URL, cfgClientID: "client-id", cfgClientSecret: "secret", }, wantInitErr: true, }, } for _, tt := range tests { clearCache() p, err := newOIDCAuthProvider("cluster.example.com", tt.cfg, new(persister)) if tt.wantInitErr { if err == nil { t.Errorf("%s: want non-nil err", tt.name) } continue } if err != nil { t.Errorf("%s: unexpected error on newOIDCAuthProvider: %v", tt.name, err) continue } provider := p.(*oidcAuthProvider) provider.client = tt.client provider.now = func() time.Time { return t0 } if _, err := provider.idToken(); err != nil { if !tt.wantTokenErr { t.Errorf("%s: failed to get id token: %v", tt.name, err) } continue } if tt.wantTokenErr { t.Errorf("%s: expected to not get id token: %v", tt.name, err) continue } if !reflect.DeepEqual(tt.wantCfg, provider.cfg) { t.Errorf("%s: expected config %#v got %#v", tt.name, tt.wantCfg, provider.cfg) } } }
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() op.AddMinimalProviderConfig(srv) 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 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 := "*****@*****.**" 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", "*****@*****.**", "", nil), &user.DefaultInfo{Name: "*****@*****.**"}, true, "", }, { // Use user defined claim (email here). "email", "", generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "*****@*****.**", "groups", []string{"group1", "group2"}), &user.DefaultInfo{Name: "*****@*****.**"}, true, "", }, { // Use user defined claim (email here). "email", "groups", generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "*****@*****.**", "groups", []string{"group1", "group2"}), &user.DefaultInfo{Name: "*****@*****.**", 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", "*****@*****.**", "groups", "group1"), &user.DefaultInfo{Name: "*****@*****.**", 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", "*****@*****.**", "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() } } }
func TestOIDCAuthentication(t *testing.T) { var err error 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) // Create a TLS server and a client. op := oidctesting.NewOIDCProvider(t) srv, err := op.ServeTLSWithKeyPair(cert, key) if err != nil { t.Fatalf("Cannot start server: %v", err) } defer srv.Close() // A provider config with all required fields. op.AddMinimalProviderConfig(srv) 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", "*****@*****.**", "", nil), &user.DefaultInfo{Name: "*****@*****.**"}, true, "", }, { // Use user defined claim (email here). "email", "", generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "*****@*****.**", "groups", []string{"group1", "group2"}), &user.DefaultInfo{Name: "*****@*****.**"}, true, "", }, { // Use user defined claim (email here). "email", "groups", generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "*****@*****.**", "groups", []string{"group1", "group2"}), &user.DefaultInfo{Name: "*****@*****.**", Groups: []string{"group1", "group2"}}, true, "", }, { "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, 1, 100 * time.Millisecond}) 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() } }