Require refresh tokens for upstream OIDC and save more session data

- Requiring refresh tokens to be returned from upstream OIDC idps
- Storing refresh tokens (for oidc) and idp information (for all idps) in custom session data during authentication
- Don't pass access=offline all the time
This commit is contained in:
Margo Crawford 2021-10-08 15:48:21 -07:00
parent 43244b6599
commit 1bd346cbeb
21 changed files with 636 additions and 244 deletions

View File

@ -302,6 +302,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
config := &upstreamldap.ProviderConfig{ config := &upstreamldap.ProviderConfig{
Name: upstream.Name, Name: upstream.Name,
ResourceUID: upstream.UID,
Host: spec.Host, Host: spec.Host,
UserSearch: upstreamldap.UserSearchConfig{ UserSearch: upstreamldap.UserSearchConfig{
Base: spec.UserSearch.Base, Base: spec.UserSearch.Base,

View File

@ -150,6 +150,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
const ( const (
testNamespace = "test-namespace" testNamespace = "test-namespace"
testResourceUID = "test-uid"
testName = "test-name" testName = "test-name"
testSecretName = "test-bind-secret" testSecretName = "test-bind-secret"
testBindUsername = "test-bind-username" testBindUsername = "test-bind-username"
@ -172,7 +173,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle)
validUpstream := &v1alpha1.ActiveDirectoryIdentityProvider{ validUpstream := &v1alpha1.ActiveDirectoryIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234, UID: testResourceUID},
Spec: v1alpha1.ActiveDirectoryIdentityProviderSpec{ Spec: v1alpha1.ActiveDirectoryIdentityProviderSpec{
Host: testHost, Host: testHost,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded},
@ -202,6 +203,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -364,7 +366,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -379,7 +381,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -407,7 +409,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -434,7 +436,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -460,7 +462,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -486,7 +488,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -517,6 +519,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -537,7 +540,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -572,6 +575,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -592,7 +596,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -630,6 +634,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com", Host: "ldap.example.com",
ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS
CABundle: testCABundle, CABundle: testCABundle,
@ -650,7 +655,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -688,6 +693,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
// even though the connection test failed, still loads into the cache because it is treated like a warning // even though the connection test failed, still loads into the cache because it is treated like a warning
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com:5678", Host: "ldap.example.com:5678",
ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS
CABundle: testCABundle, CABundle: testCABundle,
@ -709,7 +715,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -745,6 +751,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -765,7 +772,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -779,6 +786,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
upstream.Name = "other-upstream" upstream.Name = "other-upstream"
upstream.Generation = 42 upstream.Generation = 42
upstream.Spec.Bind.SecretName = "non-existent-secret" upstream.Spec.Bind.SecretName = "non-existent-secret"
upstream.UID = "other-uid"
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
@ -790,7 +798,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42, UID: "other-uid"},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -807,7 +815,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -831,7 +839,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -872,6 +880,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -892,7 +901,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -928,7 +937,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -966,7 +975,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -995,6 +1004,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1015,7 +1025,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1045,6 +1055,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1065,7 +1076,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1100,7 +1111,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1137,7 +1148,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1175,7 +1186,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1212,7 +1223,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1243,6 +1254,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1264,7 +1276,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1295,6 +1307,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1315,7 +1328,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1350,6 +1363,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1370,7 +1384,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1399,6 +1413,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1419,7 +1434,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1447,7 +1462,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1483,7 +1498,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1525,7 +1540,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1554,7 +1569,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1594,6 +1609,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1614,7 +1630,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{

View File

@ -227,6 +227,7 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
config := &upstreamldap.ProviderConfig{ config := &upstreamldap.ProviderConfig{
Name: upstream.Name, Name: upstream.Name,
ResourceUID: upstream.UID,
Host: spec.Host, Host: spec.Host,
UserSearch: upstreamldap.UserSearchConfig{ UserSearch: upstreamldap.UserSearchConfig{
Base: spec.UserSearch.Base, Base: spec.UserSearch.Base,

View File

@ -150,6 +150,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
const ( const (
testNamespace = "test-namespace" testNamespace = "test-namespace"
testName = "test-name" testName = "test-name"
testResourceUID = "test-resource-uid"
testSecretName = "test-bind-secret" testSecretName = "test-bind-secret"
testBindUsername = "test-bind-username" testBindUsername = "test-bind-username"
testBindPassword = "test-bind-password" testBindPassword = "test-bind-password"
@ -171,7 +172,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle)
validUpstream := &v1alpha1.LDAPIdentityProvider{ validUpstream := &v1alpha1.LDAPIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{
Name: testName,
Namespace: testNamespace,
Generation: 1234,
UID: testResourceUID,
},
Spec: v1alpha1.LDAPIdentityProviderSpec{ Spec: v1alpha1.LDAPIdentityProviderSpec{
Host: testHost, Host: testHost,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded},
@ -201,6 +207,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -299,7 +306,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -320,7 +327,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -348,7 +355,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -375,7 +382,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -401,7 +408,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -427,7 +434,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -458,6 +465,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -477,7 +485,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -519,6 +527,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com", Host: "ldap.example.com",
ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS
CABundle: testCABundle, CABundle: testCABundle,
@ -538,7 +547,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -580,6 +589,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
// even though the connection test failed, still loads into the cache because it is treated like a warning // even though the connection test failed, still loads into the cache because it is treated like a warning
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com:5678", Host: "ldap.example.com:5678",
ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS
CABundle: testCABundle, CABundle: testCABundle,
@ -600,7 +610,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -635,6 +645,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -654,7 +665,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -674,6 +685,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
upstream.Name = "other-upstream" upstream.Name = "other-upstream"
upstream.Generation = 42 upstream.Generation = 42
upstream.Spec.Bind.SecretName = "non-existent-secret" upstream.Spec.Bind.SecretName = "non-existent-secret"
upstream.UID = "other-uid"
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
@ -685,7 +697,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42, UID: "other-uid"},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -702,7 +714,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -729,7 +741,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -771,7 +783,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -805,7 +817,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -841,7 +853,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -877,7 +889,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -917,7 +929,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -954,7 +966,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),

View File

@ -175,6 +175,8 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst
UsernameClaim: upstream.Spec.Claims.Username, UsernameClaim: upstream.Spec.Claims.Username,
GroupsClaim: upstream.Spec.Claims.Groups, GroupsClaim: upstream.Spec.Claims.Groups,
AllowPasswordGrant: upstream.Spec.AuthorizationConfig.AllowPasswordGrant, AllowPasswordGrant: upstream.Spec.AuthorizationConfig.AllowPasswordGrant,
AdditionalAuthcodeParams: map[string]string{"prompt": "consent"},
ResourceUID: upstream.UID,
} }
conditions := []*v1alpha1.Condition{ conditions := []*v1alpha1.Condition{
c.validateSecret(upstream, &result), c.validateSecret(upstream, &result),
@ -374,6 +376,7 @@ func computeScopes(additionalScopes []string) []string {
// First compute the unique set of scopes, including "openid" (de-duplicate). // First compute the unique set of scopes, including "openid" (de-duplicate).
set := make(map[string]bool, len(additionalScopes)+1) set := make(map[string]bool, len(additionalScopes)+1)
set["openid"] = true set["openid"] = true
set["offline_access"] = true
for _, s := range additionalScopes { for _, s := range additionalScopes {
set[s] = true set[s] = true
} }

View File

@ -18,6 +18,7 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
@ -123,12 +124,14 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
testName = "test-name" testName = "test-name"
testSecretName = "test-client-secret" testSecretName = "test-client-secret"
testAdditionalScopes = []string{"scope1", "scope2", "scope3"} testAdditionalScopes = []string{"scope1", "scope2", "scope3"}
testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"} testExpectedScopes = []string{"offline_access", "openid", "scope1", "scope2", "scope3"}
testExpectedAdditionalParams = map[string]string{"prompt": "consent"}
testClientID = "test-oidc-client-id" testClientID = "test-oidc-client-id"
testClientSecret = "test-oidc-client-secret" testClientSecret = "test-oidc-client-secret"
testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)} testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)}
testGroupsClaim = "test-groups-claim" testGroupsClaim = "test-groups-claim"
testUsernameClaim = "test-username-claim" testUsernameClaim = "test-username-claim"
testUID = types.UID("test-uid")
) )
tests := []struct { tests := []struct {
name string name string
@ -561,13 +564,13 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
{ {
name: "upstream with error becomes valid", name: "upstream with error becomes valid",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name", UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: append(testAdditionalScopes, "xyz", "openid"), AdditionalScopes: append(testAdditionalScopes, "xyz", "openid", "offline_access"),
AllowPasswordGrant: true, AllowPasswordGrant: true,
}, },
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
@ -598,10 +601,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
UsernameClaim: testUsernameClaim, UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: true, AllowPasswordGrant: true,
AdditionalAuthcodeParams: testExpectedAdditionalParams,
ResourceUID: testUID,
}, },
}, },
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -614,7 +619,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
{ {
name: "existing valid upstream", name: "existing valid upstream",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
@ -651,10 +656,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
UsernameClaim: testUsernameClaim, UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false, AllowPasswordGrant: false,
AdditionalAuthcodeParams: testExpectedAdditionalParams,
ResourceUID: testUID,
}, },
}, },
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -667,7 +674,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
{ {
name: "existing valid upstream with trailing slash", name: "existing valid upstream with trailing slash",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/ends-with-slash/", Issuer: testIssuerURL + "/ends-with-slash/",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
@ -701,10 +708,12 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
UsernameClaim: testUsernameClaim, UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false, AllowPasswordGrant: false,
AdditionalAuthcodeParams: testExpectedAdditionalParams,
ResourceUID: testUID,
}, },
}, },
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -860,6 +869,8 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim()) require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim())
require.Equal(t, tt.wantResultingCache[i].GetGroupsClaim(), actualIDP.GetGroupsClaim()) require.Equal(t, tt.wantResultingCache[i].GetGroupsClaim(), actualIDP.GetGroupsClaim())
require.Equal(t, tt.wantResultingCache[i].AllowsPasswordGrant(), actualIDP.AllowsPasswordGrant()) require.Equal(t, tt.wantResultingCache[i].AllowsPasswordGrant(), actualIDP.AllowsPasswordGrant())
require.Equal(t, tt.wantResultingCache[i].GetAdditionalAuthcodeParams(), actualIDP.GetAdditionalAuthcodeParams())
require.Equal(t, tt.wantResultingCache[i].GetResourceUID(), actualIDP.GetResourceUID())
require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes()) require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes())
// We always want to use the proxy from env on these clients, so although the following assertions // We always want to use the proxy from env on these clients, so although the following assertions

View File

@ -26,6 +26,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -343,6 +344,9 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
func(s *fosite.TokenType, c fuzz.Continue) { func(s *fosite.TokenType, c fuzz.Continue) {
*s = fosite.TokenType(randString(c)) *s = fosite.TokenType(randString(c))
}, },
func(s *types.UID, c fuzz.Continue) {
*s = types.UID(randString(c))
},
// handle string type alias // handle string type alias
func(s *fosite.Arguments, c fuzz.Continue) { func(s *fosite.Arguments, c fuzz.Continue) {
n := c.Intn(3) + 1 // 1 to 3 items n := c.Intn(3) + 1 // 1 to 3 items

View File

@ -18,6 +18,7 @@ import (
oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes" oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes"
pkce "go.pinniped.dev/pkg/oidcclient/pkce" pkce "go.pinniped.dev/pkg/oidcclient/pkce"
oauth2 "golang.org/x/oauth2" oauth2 "golang.org/x/oauth2"
types "k8s.io/apimachinery/pkg/types"
) )
// MockUpstreamOIDCIdentityProviderI is a mock of UpstreamOIDCIdentityProviderI interface. // MockUpstreamOIDCIdentityProviderI is a mock of UpstreamOIDCIdentityProviderI interface.
@ -72,6 +73,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) ExchangeAuthcodeAndVali
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthcodeAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).ExchangeAuthcodeAndValidateTokens), arg0, arg1, arg2, arg3, arg4) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthcodeAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).ExchangeAuthcodeAndValidateTokens), arg0, arg1, arg2, arg3, arg4)
} }
// GetAdditionalAuthcodeParams mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetAdditionalAuthcodeParams() map[string]string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAdditionalAuthcodeParams")
ret0, _ := ret[0].(map[string]string)
return ret0
}
// GetAdditionalAuthcodeParams indicates an expected call of GetAdditionalAuthcodeParams.
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetAdditionalAuthcodeParams() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAdditionalAuthcodeParams", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetAdditionalAuthcodeParams))
}
// GetAuthorizationURL mocks base method. // GetAuthorizationURL mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetAuthorizationURL() *url.URL { func (m *MockUpstreamOIDCIdentityProviderI) GetAuthorizationURL() *url.URL {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -128,6 +143,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetName() *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetName)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetName))
} }
// GetResourceUID mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetResourceUID() types.UID {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetResourceUID")
ret0, _ := ret[0].(types.UID)
return ret0
}
// GetResourceUID indicates an expected call of GetResourceUID.
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetResourceUID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceUID", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetResourceUID))
}
// GetScopes mocks base method. // GetScopes mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetScopes() []string { func (m *MockUpstreamOIDCIdentityProviderI) GetScopes() []string {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -30,7 +30,10 @@ import (
"go.pinniped.dev/pkg/oidcclient/pkce" "go.pinniped.dev/pkg/oidcclient/pkce"
) )
const promptParamNone = "none" const (
promptParamName = "prompt"
promptParamNone = "none"
)
func NewHandler( func NewHandler(
downstreamIssuer string, downstreamIssuer string,
@ -51,13 +54,13 @@ func NewHandler(
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method) return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
} }
oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpLister) oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister)
if err != nil { if err != nil {
plog.WarningErr("authorize upstream config", err) plog.WarningErr("authorize upstream config", err)
return err return err
} }
if oidcUpstream != nil { if idpType == psession.ProviderTypeOIDC {
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 { if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 {
// The client set a username header, so they are trying to log in with a username/password. // The client set a username header, so they are trying to log in with a username/password.
return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream)
@ -74,6 +77,7 @@ func NewHandler(
return handleAuthRequestForLDAPUpstream(r, w, return handleAuthRequestForLDAPUpstream(r, w,
oauthHelperWithStorage, oauthHelperWithStorage,
ldapUpstream, ldapUpstream,
idpType,
) )
})) }))
} }
@ -83,6 +87,7 @@ func handleAuthRequestForLDAPUpstream(
w http.ResponseWriter, w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
ldapUpstream provider.UpstreamLDAPIdentityProviderI, ldapUpstream provider.UpstreamLDAPIdentityProviderI,
idpType psession.ProviderType,
) error { ) error {
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper) authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
if !created { if !created {
@ -108,7 +113,14 @@ func handleAuthRequestForLDAPUpstream(
username = authenticateResponse.User.GetName() username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups) customSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstream.GetResourceUID(),
ProviderName: ldapUpstream.GetName(),
ProviderType: idpType,
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
} }
func handleAuthRequestForOIDCUpstreamPasswordGrant( func handleAuthRequestForOIDCUpstreamPasswordGrant(
@ -147,6 +159,15 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client
} }
if token.RefreshToken == nil || token.RefreshToken.Token == "" {
plog.Warning("refresh token not returned by upstream provider during password grant",
"upstreamName", oidcUpstream.GetName(),
"scopes", oidcUpstream.GetScopes())
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHint(
"Refresh token not returned by upstream provider during password grant."))
}
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
if err != nil { if err != nil {
// Return a user-friendly error for this case which is entirely within our control. // Return a user-friendly error for this case which is entirely within our control.
@ -155,7 +176,15 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
) )
} }
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups) customSessionData := &psession.CustomSessionData{
ProviderUID: oidcUpstream.GetResourceUID(),
ProviderName: oidcUpstream.GetName(),
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: token.RefreshToken.Token,
},
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
} }
func handleAuthRequestForOIDCUpstreamAuthcodeGrant( func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
@ -223,17 +252,20 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
} }
authCodeOptions := []oauth2.AuthCodeOption{ authCodeOptions := []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
nonceValue.Param(), nonceValue.Param(),
pkceValue.Challenge(), pkceValue.Challenge(),
pkceValue.Method(), pkceValue.Method(),
} }
promptParam := r.Form.Get("prompt") promptParam := r.Form.Get(promptParamName)
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired) return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired)
} }
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val))
}
if csrfFromCookie == "" { if csrfFromCookie == "" {
// We did not receive an incoming CSRF cookie, so write a new one. // We did not receive an incoming CSRF cookie, so write a new one.
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
@ -280,8 +312,9 @@ func makeDownstreamSessionAndReturnAuthcodeRedirect(
subject string, subject string,
username string, username string,
groups []string, groups []string,
customSessionData *psession.CustomSessionData,
) error { ) error {
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {
@ -340,13 +373,13 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
} }
// Select either an OIDC, an LDAP or an AD IDP, or return an error. // Select either an OIDC, an LDAP or an AD IDP, or return an error.
func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, error) { func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) {
oidcUpstreams := idpLister.GetOIDCIdentityProviders() oidcUpstreams := idpLister.GetOIDCIdentityProviders()
ldapUpstreams := idpLister.GetLDAPIdentityProviders() ldapUpstreams := idpLister.GetLDAPIdentityProviders()
adUpstreams := idpLister.GetActiveDirectoryIdentityProviders() adUpstreams := idpLister.GetActiveDirectoryIdentityProviders()
switch { switch {
case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0: case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0:
return nil, nil, httperr.New( return nil, nil, "", httperr.New(
http.StatusUnprocessableEntity, http.StatusUnprocessableEntity,
"No upstream providers are configured", "No upstream providers are configured",
) )
@ -362,16 +395,16 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider
upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
} }
plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames) plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames)
return nil, nil, httperr.New( return nil, nil, "", httperr.New(
http.StatusUnprocessableEntity, http.StatusUnprocessableEntity,
"Too many upstream providers are configured (support for multiple upstreams is not yet implemented)", "Too many upstream providers are configured (support for multiple upstreams is not yet implemented)",
) )
case len(oidcUpstreams) == 1: case len(oidcUpstreams) == 1:
return oidcUpstreams[0], nil, nil return oidcUpstreams[0], nil, psession.ProviderTypeOIDC, nil
case len(adUpstreams) == 1: case len(adUpstreams) == 1:
return nil, adUpstreams[0], nil return nil, adUpstreams[0], psession.ProviderTypeActiveDirectory, nil
default: default:
return nil, ldapUpstreams[0], nil return nil, ldapUpstreams[0], psession.ProviderTypeLDAP, nil
} }
} }

View File

@ -30,6 +30,7 @@ import (
"go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/csrftoken"
"go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/internal/testutil/oidctestutil"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
@ -39,7 +40,13 @@ import (
func TestAuthorizationEndpoint(t *testing.T) { func TestAuthorizationEndpoint(t *testing.T) {
const ( const (
oidcUpstreamName = "some-oidc-idp" oidcUpstreamName = "some-oidc-idp"
oidcUpstreamResourceUID = "oidc-resource-uid"
oidcPasswordGrantUpstreamName = "some-password-granting-oidc-idp" oidcPasswordGrantUpstreamName = "some-password-granting-oidc-idp"
oidcPasswordGrantUpstreamResourceUID = "some-password-granting-resource-uid"
ldapUpstreamName = "some-ldap-idp"
ldapUpstreamResourceUID = "ldap-resource-uid"
activeDirectoryUpstreamName = "some-active-directory-idp"
activeDirectoryUpstreamResourceUID = "active-directory-resource-uid"
oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamIssuer = "https://my-upstream-issuer.com"
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
@ -48,6 +55,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
oidcUpstreamPassword = "test-oidc-pinniped-password" //nolint: gosec oidcUpstreamPassword = "test-oidc-pinniped-password" //nolint: gosec
oidcUpstreamUsernameClaim = "the-user-claim" oidcUpstreamUsernameClaim = "the-user-claim"
oidcUpstreamGroupsClaim = "the-groups-claim" oidcUpstreamGroupsClaim = "the-groups-claim"
oidcPasswordGrantUpstreamRefreshToken = "some-opaque-token" //nolint: gosec
downstreamIssuer = "https://my-downstream-issuer.com/some-path" downstreamIssuer = "https://my-downstream-issuer.com/some-path"
downstreamRedirectURI = "http://127.0.0.1/callback" downstreamRedirectURI = "http://127.0.0.1/callback"
@ -146,6 +154,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
"state": happyState, "state": happyState,
} }
fositeAccessDeniedWithMissingRefreshTokenErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Refresh token not returned by upstream provider during password grant.",
"state": happyState,
}
fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery = map[string]string{ fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery = map[string]string{
"error": "access_denied", "error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.", "error_description": "The resource owner or authorization server denied the request. Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.",
@ -208,20 +222,22 @@ func TestAuthorizationEndpoint(t *testing.T) {
upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth") upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth")
require.NoError(t, err) require.NoError(t, err)
upstreamOIDCIdentityProvider := func() *oidctestutil.TestUpstreamOIDCIdentityProvider { upstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(oidcUpstreamName). WithName(oidcUpstreamName).
WithResourceUID(oidcUpstreamResourceUID).
WithClientID("some-client-id"). WithClientID("some-client-id").
WithAuthorizationURL(*upstreamAuthURL). WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow
WithAllowPasswordGrant(false). WithAllowPasswordGrant(false).
WithPasswordGrantError(errors.New("should not have used password grant on this instance")). WithAdditionalAuthcodeParams(map[string]string{}).
Build() WithPasswordGrantError(errors.New("should not have used password grant on this instance"))
} }
passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(oidcPasswordGrantUpstreamName). WithName(oidcPasswordGrantUpstreamName).
WithResourceUID(oidcPasswordGrantUpstreamResourceUID).
WithClientID("some-client-id"). WithClientID("some-client-id").
WithAuthorizationURL(*upstreamAuthURL). WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow
@ -234,6 +250,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership). WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership).
WithIDTokenClaim("other-claim", "should be ignored"). WithIDTokenClaim("other-claim", "should be ignored").
WithAllowPasswordGrant(true). WithAllowPasswordGrant(true).
WithRefreshToken(oidcPasswordGrantUpstreamRefreshToken).
WithAdditionalAuthcodeParams(map[string]string{"should-be-ignored": "doesn't apply to password grant"}).
WithUpstreamAuthcodeExchangeError(errors.New("should not have tried to exchange upstream authcode on this instance")) WithUpstreamAuthcodeExchangeError(errors.New("should not have tried to exchange upstream authcode on this instance"))
} }
@ -254,10 +272,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL) parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
require.NoError(t, err) require.NoError(t, err)
upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
Name: "some-ldap-idp",
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
if username == "" || password == "" { if username == "" || password == "" {
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator") return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
} }
@ -271,11 +286,25 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, true, nil }, true, nil
} }
return nil, false, nil return nil, false, nil
}, }
upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: ldapAuthenticateFunc,
}
upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: ldapAuthenticateFunc,
} }
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: "some-ldap-idp", Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
return nil, false, fmt.Errorf("some ldap upstream auth error") return nil, false, fmt.Errorf("some ldap upstream auth error")
}, },
@ -387,10 +416,9 @@ func TestAuthorizationEndpoint(t *testing.T) {
return encoded return encoded
} }
expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedPrompt string) string { expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedAdditionalParams map[string]string) string {
query := map[string]string{ query := map[string]string{
"response_type": "code", "response_type": "code",
"access_type": "offline",
"scope": "scope1 scope2", "scope": "scope1 scope2",
"client_id": "some-client-id", "client_id": "some-client-id",
"state": expectedUpstreamState, "state": expectedUpstreamState,
@ -399,12 +427,35 @@ func TestAuthorizationEndpoint(t *testing.T) {
"code_challenge_method": downstreamPKCEChallengeMethod, "code_challenge_method": downstreamPKCEChallengeMethod,
"redirect_uri": downstreamIssuer + "/callback", "redirect_uri": downstreamIssuer + "/callback",
} }
if expectedPrompt != "" { for key, val := range expectedAdditionalParams {
query["prompt"] = expectedPrompt query[key] = val
} }
return urlWithQuery(upstreamAuthURL.String(), query) return urlWithQuery(upstreamAuthURL.String(), query)
} }
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: psession.ProviderTypeActiveDirectory,
OIDC: nil,
}
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: psession.ProviderTypeLDAP,
OIDC: nil,
}
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
ProviderName: oidcPasswordGrantUpstreamName,
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: oidcPasswordGrantUpstreamRefreshToken,
},
}
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyState happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyState
@ -452,11 +503,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce string wantDownstreamNonce string
wantUnnecessaryStoredRecords int wantUnnecessaryStoredRecords int
wantPasswordGrantCall *expectedPasswordGrant wantPasswordGrantCall *expectedPasswordGrant
wantDownstreamCustomSessionData *psession.CustomSessionData
} }
tests := []testCase{ tests := []testCase{
{ {
name: "OIDC upstream browser flow happy path using GET without a CSRF cookie", name: "OIDC upstream browser flow happy path using GET without a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -467,7 +519,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -491,6 +543,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path using GET", name: "LDAP upstream happy path using GET",
@ -511,10 +564,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "ActiveDirectory upstream happy path using GET", name: "ActiveDirectory upstream happy path using GET",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -531,10 +585,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream browser flow happy path using GET with a CSRF cookie", name: "OIDC upstream browser flow happy path using GET with a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -545,13 +600,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
{ {
name: "OIDC upstream browser flow happy path using POST", name: "OIDC upstream browser flow happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -565,7 +620,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: "", wantContentType: "",
wantBodyString: "", wantBodyString: "",
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
@ -590,6 +645,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path using POST", name: "LDAP upstream happy path using POST",
@ -612,10 +668,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "Active Directory upstream happy path using POST", name: "Active Directory upstream happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
@ -634,10 +691,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream browser flow happy path with prompt param other than none that gets ignored", name: "OIDC upstream browser flow happy path with prompt param other than none that gets ignored",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -651,12 +709,31 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true,
},
{
name: "OIDC upstream browser flow happy path with extra params that get passed through",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123", "def": "456"}).Build()),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
name: "OIDC upstream browser flow with prompt param none throws an error because we want to independently decide the upstream prompt param", name: "OIDC upstream browser flow with prompt param none throws an error because we want to independently decide the upstream prompt param",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -673,7 +750,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "OIDC upstream browser flow with error while decoding CSRF cookie just generates a new cookie and succeeds as usual", name: "OIDC upstream browser flow with error while decoding CSRF cookie just generates a new cookie and succeeds as usual",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -686,13 +763,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
// Generated a new CSRF cookie and set it in the response. // Generated a new CSRF cookie and set it in the response.
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
{ {
name: "OIDC upstream browser flow happy path when downstream redirect uri matches what is configured for client except for the port number", name: "OIDC upstream browser flow happy path when downstream redirect uri matches what is configured for client except for the port number",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -707,7 +784,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
}, "", ""), ""), }, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -733,6 +810,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number",
@ -755,10 +833,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream browser flow happy path when downstream requested scopes include offline_access", name: "OIDC upstream browser flow happy path when downstream requested scopes include offline_access",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -771,7 +850,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
"scope": "openid offline_access", "scope": "openid offline_access",
}, "", ""), ""), }, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -834,7 +913,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "wrong upstream password for Active Directory authentication", name: "wrong upstream password for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -858,7 +937,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "wrong upstream username for Active Directory authentication", name: "wrong upstream username for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr("wrong-username"), customUsernameHeader: pointer.StringPtr("wrong-username"),
@ -882,7 +961,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing upstream username on request for Active Directory authentication", name: "missing upstream username on request for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: nil, // do not send header customUsernameHeader: nil, // do not send header
@ -906,7 +985,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing upstream password on request for Active Directory authentication", name: "missing upstream password on request for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -916,6 +995,32 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{
name: "return an error when upstream IDP did not return a refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingRefreshTokenErrorQuery),
wantBodyString: "",
},
{
name: "return an error when upstream IDP did not return a refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingRefreshTokenErrorQuery),
wantBodyString: "",
},
{ {
name: "missing upstream password on request for OIDC password grant authentication", name: "missing upstream password on request for OIDC password grant authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
@ -930,7 +1035,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants", name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
@ -942,7 +1047,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow", name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -984,7 +1089,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream redirect uri does not match what is configured for client when using active directory upstream", name: "downstream redirect uri does not match what is configured for client when using active directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{ path: modifiedHappyGetRequestPath(map[string]string{
"redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client",
@ -997,7 +1102,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream client does not exist when using OIDC upstream browser flow", name: "downstream client does not exist when using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1031,7 +1136,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream client does not exist when using active directory upstream", name: "downstream client does not exist when using active directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
@ -1040,7 +1145,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "response type is unsupported when using OIDC upstream browser flow", name: "response type is unsupported when using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1077,7 +1182,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "response type is unsupported when using active directory upstream", name: "response type is unsupported when using active directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
@ -1087,7 +1192,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow", name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1126,7 +1231,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream scopes do not match what is configured for client using Active Directory upstream", name: "downstream scopes do not match what is configured for client using Active Directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -1138,7 +1243,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing response type in request using OIDC upstream browser flow", name: "missing response type in request using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1175,7 +1280,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing response type in request using Active Directory upstream", name: "missing response type in request using Active Directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
@ -1185,7 +1290,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing client id in request using OIDC upstream browser flow", name: "missing client id in request using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1219,7 +1324,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing PKCE code_challenge in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 name: "missing PKCE code_challenge in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1261,7 +1366,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1303,7 +1408,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1345,7 +1450,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1389,7 +1494,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
// This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running
// through that part of the fosite library when using an OIDC upstream browser flow. // through that part of the fosite library when using an OIDC upstream browser flow.
name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow", name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1435,7 +1540,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow", name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1449,7 +1554,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
map[string]string{"prompt": "none login", "scope": "email"}, "", "", map[string]string{"prompt": "none login", "scope": "email"}, "", "",
), ""), ), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -1474,6 +1579,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream",
@ -1495,6 +1601,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups", name: "OIDC upstream password grant: upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups",
@ -1518,6 +1625,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing", name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing",
@ -1543,6 +1651,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value", name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value",
@ -1569,6 +1678,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value", name: "OIDC upstream password grant: upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value",
@ -1596,6 +1706,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value", name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value",
@ -1655,6 +1766,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token has a non-array value", name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token has a non-array value",
@ -1679,6 +1791,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token is a slice of interfaces", name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token is a slice of interfaces",
@ -1703,6 +1816,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream ID token does not contain requested username claim", name: "OIDC upstream password grant: upstream ID token does not contain requested username claim",
@ -1741,6 +1855,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream ID token contains username claim with weird format", name: "OIDC upstream password grant: upstream ID token contains username claim with weird format",
@ -1909,7 +2024,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream state does not have enough entropy using OIDC upstream browser flow", name: "downstream state does not have enough entropy using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1948,7 +2063,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while encoding upstream state param using OIDC upstream browser flow", name: "error while encoding upstream state param using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1962,7 +2077,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while encoding CSRF cookie value for new cookie using OIDC upstream browser flow", name: "error while encoding CSRF cookie value for new cookie using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1976,7 +2091,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while generating CSRF token using OIDC upstream browser flow", name: "error while generating CSRF token using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: sadCSRFGenerator, generateCSRF: sadCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1990,7 +2105,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while generating nonce using OIDC upstream browser flow", name: "error while generating nonce using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: sadNonceGenerator, generateNonce: sadNonceGenerator,
@ -2004,7 +2119,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while generating PKCE using OIDC upstream browser flow", name: "error while generating PKCE using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: sadPKCEGenerator, generatePKCE: sadPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -2027,7 +2142,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "too many upstream providers are configured: multiple OIDC", name: "too many upstream providers are configured: multiple OIDC",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider(), upstreamOIDCIdentityProvider()), // more than one not allowed idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build(), upstreamOIDCIdentityProviderBuilder().Build()), // more than one not allowed
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
@ -2054,7 +2169,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "too many upstream providers are configured: both OIDC and LDAP", name: "too many upstream providers are configured: both OIDC and LDAP",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
@ -2063,7 +2178,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "too many upstream providers are configured: OIDC, LDAP and AD", name: "too many upstream providers are configured: OIDC, LDAP and AD",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&upstreamLDAPIdentityProvider), // more than one not allowed idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // more than one not allowed
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
@ -2072,7 +2187,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "PUT is a bad method", name: "PUT is a bad method",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPut, method: http.MethodPut,
path: "/some/path", path: "/some/path",
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
@ -2081,7 +2196,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "PATCH is a bad method", name: "PATCH is a bad method",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPatch, method: http.MethodPatch,
path: "/some/path", path: "/some/path",
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
@ -2090,7 +2205,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "DELETE is a bad method", name: "DELETE is a bad method",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodDelete, method: http.MethodDelete,
path: "/some/path", path: "/some/path",
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
@ -2166,6 +2281,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, downstreamClientID,
test.wantDownstreamRedirectURI, test.wantDownstreamRedirectURI,
test.wantDownstreamCustomSessionData,
) )
default: default:
require.Empty(t, rsp.Header().Values("Location")) require.Empty(t, rsp.Header().Values("Location"))
@ -2239,6 +2355,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
WithClientID("some-other-new-client-id"). WithClientID("some-other-new-client-id").
WithAuthorizationURL(*upstreamAuthURL). WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}). WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}).
WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123"}).
Build() Build()
idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(newProviderSettings)}) idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(newProviderSettings)})
@ -2246,7 +2363,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(), test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(),
map[string]string{ map[string]string{
"response_type": "code", "response_type": "code",
"access_type": "offline", "prompt": "consent",
"abc": "123",
"scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation "scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation
"client_id": "some-other-new-client-id", // updated expectation "client_id": "some-other-new-client-id", // updated expectation
"state": expectedUpstreamStateParam( "state": expectedUpstreamStateParam(

View File

@ -19,6 +19,7 @@ import (
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/oidc/provider/formposthtml"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession"
) )
func NewHandler( func NewHandler(
@ -68,12 +69,27 @@ func NewHandler(
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens") return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
} }
if token.RefreshToken == nil || token.RefreshToken.Token == "" {
plog.Warning("refresh token not returned by upstream provider during authcode exchange",
"upstreamName", upstreamIDPConfig.GetName(),
"scopes", upstreamIDPConfig.GetScopes(),
"additionalParams", upstreamIDPConfig.GetAdditionalAuthcodeParams())
return httperr.New(http.StatusUnprocessableEntity, "refresh token not returned by upstream provider during authcode exchange")
}
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
if err != nil { if err != nil {
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
} }
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, &psession.CustomSessionData{
ProviderUID: upstreamIDPConfig.GetResourceUID(),
ProviderName: upstreamIDPConfig.GetName(),
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: token.RefreshToken.Token,
},
})
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@ import (
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/internal/testutil/oidctestutil"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
@ -26,8 +27,10 @@ import (
const ( const (
happyUpstreamIDPName = "upstream-idp-name" happyUpstreamIDPName = "upstream-idp-name"
happyUpstreamIDPResourceUID = "upstream-uid"
oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamIssuer = "https://my-upstream-issuer.com"
oidcUpstreamRefreshToken = "test-refresh-token"
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
oidcUpstreamSubjectQueryEscaped = "abc123-some+guid" oidcUpstreamSubjectQueryEscaped = "abc123-some+guid"
oidcUpstreamUsername = "test-pinniped-username" oidcUpstreamUsername = "test-pinniped-username"
@ -70,6 +73,12 @@ var (
"redirect_uri": []string{downstreamRedirectURI}, "redirect_uri": []string{downstreamRedirectURI},
} }
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode() happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode()
happyDownstreamCustomSessionData = &psession.CustomSessionData{
ProviderUID: happyUpstreamIDPResourceUID,
ProviderName: happyUpstreamIDPName,
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamRefreshToken},
}
) )
func TestCallbackEndpoint(t *testing.T) { func TestCallbackEndpoint(t *testing.T) {
@ -130,6 +139,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce string wantDownstreamNonce string
wantDownstreamPKCEChallenge string wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string wantDownstreamPKCEChallengeMethod string
wantDownstreamCustomSessionData *psession.CustomSessionData
wantAuthcodeExchangeCall *expectedAuthcodeExchange wantAuthcodeExchangeCall *expectedAuthcodeExchange
}{ }{
@ -157,6 +167,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -179,6 +190,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -203,6 +215,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -227,6 +240,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -253,6 +267,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -280,6 +295,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -302,6 +318,34 @@ func TestCallbackEndpoint(t *testing.T) {
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
}, },
}, },
{
name: "return an error when upstream IDP did not return a refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: refresh token not returned by upstream provider during authcode exchange\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{
name: "return an error when upstream IDP returned an empty refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: refresh token not returned by upstream provider during authcode exchange\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{ {
name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value", name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
@ -339,6 +383,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -363,6 +408,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -387,6 +433,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -537,6 +584,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -563,6 +611,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -660,6 +709,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -907,6 +957,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, downstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData,
) )
// Otherwise, expect an empty response body. // Otherwise, expect an empty response body.
@ -933,6 +984,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, downstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData,
) )
} }
}) })
@ -1036,6 +1088,7 @@ func (b *upstreamStateParamBuilder) WithStateVersion(version string) *upstreamSt
func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(happyUpstreamIDPName). WithName(happyUpstreamIDPName).
WithResourceUID(happyUpstreamIDPResourceUID).
WithClientID("some-client-id"). WithClientID("some-client-id").
WithScopes([]string{"scope1", "scope2"}). WithScopes([]string{"scope1", "scope2"}).
WithUsernameClaim(oidcUpstreamUsernameClaim). WithUsernameClaim(oidcUpstreamUsernameClaim).
@ -1046,6 +1099,7 @@ func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership). WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership).
WithIDTokenClaim("other-claim", "should be ignored"). WithIDTokenClaim("other-claim", "should be ignored").
WithAllowPasswordGrant(false). WithAllowPasswordGrant(false).
WithRefreshToken(oidcUpstreamRefreshToken).
WithPasswordGrantError(errors.New("the callback endpoint should not use password grants")) WithPasswordGrantError(errors.New("the callback endpoint should not use password grants"))
} }

View File

@ -36,7 +36,7 @@ const (
) )
// MakeDownstreamSession creates a downstream OIDC session. // MakeDownstreamSession creates a downstream OIDC session.
func MakeDownstreamSession(subject string, username string, groups []string) *psession.PinnipedSession { func MakeDownstreamSession(subject string, username string, groups []string, custom *psession.CustomSessionData) *psession.PinnipedSession {
now := time.Now().UTC() now := time.Now().UTC()
openIDSession := &psession.PinnipedSession{ openIDSession := &psession.PinnipedSession{
Fosite: &openid.DefaultSession{ Fosite: &openid.DefaultSession{
@ -46,6 +46,7 @@ func MakeDownstreamSession(subject string, username string, groups []string) *ps
AuthTime: now, AuthTime: now,
}, },
}, },
Custom: custom,
} }
if groups == nil { if groups == nil {
groups = []string{} groups = []string{}

View File

@ -9,6 +9,7 @@ import (
"sync" "sync"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"k8s.io/apimachinery/pkg/types"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
@ -24,6 +25,9 @@ type UpstreamOIDCIdentityProviderI interface {
// GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow.
GetClientID() string GetClientID() string
// GetResourceUID returns the Kubernetes resource ID
GetResourceUID() types.UID
// GetAuthorizationURL returns the Authorization Endpoint fetched from discovery. // GetAuthorizationURL returns the Authorization Endpoint fetched from discovery.
GetAuthorizationURL() *url.URL GetAuthorizationURL() *url.URL
@ -42,6 +46,9 @@ type UpstreamOIDCIdentityProviderI interface {
// flow with this upstream provider. When false, it should not be allowed. // flow with this upstream provider. When false, it should not be allowed.
AllowsPasswordGrant() bool AllowsPasswordGrant() bool
// GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests.
GetAdditionalAuthcodeParams() map[string]string
// PasswordCredentialsGrantAndValidateTokens performs upstream OIDC resource owner password credentials grant and // PasswordCredentialsGrantAndValidateTokens performs upstream OIDC resource owner password credentials grant and
// token validation. Returns the validated raw tokens as well as the parsed claims of the ID token. // token validation. Returns the validated raw tokens as well as the parsed claims of the ID token.
PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error)
@ -68,6 +75,9 @@ type UpstreamLDAPIdentityProviderI interface {
// identifier by being combined with the user's UID, since user UIDs are only unique within one provider. // identifier by being combined with the user's UID, since user UIDs are only unique within one provider.
GetURL() *url.URL GetURL() *url.URL
// GetResourceUID returns the Kubernetes resource ID
GetResourceUID() types.UID
// UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider. // UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider.
authenticators.UserAuthenticator authenticators.UserAuthenticator
} }

View File

@ -264,6 +264,7 @@ func TestManager(t *testing.T) {
"groups": "test-group1", "groups": "test-group1",
}, },
}, },
RefreshToken: &oidctypes.RefreshToken{Token: "some-opaque-token"},
}, nil }, nil
}, },
}).Build() }).Build()

View File

@ -1374,7 +1374,7 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques
Subject: "", // not used, note that callback_handler.go does not set this Subject: "", // not used, note that callback_handler.go does not set this
Username: "", // not used, note that callback_handler.go does not set this Username: "", // not used, note that callback_handler.go does not set this
}, },
Custom: &psession.PinnipedSessionData{ Custom: &psession.CustomSessionData{
OIDC: &psession.OIDCSessionData{ OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: "starting-fake-refresh-token", UpstreamRefreshToken: "starting-fake-refresh-token",
}, },

View File

@ -10,6 +10,7 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
"k8s.io/apimachinery/pkg/types"
) )
// PinnipedSession is a session container which includes the fosite standard stuff plus custom Pinniped stuff. // PinnipedSession is a session container which includes the fosite standard stuff plus custom Pinniped stuff.
@ -18,20 +19,20 @@ type PinnipedSession struct {
Fosite *openid.DefaultSession `json:"fosite,omitempty"` Fosite *openid.DefaultSession `json:"fosite,omitempty"`
// Custom Pinniped extensions to the session data. // Custom Pinniped extensions to the session data.
Custom *PinnipedSessionData `json:"custom,omitempty"` Custom *CustomSessionData `json:"custom,omitempty"`
} }
var _ openid.Session = &PinnipedSession{} var _ openid.Session = &PinnipedSession{}
// PinnipedSessionData is the custom session data needed by Pinniped. It should be treated as a union type, // CustomSessionData is the custom session data needed by Pinniped. It should be treated as a union type,
// where the value of ProviderType decides which other fields to use. // where the value of ProviderType decides which other fields to use.
type PinnipedSessionData struct { type CustomSessionData struct {
// The Kubernetes resource UID of the identity provider CRD for the upstream IDP used to start this session. // The Kubernetes resource UID of the identity provider CRD for the upstream IDP used to start this session.
// This should be validated again upon downstream refresh to make sure that we are not refreshing against // This should be validated again upon downstream refresh to make sure that we are not refreshing against
// a different identity provider CRD which just happens to have the same name. // a different identity provider CRD which just happens to have the same name.
// This implies that when a user deletes an identity provider CRD, then the sessions that were started // This implies that when a user deletes an identity provider CRD, then the sessions that were started
// using that identity provider will not be able to perform any more downstream refreshes. // using that identity provider will not be able to perform any more downstream refreshes.
ProviderUID string `json:"providerUID"` ProviderUID types.UID `json:"providerUID"`
// The Kubernetes resource name of the identity provider CRD for the upstream IDP used to start this session. // The Kubernetes resource name of the identity provider CRD for the upstream IDP used to start this session.
// Used during a downstream refresh to decide which upstream to refresh. // Used during a downstream refresh to decide which upstream to refresh.
@ -40,12 +41,20 @@ type PinnipedSessionData struct {
// The type of the identity provider for the upstream IDP used to start this session. // The type of the identity provider for the upstream IDP used to start this session.
// Used during a downstream refresh to decide which upstream to refresh. // Used during a downstream refresh to decide which upstream to refresh.
ProviderType string `json:"providerType"` ProviderType ProviderType `json:"providerType"`
// Only used when ProviderType == "oidc". // Only used when ProviderType == "oidc".
OIDC *OIDCSessionData `json:"oidc,omitempty"` OIDC *OIDCSessionData `json:"oidc,omitempty"`
} }
type ProviderType string
const (
ProviderTypeOIDC ProviderType = "oidc"
ProviderTypeLDAP ProviderType = "ldap"
ProviderTypeActiveDirectory ProviderType = "activedirectory"
)
// OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider. // OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider.
type OIDCSessionData struct { type OIDCSessionData struct {
UpstreamRefreshToken string `json:"upstreamRefreshToken"` UpstreamRefreshToken string `json:"upstreamRefreshToken"`
@ -58,7 +67,7 @@ func NewPinnipedSession() *PinnipedSession {
Claims: &jwt.IDTokenClaims{}, Claims: &jwt.IDTokenClaims{},
Headers: &jwt.Headers{}, Headers: &jwt.Headers{},
}, },
Custom: &PinnipedSessionData{}, Custom: &CustomSessionData{},
} }
} }

View File

@ -20,6 +20,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
@ -59,12 +60,17 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct {
type TestUpstreamLDAPIdentityProvider struct { type TestUpstreamLDAPIdentityProvider struct {
Name string Name string
ResourceUID types.UID
URL *url.URL URL *url.URL
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error)
} }
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID {
return u.ResourceUID
}
func (u *TestUpstreamLDAPIdentityProvider) GetName() string { func (u *TestUpstreamLDAPIdentityProvider) GetName() string {
return u.Name return u.Name
} }
@ -80,10 +86,12 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
type TestUpstreamOIDCIdentityProvider struct { type TestUpstreamOIDCIdentityProvider struct {
Name string Name string
ClientID string ClientID string
ResourceUID types.UID
AuthorizationURL url.URL AuthorizationURL url.URL
UsernameClaim string UsernameClaim string
GroupsClaim string GroupsClaim string
Scopes []string Scopes []string
AdditionalAuthcodeParams map[string]string
AllowPasswordGrant bool AllowPasswordGrant bool
ExchangeAuthcodeAndValidateTokensFunc func( ExchangeAuthcodeAndValidateTokensFunc func(
@ -105,6 +113,16 @@ type TestUpstreamOIDCIdentityProvider struct {
passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs
} }
var _ provider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{}
func (u *TestUpstreamOIDCIdentityProvider) GetResourceUID() types.UID {
return u.ResourceUID
}
func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalAuthcodeParams() map[string]string {
return u.AdditionalAuthcodeParams
}
func (u *TestUpstreamOIDCIdentityProvider) GetName() string { func (u *TestUpstreamOIDCIdentityProvider) GetName() string {
return u.Name return u.Name
} }
@ -304,12 +322,15 @@ func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder {
type TestUpstreamOIDCIdentityProviderBuilder struct { type TestUpstreamOIDCIdentityProviderBuilder struct {
name string name string
resourceUID types.UID
clientID string clientID string
scopes []string scopes []string
idToken map[string]interface{} idToken map[string]interface{}
refreshToken *oidctypes.RefreshToken
usernameClaim string usernameClaim string
groupsClaim string groupsClaim string
authorizationURL url.URL authorizationURL url.URL
additionalAuthcodeParams map[string]string
allowPasswordGrant bool allowPasswordGrant bool
authcodeExchangeErr error authcodeExchangeErr error
passwordGrantErr error passwordGrantErr error
@ -320,6 +341,11 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUp
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithResourceUID(value types.UID) *TestUpstreamOIDCIdentityProviderBuilder {
u.resourceUID = value
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder {
u.clientID = value u.clientID = value
return u return u
@ -373,6 +399,26 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutIDTokenClaim(claim stri
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalAuthcodeParams(params map[string]string) *TestUpstreamOIDCIdentityProviderBuilder {
u.additionalAuthcodeParams = params
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = &oidctypes.RefreshToken{Token: token}
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = &oidctypes.RefreshToken{Token: ""}
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = nil
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
u.authcodeExchangeErr = err u.authcodeExchangeErr = err
return u return u
@ -387,22 +433,24 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent
return &TestUpstreamOIDCIdentityProvider{ return &TestUpstreamOIDCIdentityProvider{
Name: u.name, Name: u.name,
ClientID: u.clientID, ClientID: u.clientID,
ResourceUID: u.resourceUID,
UsernameClaim: u.usernameClaim, UsernameClaim: u.usernameClaim,
GroupsClaim: u.groupsClaim, GroupsClaim: u.groupsClaim,
Scopes: u.scopes, Scopes: u.scopes,
AllowPasswordGrant: u.allowPasswordGrant, AllowPasswordGrant: u.allowPasswordGrant,
AuthorizationURL: u.authorizationURL, AuthorizationURL: u.authorizationURL,
AdditionalAuthcodeParams: u.additionalAuthcodeParams,
ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
if u.authcodeExchangeErr != nil { if u.authcodeExchangeErr != nil {
return nil, u.authcodeExchangeErr return nil, u.authcodeExchangeErr
} }
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}}, nil return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil
}, },
PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) { PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) {
if u.passwordGrantErr != nil { if u.passwordGrantErr != nil {
return nil, u.passwordGrantErr return nil, u.passwordGrantErr
} }
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}}, nil return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil
}, },
} }
} }
@ -479,6 +527,7 @@ func RequireAuthCodeRegexpMatch(
wantDownstreamNonce string, wantDownstreamNonce string,
wantDownstreamClientID string, wantDownstreamClientID string,
wantDownstreamRedirectURI string, wantDownstreamRedirectURI string,
wantCustomSessionData *psession.CustomSessionData,
) { ) {
t.Helper() t.Helper()
@ -513,6 +562,7 @@ func RequireAuthCodeRegexpMatch(
wantDownstreamRequestedScopes, wantDownstreamRequestedScopes,
wantDownstreamClientID, wantDownstreamClientID,
wantDownstreamRedirectURI, wantDownstreamRedirectURI,
wantCustomSessionData,
) )
// One PKCE should have been stored. // One PKCE should have been stored.
@ -563,6 +613,7 @@ func validateAuthcodeStorage(
wantDownstreamRequestedScopes []string, wantDownstreamRequestedScopes []string,
wantDownstreamClientID string, wantDownstreamClientID string,
wantDownstreamRedirectURI string, wantDownstreamRedirectURI string,
wantCustomSessionData *psession.CustomSessionData,
) (*fosite.Request, *psession.PinnipedSession) { ) (*fosite.Request, *psession.PinnipedSession) {
t.Helper() t.Helper()
@ -634,6 +685,9 @@ func validateAuthcodeStorage(
require.Empty(t, actualClaims.AuthenticationContextClassReference) require.Empty(t, actualClaims.AuthenticationContextClassReference)
require.Empty(t, actualClaims.AuthenticationMethodsReference) require.Empty(t, actualClaims.AuthenticationMethodsReference)
// Check that the custom Pinniped session data matches.
require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom)
return storedRequestFromAuthcode, storedSessionFromAuthcode return storedRequestFromAuthcode, storedSessionFromAuthcode
} }

View File

@ -23,7 +23,7 @@ func NewFakePinnipedSession() *psession.PinnipedSession {
Username: "snorlax", Username: "snorlax",
Subject: "panda", Subject: "panda",
}, },
Custom: &psession.PinnipedSessionData{ Custom: &psession.CustomSessionData{
ProviderUID: "fake-provider-uid", ProviderUID: "fake-provider-uid",
ProviderType: "fake-provider-type", ProviderType: "fake-provider-type",
ProviderName: "fake-provider-name", ProviderName: "fake-provider-name",

View File

@ -20,6 +20,7 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/google/uuid" "github.com/google/uuid"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/utils/trace" "k8s.io/utils/trace"
@ -82,6 +83,9 @@ type ProviderConfig struct {
// Name is the unique name of this upstream LDAP IDP. // Name is the unique name of this upstream LDAP IDP.
Name string Name string
// ResourceUID is the Kubernetes resource UID of this identity provider.
ResourceUID types.UID
// Host is the hostname or "hostname:port" of the LDAP server. When the port is not specified, // Host is the hostname or "hostname:port" of the LDAP server. When the port is not specified,
// the default LDAP port will be used. // the default LDAP port will be used.
Host string Host string
@ -265,6 +269,10 @@ func (p *Provider) GetName() string {
return p.c.Name return p.c.Name
} }
func (p *Provider) GetResourceUID() types.UID {
return p.c.ResourceUID
}
// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base". // Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base".
// This URL is not used for connecting to the provider, but rather is used for creating a globally unique user // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user
// identifier by being combined with the user's UID, since user UIDs are only unique within one provider. // identifier by being combined with the user's UID, since user UIDs are only unique within one provider.

View File

@ -14,6 +14,7 @@ import (
coreosoidc "github.com/coreos/go-oidc/v3/oidc" coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
@ -32,11 +33,13 @@ func New(config *oauth2.Config, provider *coreosoidc.Provider, client *http.Clie
// ProviderConfig holds the active configuration of an upstream OIDC provider. // ProviderConfig holds the active configuration of an upstream OIDC provider.
type ProviderConfig struct { type ProviderConfig struct {
Name string Name string
ResourceUID types.UID
UsernameClaim string UsernameClaim string
GroupsClaim string GroupsClaim string
Config *oauth2.Config Config *oauth2.Config
Client *http.Client Client *http.Client
AllowPasswordGrant bool AllowPasswordGrant bool
AdditionalAuthcodeParams map[string]string
Provider interface { Provider interface {
Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier
Claims(v interface{}) error Claims(v interface{}) error
@ -44,6 +47,14 @@ type ProviderConfig struct {
} }
} }
func (p *ProviderConfig) GetResourceUID() types.UID {
return p.ResourceUID
}
func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string {
return p.AdditionalAuthcodeParams
}
func (p *ProviderConfig) GetName() string { func (p *ProviderConfig) GetName() string {
return p.Name return p.Name
} }